关注微信公众号获取更多资讯,第一时间学习最新前沿渗透技术!
0x01 前言
还是像往常一样对某个APP进行安全测试,由于是支付软件根据安全合规要求APP发送的请求大多是加密的,这点在测试之前已经想好解决方案。根据以前遇到的加密方式,大多都是AES、DES、RSA、SM3、SM4这种单调的加密,秘钥存储在APP客户端或者存储在JS中,只需要通过APP逆向或者JS断点拿到KEY或者公钥,就可以还原加密算法。但是这次遇到的这个支付APP跟往常加密算法存在着很大不同,下文将逐步分析它的加密流程。
0x02 加密分析
打开APP某个功能点,触发HTTP请求,数据包如下,简单的分析一下该请求的结构
从请求头来看,请求头中带有Referer,说明该请求虽然是从APP发出的,但实际还是由APP嵌入的网页发起的请求,利用这点可以通过搜索请求URL找到相关的WEB页面,相关加密算法肯定存在此WEB页面中。这样就不用逆向APP少走弯路。
从请求数据来看,openId是我的手机号;aesKeySign可能是某种签名、加密算法可能与AES有关;encData可能是请求数据;encType是加密类型。
分析完数据包,就该根据分析结果进行相关操作,先看看该请求的URL是由哪个WEB页面发起的
全局从响应包中搜索personProductInfo
发现相关JS文件
查看JS文件的Referer即可获取发起请求的网页
打开该网页
在该网页中全局搜索encType、aesKeySign相关关键字定位加密算法具体位置
最终在0.d4f14.js文件中发现实现加密的函数
具体代码位置在6755至6815行之间
逐行分析关键代码
- 在6761行调用/gapi/mapi-gateway/auth/getPublicKey接口获取服务端RSA公钥
相关请求如下
- 在6722行调用clientRsaKeyUpload函数,客户端生成一对RSA公钥与秘钥
- 在6774行至6780行中,创建了几个变量,大致含义如下
var s = {openId: <手机号>, clientRsaPublicKey: <在第二步生成的客户端公钥>}
var a = new JSencrypt; // 创建一个JSencrypt,也就是RSA对象
Ie.serverRsaPublickey = r.result; // 将第一步获取到的服务端公钥,保存到Ie.serverRsaPublickey中
a.setPublickey(r.result); // 对刚刚创建的RSA对象(a)设置公钥,公钥为r.result(也就是服务端公钥)
- 在6781行至6794行中,调用/gapi/mapi-gateway/auth/getAesKey接口获取AesKey
POST的数据为{"encData": a.encrypt(s)}
用第三步的a
(RSA-服务端公钥)加密了s
(openId-手机号、clientRsaPublicKey-客户端公钥)
思考:这里为什么会用服务端公钥
来加密手机号
以及客户端公钥
,发送给服务器
解释:因为将手机号
与客户端公钥
用服务端公钥
加密后发送给服务端,服务端
可以用服务端私钥
解密获取客户端公钥
与手机号
,利用用户手机号
作为索引
存储用户客户端公钥
与随机生成的AES KEY
,同时将AES KEY
用客户端公钥
加密返回给客户端,这样服务端
与客户端
在互不知道对方RSA私钥的情况下,就完成了数据加密传输,并且每次请求客户端RSA公钥与私钥
是不同的,服务端返回的AES KEY
也是随机生成的,如果中间存在流量劫持设备,想解密流量获取AES KEY
几乎是不可能的
相关请求如下
明文数据:
encData => {"openId":"<手机号>","clientRsaPublicKey":"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCX/IR8dij6y3JKKTubmhRx19CBEQ7/ijuslmcl5CCWm7OcmQ1sH0HiLQUTpdrX5YCDEpx10P9CEETG3Pxru02GZ+lXmI139XJltmH4zsnipKxiRK9z7QSgMI35APd+l20SG9V6WN2mlr4lhAC2EnknV/SHbkX2R8FIpY+Ka+FKQwIDAQAB"}
result => {"aesKey":"05399b76a2984561bf843d0ef62cabc9","clientRsaPublicKeySign":"nUMY8W897L+Wyi4oAloqVAib2TIG1151A5BtiDiPJ6wNw/9WPov0UGVQ7wTDOVQ6WlCWWwsiOLPJu+q3PiAz7Dg4TJTKrd112ktoZ9ytt1SZO1FeRou6HrISzCywFpcp160Yo8LJnWh0j+HMWPraXoc8Tz8SCm9qpfYeJz+plv1hstKwCbAyF/Z9zDz+bsPa72ZaXnqvkjciJs71wge7yqKGcJHna3NXwl2NUi8gRVpBLb2wquJoqXcYUguW2O4G1bDJYW7ysUZjuvMqd+wYloMRa8uoX2BjkmaVWei6nZ9KREf0vTCj17EoV2x7mB7onLQQp/5AzepEhbnYNL4lvBKg33T9eB/Zfp6ilQUdIles9HSmx8ooT8vdd1J2mDOebGKe0RVMX3OlR0NB8F4hvOlcPOlhcjCXVOh1yfG+czqFSkcX5r1RsUl086BxlLtvUYT1m3RnvXqGSata3rWi8cIWWpNXlgeLPQvBLLwfID0ro4S98GuBxQ5kDr5utaZyMz5/PPnn2QhggM44/BYMFVqBrXg3SRLyF+tvrvsnOyDBvUbxwZR7KP+VoDLt+1yhgdUyz9BkHClzqVhoEkFRQtr7LQOZ0N16AUhL8aAPQv/QbOrd/bcn7AqLCRDTSA0QLHl2vp11qpBvXrRIv/Tozg6CE0DttxUl9hu1KvDqoCo="}
- 在6797行至6803行中,客户端用客户端私钥
Ie.clientKey.clientRsaPrivateKey
解密了第四步中的响应数据,获取到clientRsaPublicKeySign,使用服务端公钥验证了客户端公钥Ie.clientKey.clientRsaPublicKey没有被篡改。获取到aesKey保存到Ie.aesKey
- 在6804至6816行中
var a = GibberishAES.aesEncrypt(e, Ie.aesKey); //使用Ie.aesKey对用户数据(e)进行加密
o.setPrivateKey(Ie.clientKey.clientRsaPrivateKey); // o(RSA)设置客户端私钥
var c = o.sign(Ie.aesKey, CryptoJS.SHA512, "sha512"); //o(RSA)对Ie.aesKey进行SHA512进行签名
i({result: {openId: t,aesKeySign: c,encData: a,encyType: "C006"},success: !0}) // 发送请求
使用获取到的 Ie.aesKey 对用户数据进行加密
- 服务端收到请求后,利用数据包中的
openId(手机号)
获取到存储的客户端公钥
验证aesKey
没有被篡改,再获取之前生成的aesKey
解密encData
,服务端使用aesKey
加密数据返回给客户端,客户端使用aesKey
解密,至此加密流程结束
0x03 总结
- 客户端生成RSA公钥以及秘钥
- 客户端发送请求获取服务端的公钥
- 客户端使用服务端的公钥加密客户端的公钥并发送给服务器
- 服务器使用服务器端的私钥解密请求获取客户端公钥,服务端根据请求包中的OpenId生成一个新的AES KEY,并使用客户端公钥加密AES KEY,服务端使用服务端私钥对客户端公钥进行SHA512签名,将以上数据使用客户端公钥加密后返回给客户端
- 客户端使用客户端私钥解密数据,获取AES KEY与签名数据,客户端使用之前获取的服务端公钥对签名进行验证,如果验证成功则继续进行加密流程
- 客户端使用该KEY加密用户数据,再使用客户端私钥对AES KEY进行签名验证,将结果发送给服务器
- 服务器使用AES KEY解密用户数据,并对发送过来的AES KEY的签名进行验证
加密流程分析完成后,还需要将网页中的JS代码扣下来完善成加密脚本以便之后修改参数对APP进行安全测试。然后仔细观察一下加密流程,还是有一处BUG的,用于存储用户aesKey
与客户端公钥
的索引是手机号。当用户的手机号泄露后,攻击者可以根据手机号构造请求无限刷新服务端存储的aesKey
与客户端公钥
,由于这两个变量需要用户客户端
与服务端
保持一致才能正常进行解密流程,当攻击者出现打乱他们之间的同步,就会造成客户端与服务端无法正常解密用户数据的现象。