文章总结: 本文记录了一次针对大厂系统的渗透测试过程。面对AES+RSA混合加密,作者通过逆向JS分析出加密逻辑并编写Python脚本成功模拟请求。随后尝试利用该机制进行orderby注入,但因后端校验严格仅允许数字导致利用失败。文章强调了前端逆向的重要性并建议结合mitmproxy等工具优化自动化测试流程。 综合评分: 86 文章分类: 渗透测试,WEB安全,逆向分析,实战经验
记一次AES+RSA混合加密orderby注入失败的经历
原创
oLy
小趴菜网安学习路
2025年12月28日 14:02 江苏
背景
这个系统某大厂的知名产品,前几年 hw 爆出过来严重 0day,这次也是客户要求必须周六做测试,又又又遇到这个产品了,这次遇到主要是一个功能点感觉存在 SQL 注入的可能性,但是多重加密,首先就得逆向。
除了整个请求体加密之外,请求头里多了Idp-Header&Idp-Content&FContext三个字段,其中Idp-Header值固定不变,FContext对请求没有影响,因此只需要搞清楚Idp-Header&请求体data的加密逻辑即可
AES+RSA混合加密
首先定位xhr相关加密js文件
手动添加xhr断点,重新刷新页面,定位加密数据字段,通过断点已经知道请求体data加密是由wrapInfo.data传入wrapInfo是由getWrapParams(api, data)赋值,到这里data已经是加密内容
跟一下getWrapParams(api, data)函数,任意下个断点,data加密,Idp-Header加密,首先看dataObj的三元运算符, 如果data为真赋值JSON.parse(data) 给dataObj,将dataObj转换为一个对象 ,否则将data赋值给dataObj,根据断点结果,dataObj被赋予的是JSON.parse(data)的值,因为data值是加密的
打印一下两个值,可以看到是一样的,上面的JSON.stringify(dataObj)只是json解析的结果,dataObj为json转换为对象的结果
在getWrapParams函数中存在一个if逻辑,可以得到Idp-Header是如何生成的。
通过encData = idp.utils.randomEncrpty(JSON.stringify(dataObj))的encData.key赋值给Idp-Header,encData.data赋值给data,那接下来就需要跟idp.utils.randomEncrpty(JSON.stringify(dataObj))的逻辑
跟进idp.utils.randomEncrpty方法,key是由idp.utils.rsa(idp.utils.getRandomKey())随机生成,还有rsa字样,那key的随机值可能就是rsa加密的,data是由idp.utils.jse(str, key)生成,其中str就是上面传入的dataObj
继续跟进idp.utils.rsa方法,发现紧跟着idp.utils.jse方法,而且AES的key&iv也都给出了,开发很友好,还给出了rsa的私钥(但这里的私钥是另一个公钥所对应的),根据上面的key生成逻辑,utils.rsa中返回的return data就是最终rsa加密的key的值,也就最终的Idp-Header,因此Idp-Header的加密逻辑已经很清楚了。
这里附一张Idp-Header的生成公式
(说一下这里为什么必须要替换Idp-Header,因为后端校验了这个字段,不添加这个请求头,后端直接报错,不像上面的分析的FContext请求头不影响请求)
继续分析data会进入getAesString(data, newKey || key, iv)函数
跟进去,aes加密,key&iv已知,CBC模式,Pkcs7填充方式,最终data=decryptedStr(一开始还以为跟错了,这命名decrypteStr分明就是解密,但是函数实现的加密过程,难道是开发老哥的障眼法?)
到此请求体data的加密的逻辑也很清楚了,再附一张公式图
模拟请求
知道加密逻辑之后,直接python脚本模拟加密逻辑进行请求即可
import base64import jsonimport randomimport requests
from Crypto.Cipher import AESfrom Crypto.Util.Padding import padfrom Crypto.PublicKey import RSAfrom Crypto.Cipher import PKCS1_v1_5
PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----# 填入公钥-----END PUBLIC KEY-----"""
def get_random_key(): """ 模拟 JS 中的 idp.utils.getRandomKey() 返回一个 16 位的随机字符串,字符集为 [0-9a-zA-Z] """ charset = ( "0123456789" "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ) # 字符串索引更快一些 return ''.join(random.choice(charset) for _ in range(16))
def get_idpHeader(plaintext: str, public_key_pem: str) -> str: """ 模拟 JSEncrypt.encrypt() 行为: - 使用 PKCS#1 v1.5 填充 - 输出 Base64 编码字符串 """ # 1. 导入公钥 public_key = RSA.import_key(public_key_pem)
# 2. 创建加密器(PKCS1_v1_5 = JSEncrypt 默认) cipher = PKCS1_v1_5.new(public_key)
# 3. 加密动态随机 key encrypted = cipher.encrypt(rand_key.encode('utf-8')) return base64.b64encode(encrypted).decode('utf-8')
def generate_data(plaintext_json, random_key): iv = b"iv值" key = random_key.encode('utf-8') # e.g., "aB3xK9mQpL2wR8nT"
cipher = AES.new(key, AES.MODE_CBC, iv) padded = pad(plaintext_json.encode('utf-8'), AES.block_size) ciphertext = cipher.encrypt(padded) return base64.b64encode(ciphertext).decode('utf-8')
def post_req(idpHeader, encdata): url = "http://ip/api/path" cookies = {"xxx_session": "session"} headers = {"Pragma": "no-cache", "Cache-Control": "no-cache", "Idp-Header": idpHeader, "Idp-Content": "decode", "X-Requested-With": "XMLHttpRequest", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36", "Accept": "application/json, text/javascript, */*; q=0.01", "Content-Type": "application/json", "Origin": "http://ip", "Referer": "http://ip/path", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Connection": "close"} response = requests.post(url, headers=headers, cookies=cookies, data=encdata) print("状态码:", response.status_code) print("响应内容 (文本):") print(response.text)
dataJson = """{}"""
rand_key = get_random_key()idpHeader = get_idpHeader(rand_key, PUBLIC_KEY_PEM)encrypted_data = generate_data(dataJson, rand_key)post_req(idpHeader, encrypted_data)
# print(f"随机key: {rand_key}")# print(f"获取Idp-Header: {idpHeader}")# print(f"data值为: {data}")
失败的orderby注入
使用原有请求体测试,正常回显,和burp返回一样(这里忘截图了)
直接上测试payload的截图,因为专门找的order的点,但是发现后端校验是真严格啊。
orderby做了校验,除了数字,什么都不能加,连个,也不能加,这后端校验的跟铜墙铁壁似的(主要是我太菜了)
好了,经过hw锤炼过的系统就是坚挺多了,下机开摆了。
写在最后
这个脚本还可以进行优化,配合mitmproxy进行动态注入加密请求和请求头,但是因为这里只发现这一个orderby的功能点,没有联动mitmproxy进行全局替换。如果要在burp中需要抓取明文请求,需要在请求体加密之前进行Js-Hook替换抓到明文包,继而转发到mitmproxy替换加密内容即可。
这种情况其实也很常见,并不是逆向之后就一定会出洞,但是需要尽可能的去发现各种可能。
安全经过这几年的发展,前端对抗也变得越来越重要,尤其金融类的产品,基本上全局加密。最后,快新年了,祝各位师傅漏洞多多,赏金多多~
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:小趴菜网安学习路 oLy《记一次AES+RSA混合加密orderby注入失败的经历》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论