渗透测试请求加密及签名解决方案
简介......
不是所有加密都能过 WAF 的,有些在都走的网关加密,在安全设备里会卸掉加密。
目录
Web
首先需要熟练用 DeveloperTools:
- https://alan.norbauer.com/articles/browser-debugging-tricks#logpoints--tracepoints
- https://sspai.com/post/85686
- https://juejin.cn/post/7235663093748940837
- https://blittle.github.io/chrome-dev-tools/network/README.html
调试 JS:
有时无法格式化,文件大在线工具也不支持,可以在 “Sources -> Snippets” 选项卡中粘贴,点 Pretty print 格式化查看。
如何调试现有页面 JS?打开开发者工具,找到源代码选项卡下的 Override(覆盖),勾选后刷新页面,就可以编辑对应 JS,编辑完保存后会再指定文件夹中写入编辑后的脚本,后续网站则使用编辑后的脚本执行 JS。
分析加密特征-了解常见的加密特征
加密:对称或非对称对传输数据做加密。常见对称加密(AES/DES/SM4)的加密结果大多都是十六进制或者 Base64 编码,非对称加密(RSA/SM2)每次加密结果不同,对称每次加密结果相同。有些加密结果传递的可能是二进制数据,而不是加密结果。
哈希:哈希摘要算法(MD5/SHA-1/SHA-2/SM3)对整个请求包做签名。有的也有用 HMAC,这个 HCMA 也可以通过 Key 来校验数据。
编码:URL、Base、Unicode
定位加密位置(寻找加密方法)
手动调试阶段加解密推荐使用离线工具 CyberChef。
关键字定位:
- Ctrl + Shift + F 全局搜索全局搜索加解密关键字或加密库关键字,通用关键字 encrypt、crypt,如对称加密:aes、sm4、ecb、cbc、key、mode,非对称加密:sm2、rsa。
- 搜索 URL Pattern 接口名称,例如:/api/username
- 搜索加密数据参数名
- 有些会用 Axios 拦截器前置加解密,搜 interceptor,interceptor.request.use,interceptor.response.use
- 比较老的前端应用可能还存在用 JQuery,这种通过取标签中值加密的情况,可以搜索 HTM id/class 找到哪里获取的 value,上下文去翻最终存在那个变量,应该会有请求时会调用此变量。
断点分析:
找到疑似加密的方法后,下断点后上下文分析,再找不到就堆栈回溯找调用链,来确定加密前穿过来的参数和我们输入的参数是否一致,最后再次确定加密后的参数匹配和真实请求中的加密参数是否一致。
- 请求断点:XHR/fetch Breakpoints
- 网络面板选中具体请求,找到启动器面板,通过请求调用堆栈回溯
- JS 表达式断点:对指定 JS 指定行数断点
- 事件断点:如 mouseclick
反调试:
Q:遇到无限 Debugger 怎么解决
A:尝试更换浏览器,有时候 Chrome/Edge 拦但 Firefox 就没拦,或者也可右键对 debugger 表达式启用 Never pause here、Conditional breakpoint 来避免跳转到此处,更有甚者直接底层去除 Chrome 的 debugger。最后的大杀器是 Firefox 有一个功能 Pause on debugger statement,取消勾选就能绕过暂停。
CryptoJS AES 解密案例
参数每次加密结果都一样是 N2H1AiG/7rGW9MtDjWKf4A==
,输出结果还是 Base64 大概率为对称加密。跟到这个发请求的位置后就是找不到 Key。
function jiam(val){
return CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(val), { mode: CryptoJS.mode.ECB, padding:CryptoJS.pad.Pkcs7}).toString();
}
CryptoJS.AES.encrypt 跟进去发现 encrypt 方法可以用来加密,结果里面传的 16 字节整数数组,这个大概率是 Key。
/**
* Creates shortcut functions to a cipher's object interface.
*
* @param {Cipher} cipher The cipher to create a helper for.
*
* @return {Object} An object with encrypt and decrypt shortcut functions.
*
* @static
*
* @example
*
* var AES = CryptoJS.lib.Cipher._createHelper(CryptoJS.algo.AES);
*/
_createHelper: (function () {
function selectCipherStrategy(key) {
if (typeof key == 'string') {
return PasswordBasedCipher;
} else {
return SerializableCipher;
}
}
return function (cipher) {
return {
encrypt: function (message,cfg) {
return selectCipherStrategy({sigBytes: 16,words: [1114271858,1681932352,1077952577,1936942452]}).encrypt(cipher, message, {sigBytes: 16,words: [1114271858,1681932352,1077952577,1936942452]}, cfg);
},
decrypt: function (ciphertext,cfg) {
return selectCipherStrategy({sigBytes: 16,words: [1114271858,1681932352,1077952577,1936942452]}).decrypt(cipher, ciphertext, {sigBytes: 16,words: [1114271858,1681932352,1077952577,1936942452]}, cfg);
}
};
};
}())
通过 AI 发现不管要加密的 Message 还是加密设置的 Key、IV 都会转成 CryptoJS.lib.WordArray 对象,自然也能从 WordArray 转回 Hex、Base64、UTF-8 格式的内容。
// 创建WordArray
let keyData = {
words: [1114271858, 1681932352, 1077952577, 1936942452],
sigBytes: 16
};
// 创建WordArray对象
let wordArray = CryptoJS.lib.WordArray.create(keyData.words, keyData.sigBytes);
// 输出各种格式的 Key
console.log('Hex: ', wordArray.toString(CryptoJS.enc.Hex)); // 输出16进制字符串
console.log('Utf8: ', wordArray.toString(CryptoJS.enc.Utf8)); // 输出UTF-8字符串
console.log('Base64: ', wordArray.toString(CryptoJS.enc.Base64)); // 输出Base64字符串
运行输出得到各个格式的数值。
VM177829:11 Hex: 426a7072644040404040404173736574
VM177829:12 Utf8: Bjprd@@@@@@Asset
VM177829:13 Base64: QmpwcmRAQEBAQEBBc3NldA==
去 CyberChef 成功解密。
也遇到过 CryptoJS 用 AES 加密的 Key 变量是对象,用 CryptoJS.enc.Utf8.parse() 转的,咋才能变回字符串呢?
var key = {
sigBytes: 16,
words:[1112486448,959657784,892745544,0]
}
可以用 CryptoJS.enc.Utf8.stringify() 还原字符。
CryptoJS.enc.Utf8.stringify(key)
'BO209378567H\x00\x00\x00\x00'
如果没有 CryptoJS 环境课程可以参考《浏览器使用 CryptoJS,从浏览器控制台加载外部脚本》一文,在控制台执行下面 JS 将 crypto-js.min.js 引入到当前页面。
function loadJs(jsUrl){
let scriptTag = document.createElement('script');
scriptTag.src = jsUrl;
document.getElementsByTagName('head')[0].appendChild(scriptTag);
};
// 加载 CryptoJS 的浏览器版本,得到全局对象 CryptoJS
loadJs('https://cdn.bootcdn.net/ajax/libs/crypto-js/4.1.1/crypto-js.min.js');
人工手动修改数据
加密数据修改:
- 对于加密的数据,如果是对称加密,可以自己编写脚本搞个 Flask 做代理服务器对数据进行加解密转发。如果是非对称每次测试都要先拿到明文在篡改,接着手工加密发。
- 要么就是在浏览器加密数据前在调试过程中对参数修改。比如断点在加密前的参数,在 Console 对变量进行修改,或者在作用域处修改变量值。
半自动化修改数据
这两种都需手动操作,流程繁琐。于是有人就想在参数被加密前,让浏览器将明文参数通过 Ajax 发送 BurpSuite,由 BurpSuite 修改完后响应给浏览器,再进行后面的加密步骤。这大家就是半自动化方案。
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://127.0.0.1:28080/REQUEST, false)
xhr.send(packageData); // 发送明文参数 packageData
packageData = xhr.responseText; // 得到修改后的参数重新修改原有参数值
更细致的描述来说,通过 JS 文件定位到加密方法,在加密前将数据 Ajax 发送到到本地启动的服务器 127.0.1:28080/REQUEST,挂上代理转到 BurpSuite,改完后发出请求到 127.0.0.1:28080,由它将修改后的明文数据作为响应返回,最后参数被重新赋值传入。
JS-Forward 整个流程架构是。
PS:本地启一个 HTTP 服务不优雅,不如直接做成 BurpSuite 插件。这样做的目的是为了优化用户体验,此时https://github.com/f0ng/autoDecoder(用法https://www.t00ls.com/viewthread.php?tid=68439)产生,整合了常见加解密算法设置,直接在 BurpSuite 里对各个模块请求解密,如果想像 js-forwarded 一样也可以写 Flask 脚本来自定义需求。
为了将这段 JS 插入到加密参数的上下文中,可以用重写,一旦原有的 JS 有变动,会缓存下来使用本地编辑后的 JS。
特别要注意的一点是修改浏览器 JS 时,Sources -> Overrides 要先选择一个目录用来存放修改后的 JS(会以域名创建一个文件夹),这个目录归档时最好放在测试目录中乱放不方便找,不管浏览器重新加载关闭后重新打开修改过的内容依旧存在。需要注意的是代码格式化后无法修改,只有文件落地后格式化的代码才能修改。
不开启就仅仅在当前修改生效,浏览器重新加载或关闭后修改丢失,又要重新改很麻烦的。
要是没法用重写呢?在工作中遇到 CefSharp 编写的客户端,虽然启用了开发者模式,但重写功能没法选择要保存 JS 的位置。这时候只能选择抓包时修改 JS,我的做法是先选择好 JS 存放到位置,再通过 BurpSuite 抓取 JS,使用 Match/Replace 功能进行替换。在操作的时候要是没法修改成功,需要注意 JS 响应是不是被缓存。
全自动化修改数据
mitmproxy
最近又流行一种加解密方案,根本逻辑还是先分析出到底是什么加密算法,再去写个代理服务器处理请求响应加解密,相比以前用 Flask 写个代理服务器它可以处理 HTTPS,这是优势。
使用 NodeJS 写加密算法代码 JS 脚本,再用 Python 库 execjs 执行 JS,最后通过 mitmdump(是 mitmproxy 工具集其中的一个工具)代理加载 Python 脚本完成请求自动化加解密。
浏览器(请求开始)
↓
mitmdump1(解密请求)
↓
BurpSuite(修改明文请求并发送)
↓
mitmdump2(将请求请求加密发送,将收到的响应密文解密返回)
↓
BurpSuite(修改明文响应并返回)
↓
mitmdump1(将明文响应加密返回)
↓
浏览器(请求结束)
如果不想写脚本加密响应,可以把前端处理后端数据的逻辑搞清楚,再去篡改它们的控制流,让前端直接处理我们返回的明文。
参考文章:
- https://xz.aliyun.com/t/13218
https://xz.aliyun.com/t/16673?time__1311=Gui%3DYKGILDOD%2FD0ltGkDumD9Qtf2aaPpD - https://mp.weixin.qq.com/s?__biz=MzkzNTcwOTgxMQ==&mid=2247484397&idx=1&sn=67bd315311e12bdf2011bde87566d3c8
https://mp.weixin.qq.com/s/L5GD6PwivRG-1IfgpSCfEw - https://xz.aliyun.com/t/15051
https://xz.aliyun.com/t/15252
mitmproxy 解决前端签名案例
目标站点用户检查请求存在 Timestamp、Sign、Noceid,每个请求只能使用一次,每次请求值都不同,猜测是这几个参数组成了签名。
GET /dc_cloud_manage_service/forgot/user_exists/?userCode=13412341234 HTTP/1.1
Host: xxx
Timestamp: 1755827259282
Cache-Control: no-cache
Sign: 824F6200FC074995795B3169FA3D7540
Pragma: no-cache
Year: undefined
App-Key: white-page-key
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0
Dnt:1
Noceid: 3b363a42e9ad63a125b6758a93e1fdac
Token: null
Referer: xxx
Connection: keep-alive
首先需要分析完前端加密情况,后续才能编写脚本。
Sign 组成是其他数据拼接后的 MD5 哈希摘要。
// 'appKey=edu&noceId=3b363a42e9ad63a125b6758a93e1fdac×tamp=1755827259282&url=/dc_cloud_manage_service/forgot/user_exists/&key=984B28C0E84569228F89FAA698268B4F'
s()(f).toUpperCase();
// '824F6200FC074995795B3169FA3D7540'
let g = md5()(f).toUpperCase();
timestamp 生成,就是当前 Unix 时间戳。
const d = (new Date).getTime()
url 生成,获取当前请求路径进行字符串拆分,保证获取到的是路径。
"/dc_cloud_manage_service/forgot/user_exists/?userCode=13412341234".split("?")[0]
appKey 是硬编码。
let a = "edu"
noceId 生成,把当前的请求参数放到 JSON 里,最后拼接时间戳,形成 noceId。这里空的 {} 代表没有值如果有值就是转义后的 JSON。
noceId=md5('{"get":"{\\"userCode\\":\\"13412341234\\"}","post":"{}"}1755827259282')
key 也是硬编码做了 MD5。
o = "31C8710156D34A3C"
key = md5(o + " ").toUpperCase()
搞清楚签名组成逻辑后,通过 mitmproxy 可以自动化完成请求签名的添加。
首先 Windows 安装 mitmproxy。
pip3 install mitmproxy
运行一下命令 mitmproxy 后用户家目录下会产生 .mitmproxy 目录,进入此目录安装 mitmproxy-ca.p12 证书,避免发送出去的请求产生警告。
PS C:\Users\gbb> ls .\.mitmproxy\
目录: C:\Users\gbb\.mitmproxy
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2025/3/4 14:40 1172 mitmproxy-ca-cert.cer
-a---- 2025/3/4 14:40 1035 mitmproxy-ca-cert.p12
-a---- 2025/3/4 14:40 1172 mitmproxy-ca-cert.pem
-a---- 2025/3/4 14:40 2413 mitmproxy-ca.p12
-a---- 2025/3/4 14:40 2851 mitmproxy-ca.pem
-a---- 2025/3/4 14:40 770 mitmproxy-dhparam.pem
使用 mitmproxy 的 Python 库编写对应脚本。
from mitmproxy import http
import hashlib
from urllib.parse import urlsplit, parse_qs
import time
import json
def generate_md5(input_string: str) -> str:
"""
生成字符串的 MD5 哈希值
:param input_string: 输入的字符串
:return: MD5 哈希值(32 位小写十六进制字符串)
"""
# 创建 MD5 哈希对象
md5_hash = hashlib.md5()
# 更新哈希对象,需要将字符串编码为字节
md5_hash.update(input_string.encode('utf-8'))
# 返回十六进制格式的哈希值
return md5_hash.hexdigest()
def generate_timestamp():
"""
生成 Unix 时间戳
return: 10 位数值类型时间戳,如 1741085814
"""
return int(time.time() * 1000)
def request(flow: http.HTTPFlow) -> None:
# 处理 POST 请求参数适配签名处理
post_tmp_data = json.dumps(flow.request.content.decode("utf-8", errors="replace"), ensure_ascii=False)
print(post_tmp_data)
postData = post_tmp_data if len(post_tmp_data) == '' else '\"{}\"'
# 处理 GET 请求参数适配签名处理
query_string = urlsplit(flow.request.pretty_url).query # 提取查询参数部分
query_params = parse_qs(query_string) # 将查询参数解析为字典
query_params_single = {k: v[0] for k, v in query_params.items()} # 将列表值转换为字符串
getData = json.dumps(query_params_single, separators=(',', ':')).replace('"', '\\"')
# 用于调试
# print(getData)
# print(postData)
# 实时创建签名
timestamp = str(generate_timestamp())
appKey = 'edu'
noceid_value = '{"get":"'+ getData + '",' + '"post":' + postData + '}' + timestamp
noceid = generate_md5('{"get":"' + getData +'",' + '"post":' + postData + '}' + timestamp)
url = urlsplit(flow.request.pretty_url)[2]
key = generate_md5('31C8710156D34A3C ').upper()
sign = generate_md5(f"appKey={appKey}&noceId={noceid}×tamp={timestamp}&url={url}&key={key}").upper()
# 用于调试
# print("noceid_value:"+ noceid_value)
# print("appKey:"+appKey)
# print("noceId:"+noceid)
# print("timestamp:"+timestamp)
# print("url:"+url)
# print("key:"+key)
# print("sign:" + sign)
# print("sign-decoe:" + f'appKey={appKey}&noceId={noceid}×tamp={timestamp}&url={url}&key={key}')
# 修改原始请求头中的签名为我们实时计算的签名
if "Timestamp" in flow.request.headers:
flow.request.headers["Timestamp"] = timestamp
if "Sign" in flow.request.headers:
flow.request.headers["Sign"] = sign
if "Noceid" in flow.request.headers:
flow.request.headers["Noceid"] = noceid
# 打印修改后的请求用于调试
# print(f"{flow.request.method} {flow.request.pretty_url} {flow.request.http_version}") #请求行
# for key, value in flow.request.headers.items():#请求头
# print(f"{key}: {value}")
# if flow.request.content:#请求体
# print("\n" + flow.request.content.decode("utf-8", errors="replace"))
# print("-" * 80) #每个请求之间的分隔符
运行脚本。
# 调试用
mitmdump -p 8081 -s E:\desktop\edu-encrypt.py --ssl-insecure
# 正式运行用
mitmdump -p 8081 -s E:\desktop\edu-encrypt.py --ssl-insecure -q
在 BurpSuite 中 upstream 上挂 8081 代理,就会把所有请求转发到 mitmdump 中,脚本中的 request 方法执行完毕后会把处理完成的请求自动发到服务器。整个请求流转过程如下。
Browser -> BurpSuite -> mitmproxy -> WebApp
在 BurpSuite Repeater 重放请求过程中发现 mitmproxy 也有缺点,从命令行日志发现,每个请求都是重新建立连接,并没有长链接的过程有时候还会连接失败,这里 Intruder 没发现此问题。
油猴
浏览器插件更为方便,直接 Hook。
https://github.com/0xsdeo/Hook_JS
App
和 Web 分析思路类似,只是操作不同。
遇到原生 App 加密用 Frida Hook 即可,如果是 Android 没加固还能直接看加密代码。
另一种情况是 H5 开发的应用,直接抓包会发现是有请求 JS,在 JS 里面写的加密,这种情况和 Web 一样,直接 Hook 明文转发即可。
最后一种是混合开发,是 JS 打包到 App 里了通过本地调用,Android 通过翻包文件,或者 iOS 用爱思助手图形化工具查看安装包所在位置的本地文件。Objection 能够自动化查找
使用 Frida Hook 对方法进行重写。
等后续有案例再更新分析过程把,大概率使用 mPaaS 做案例。
最近更新:
发布时间: