H5业务逻辑漏洞实战:从负数金额到签名算法绕过 1. 项目概述一次典型的H5业务逻辑漏洞挖掘之旅最近在复盘一个很有意思的实战案例目标是一个典型的混合开发APP其核心业务逻辑都封装在H5页面里。整个过程从发现一个看似简单的“负数金额”漏洞开始逐步深入最终绕过了其核心的签名校验机制揭示了这类应用在安全设计上常见的薄弱环节。这不仅仅是两个独立漏洞的发现更是一次完整的攻击路径演示非常适合刚接触APP或Web渗透测试的朋友理解业务逻辑漏洞的挖掘思路。如果你对抓包、前端代码审计、签名算法逆向感兴趣那么这个案例能给你提供一个非常清晰的实操范本。简单来说这次实战的目标是一个水电卡缴费系统。用户可以通过APP进行水卡充值、转账以及缴纳电费。测试环境是我自己搭建的模拟靶场完全复现了真实场景中的逻辑。整个渗透过程的核心思路是先通过常规的参数篡改测试发现低垂的果实负数金额漏洞再利用混合APP的特性前端代码暴露深入分析其安全防护机制签名算法最终实现完全绕过。这种由浅入深、环环相扣的测试方法在实际的授权渗透测试中非常有效。2. 环境准备与目标分析2.1 为什么选择混合开发APP作为切入点在移动应用渗透测试中目标类型的选择往往决定了测试的难易度和入门门槛。原生APPNative App通常需要逆向工程、动态调试如Frida Hook等相对高阶的技术对新手不太友好。而混合开发APPHybrid App则是一个绝佳的起点。混合APP的本质是在一个原生容器通常是WebView中运行网页H5。这意味着业务逻辑在前端大量的核心业务逻辑包括参数校验、数据组装、甚至部分加密签名算法都是用JavaScript编写的并随着H5资源文件下发到客户端。通信透明H5页面与后端服务器的通信依然是标准的HTTP/HTTPS协议我们可以用常规的抓包工具如Burp Suite、Charles、Reqable轻松拦截和修改。代码可读前端JS代码虽然可能被压缩或混淆但相较于原生二进制代码其可读性和可分析性要高得多。本次实战的目标应用就是一个使用“原生壳H5页面”架构的典型物业缴费APP。判断方法很简单抓包时观察请求URL或静态资源路径如果出现*h5*、*web*等字眼或者直接能抓到.html文件的请求基本就可以确定。更彻底一点可以解压APK文件查看assets或res目录下是否存在大量的html、js、css文件。2.2 工具链准备工欲善其事必先利其器。针对这类H5渗透测试我习惯使用以下工具组合它们覆盖了从流量捕获到代码分析的完整链条抓包工具Burp Suite / Charles老牌且功能全面的代理工具适合深度测试和漏洞利用。但对于某些快速重定向或WebSocket请求可能需要精细配置。Reqable这是我近年来非常喜欢的一款国产抓包工具界面现代化对HTTP/HTTPS、WebSocket的支持很好特别是在处理APP请求时非常稳定。本次实战中因为目标应用在操作成功后页面会立即跳转浏览器开发者工具F12的Network面板经常来不及捕获完整的请求/响应Reqable的全局抓包能力就派上了大用场。浏览器开发者工具Chrome或Edge的DevTools是分析前端代码的利器。主要用到以下几个面板Network查看所有网络请求复制为cURL命令重放请求。Sources查看、搜索和调试前端JavaScript代码。可以设置断点单步跟踪变量和函数执行。Console执行JS代码测试函数查看日志输出。代码编辑与脚本工具VS Code / Sublime Text用于查看和格式化JS代码。Python3 requests库用于编写POC脚本模拟签名算法自动化发送测试请求。这是绕过签名校验的关键一步。模拟靶场环境为了合法、安全地学习和研究我基于真实漏洞逻辑在本地搭建了模拟靶场。强烈建议所有安全研究人员都在授权环境或自建靶场中进行测试严格遵守法律法规。3. 第一阶段漏洞挖掘负数金额的逻辑缺陷3.1 充值功能的初步测试拿到目标APP后我首先从最核心的“充值”功能开始测试。流程很简单输入充值金额例如50点击提交。同时打开抓包工具这里我用Reqable监控所有流量。一个正常的充值请求包可能如下所示POST /api/v1/recharge HTTP/1.1 Host: target.com Content-Type: application/x-www-form-urlencoded user_id12345amount50.00order_id20240320001我的第一个测试点就是修改amount参数。将正数50改为0、改为一个极大的数如999999或者改为一个负数-50然后重放Replay这个请求观察后端响应和账户余额的实际变化。注意在测试金融类操作时务必使用测试账户并确认环境是隔离的。直接在生产环境测试是极其危险且违法的行为。3.2 漏洞原理与利用当我将amount参数修改为-50并重放请求后服务器返回了“充值成功”。查询我的账户余额发现不是减少了50而是增加了50这就是典型的“负数金额”逻辑漏洞。其根本原因在于后端服务器在处理业务逻辑时缺乏对关键参数这里是金额的有效性校验。我们来看一下问题代码可能的样子# 有漏洞的后端逻辑伪代码 def recharge(user_id, amount): user get_user(user_id) # 错误没有校验 amount 0 user.balance amount # 如果 amount -50 则 balance balance (-50) user.save() return “充值成功”正确的逻辑应该在进行余额操作前增加一道校验# 修复后的后端逻辑伪代码 def recharge(user_id, amount): if amount 0: return “充值金额必须大于零” user get_user(user_id) user.balance amount user.save() return “充值成功”这个漏洞的危害非常直观攻击者可以通过“充值”负数无限增加自己的账户余额。3.3 漏洞的横向扩展转账功能发现充值漏洞后我立刻进行了横向思维同一个系统同一个开发团队充值功能有漏洞那么类似的“转账”功能呢果然在测试转账功能时遇到了和充值一样的情况前端页面虽然对输入框做了限制不允许输入负数但通过抓包修改amount为负数请求同样成功。转账漏洞的后果有时比充值更严重。假设A向B转账-100元A的余额变化balance_A balance_A - (-100) balance_A 100A的余额增加了B的余额变化balance_B balance_B (-100) balance_B - 100B的余额被扣除了这就造成了“我转给你负钱我反而赚钱你却被扣钱”的荒谬局面业务逻辑完全失控。实操心得在发现某一类漏洞如参数未校验后一定要立即对系统内所有功能相似、接口相似的模块进行“地毯式”测试。开发人员往往会复制粘贴代码导致同一个漏洞模式在多个地方出现。4. 深入核心业务电费缴纳与签名机制的发现4.1 业务深入与新的挑战充值和水卡转账属于“资金入口”的漏洞那么资金消耗的“业务出口”呢我顺着业务流程测试了“电费缴纳”功能。流程是输入电表号查询欠费金额然后进行支付。抓取支付请求包后我发现了一个关键变化POST /api/v1/pay_electric HTTP/1.1 Host: target.com Content-Type: application/x-www-form-urlencoded meter_noEB123456amount100.00×tamp1647841234signa1b2c3d4e5f67890...相比之前的接口这里多了一个sign参数。这是一个非常明显的信号后端可能对请求参数进行了签名校验以防止参数在传输过程中被篡改。我尝试像之前一样直接将amount从100.00改为-100.00然后重放请求。服务器返回了明确的错误“签名验证失败”。这说明签名机制生效了它检测到了我对amount参数的篡改。4.2 签名机制的原理分析签名是一种常见的数据完整性校验和身份验证机制。其基本原理是客户端将需要发送的参数不包括sign本身按照一定规则如按字母排序拼接成一个字符串然后与一个只有客户端和服务器知道的“密钥”Secret Key组合最后对这个组合字符串进行哈希运算如MD5、SHA256得到的结果就是sign值。客户端将参数和计算出的sign一起发送给服务器。服务器收到请求后用同样的规则和密钥对收到的参数重新计算一次签名。服务器比较自己计算出的签名和请求中传来的sign值。如果一致说明参数未被篡改如果不一致则拒绝请求。这个机制的关键在于“密钥”的保密性。如果密钥泄露或者签名算法被攻击者掌握那么签名就形同虚设。5. 第二阶段漏洞挖掘逆向前端签名算法5.1 定位签名生成代码既然签名校验阻止了我们直接修改参数那么下一步就是分析这个sign是如何生成的。在混合APP中签名算法有很大概率就写在前端的JavaScript代码里。我打开浏览器开发者工具Sources面板在支付页面触发一次缴费操作同时在Network面板找到那个带sign的请求。然后我在Sources面板中全局搜索CtrlShiftF关键词如sign、generateSign、md5、加密等。很快我找到了关键代码。通常开发者会有一个名为utils.js、common.js或包含sign字样的文件。通过搜索我定位到了一个名为generateSign的函数并在支付按钮的点击事件处理函数中找到了对它的调用。我在generateSign函数入口处打了一个断点再次触发支付。当断点命中时所有局部变量、传入参数都清晰可见。我看到了传入的params对象包含了meter_no、amount、timestamp唯独没有sign。这印证了签名计算不包含sign自身的常识。5.2 算法逆向与分析一步步单步执行F10我清晰地看到了签名算法的每一步function generateSign(params) { // 1. 获取所有参数名过滤掉sign本身然后按字母顺序排序 // 假设 params {meter_no: “EB123456”, amount: “100.00”, timestamp: “1647841234”, sign: “...”} // 则 sortedKeys [“amount”, “meter_no”, “timestamp”] const sortedKeys Object.keys(params).filter(k k ! ‘sign’).sort(); // 2. 将参数按 keyvalue 格式拼接用 连接 // signStr “amount100.00meter_noEB123456×tamp1647841234” let signStr sortedKeys.map(k ${k}${params[k]}).join(‘’); // 3. 在末尾追加密钥关键密钥硬编码在前端 signStr ‘keyWaterCard2024#SecretKey’; // 密钥暴露了 // 4. 对拼接后的字符串进行MD5哈希得到签名 return md5(signStr); // 返回类似 ‘a1b2c3d4e5f67890...’ 的字符串 }至此签名算法完全清晰排序将所有非sign参数按键名升序排列。拼接将排序后的参数转换为keyvalue形式用连接。加盐在拼接好的字符串末尾加上固定的连接符和密钥字符串keyWaterCard2024#SecretKey。哈希对最终字符串计算MD5值作为签名。最大的安全问题暴露了密钥WaterCard2024#SecretKey被硬编码Hard-Coded在前端JS代码中对于任何可以访问前端代码的用户而所有用户都可以这个密钥就不再是秘密。这是混合APP乃至Web应用非常常见的安全反模式。5.3 编写POC脚本绕过签名掌握了算法和密钥我就可以在服务器外自主计算任何参数组合对应的合法签名了。我立刻写了一个Python脚本来实现这个算法import hashlib import urllib.parse import time def generate_sign(params, secret_key‘WaterCard2024#SecretKey’): “”” 根据逆向的算法生成签名 params: 参数字典不应包含 ‘sign’ 键 secret_key: 从前端代码中提取的密钥 “”” # 1. 过滤并排序键 sorted_keys sorted([k for k in params.keys() if k ! ‘sign’]) # 2. 拼接 keyvalue 字符串 sign_str ‘’.join([f‘{k}{params[k]}’ for k in sorted_keys]) # 3. 追加密钥 sign_str f‘key{secret_key}’ # 4. 计算MD5 md5_hash hashlib.md5() md5_hash.update(sign_str.encode(‘utf-8’)) return md5_hash.hexdigest() # 测试用例正常缴费100元 params { ‘meter_no’: ‘EB123456’, ‘amount’: ‘100.00’, ‘timestamp’: str(int(time.time())) # 通常使用当前时间戳 } sign generate_sign(params) print(f“正常签名: {sign}”) # 可以构造请求params[‘sign’] sign # 攻击用例缴纳-100元“电费” attack_params { ‘meter_no’: ‘EB123456’, ‘amount’: ‘-100.00’, # 篡改金额为负数 ‘timestamp’: params[‘timestamp’] # 使用相同的时间戳 } attack_sign generate_sign(attack_params) print(f“攻击签名: {attack_sign}”)运行脚本我成功得到了篡改后参数amount-100.00对应的“合法”签名。用这个签名替换原请求中的sign值再次发送请求服务器返回了“缴费成功”5.4 漏洞的复合影响这个绕过签名后的负数金额漏洞危害性比之前的充值和转账更大因为它直接影响了物理世界的计量系统。在这个水电卡系统中缴纳电费可能伴随着电表读数的更新。如果缴纳-100度电的费用资金层面用户账户会增加100元的余额因为扣除了负数的费用。计量层面用户的电表读数可能被减少100度。这意味着不仅“赚钱”还可能“偷电”。这体现了业务逻辑漏洞的一个特点漏洞的危害性不仅取决于漏洞本身更取决于它所处的业务上下文。同一个“负数金额”漏洞在充值模块是金融损失在电费缴纳模块就可能演变为涉及计量欺诈的严重安全问题。6. 漏洞挖掘的通用思路与防御建议6.1 针对H5/混合APP的渗透测试方法论通过这个案例我们可以总结出一套针对混合APP或Web前端业务逻辑测试的通用流程信息收集与目标分析确认目标为混合APP抓包识别主要功能接口和参数。基础参数篡改测试对每个接口的关键参数如ID、金额、数量、状态进行边界值测试负数、0、极大值、特殊字符。发现初级漏洞如本例的未校验负数金额。这类漏洞往往因为后端过度信任前端校验或简单遗漏导致。识别防护机制测试中遇到如签名校验、Token验证等阻拦时不要轻易放弃。这标志着进入了更深层的测试。逆向前端逻辑利用开发者工具分析JS代码寻找校验算法的实现。重点搜索encrypt、sign、md5、sha、check、validate等关键词并跟踪核心函数的调用栈。算法还原与密钥查找分析出算法步骤并特别注意查找硬编码的密钥、盐值或IV。这是突破防线的关键。编写POC实现绕过使用Python等脚本语言复现签名算法构造合法的恶意请求。漏洞串联与扩大影响思考如何将绕过后的漏洞与之前发现的漏洞结合或者应用到其他模块评估最大危害。6.2 开发者的安全防御方案对于开发者和企业来说如何避免这类漏洞后端进行完整的业务校验这是铁律永远不要信任前端传来的数据。所有业务规则校验金额0、库存充足、状态合法必须在后端严格进行。前端校验仅用于提升用户体验。签名密钥必须保密签名或加密用的密钥、盐值等敏感信息绝对不可以硬编码在客户端代码如JS、APP中。它们应该存储在服务器端的安全配置中心或环境变量中。客户端不参与核心签名的生成或者使用一次性的临时Token。使用更安全的签名方案非对称加密考虑使用RSA等非对称加密。服务器持有私钥用于生成签名客户端或前端使用公钥进行验证。即使公钥暴露攻击者也无法伪造签名。动态密钥可以为每个会话或每次请求生成动态的签名因子增加预测难度。加入随机数Nonce在签名参数中加入服务器下发的随机数并确保一次性使用防止重放攻击。对关键业务操作进行多重校验对于支付、转账、重要状态变更等操作除了签名还可以加入短信验证码、二次密码、人工审核等机制。定期安全审计与代码审查建立代码审查制度重点关注业务逻辑校验点和敏感信息处理。定期进行渗透测试和安全扫描主动发现潜在问题。7. 常见问题与排查技巧实录在实际的测试和后续的复盘中我遇到并总结了一些典型问题和技巧这里分享给大家问题抓包工具抓不到APP的HTTPS请求排查这是因为没有安装抓包工具的CA证书到手机系统信任区。仅安装在用户证书区APP默认是不信任的。解决对于Android 7.0以上需要将Burp/Charles的CA证书导出并借助Magisk等工具进行系统级安装或者将APP的网络安全配置android:networkSecurityConfig设置为允许用户证书。对于测试更方便的方法是使用已Root的模拟器或测试机。问题前端JS代码被压缩混淆完全看不懂怎么办技巧首先尝试使用浏览器的“Pretty Print”功能Sources面板中{}图标格式化代码。对于常见混淆可以搜索关键字符串如API端点/api/v1/、参数名amount这些字符串通常不会被混淆。找到疑似函数后通过断点调试观察其输入输出反向推导功能而不必完全理解每一行代码。问题签名算法看起来复杂逆向困难技巧善用“Hook”思想。即使不逆向算法也可以在前端代码中“劫持”签名函数。例如在控制台重新定义generateSign函数让它先打印出传入的参数和计算前的原始字符串再调用原函数。这样就能直接窃取到正确的签名逻辑和密钥。这是一种“黑盒”式的动态分析技巧。问题时间戳timestamp参数是签名的一部分服务器会校验吗分析是的通常服务器会校验时间戳以防止重放攻击Replay Attack。常见策略是检查客户端时间戳与服务器时间的差值比如超过5分钟就拒绝请求。绕过在构造攻击请求时需要生成一个当前的有效时间戳。如果服务器校验非常严格如要求时间戳单调递增可能需要更精细的同步。但在很多实现不严的场景直接使用当前时间戳即可。问题除了MD5还遇到其他哈希或加密算法怎么办思路方法不变。核心是找到算法和密钥。常见的算法如SHA1、SHA256、HMAC、AES等在JS中都有标准的库如CryptoJS。在代码中搜索这些算法库的初始化代码或密钥字符串。对于自定义的加密函数则需要耐心跟踪其每一步操作。这次从简单的负数金额到签名绕过的H5渗透实战清晰地展示了一条由浅入深的测试路径。它告诉我们很多看似坚固的防护如签名其弱点可能就暴露在最容易访问的地方。对于防御方而言牢记“不信任客户端”和“保护密钥”的原则对于学习渗透测试的朋友这个案例则提供了一个完美的模板保持好奇心遇到阻碍时深入分析往往就能发现通往核心的钥匙。在安全的世界里逻辑的严谨性永远是第一道也是最后一道防线。