在平时的日常测站过程中,经常遇到前端加密相关的对抗,对请求包及响应包进行加密、HASH校验对抗修改,根据加密算法可以分为以下几种:
字符编码:例如base64等。
对称加密:AES、RC4、DES、国密SM4等。
非对称加密:RSA、ECC、国密SM2等。
混合加密:混合多步加密,例如使用AES加密内容 + RSA加密密钥,拼接进SHA1做哈希,AES + base64等。
哈希算法:用于加密解密时的数据校验,例如SHA1/256、MD5、国密SM3等。
对于字符加密,对称加密,部分非对称加密算法:
- 使用 autoDecode 自带的算法可解。
对于复杂的加密算法,或是不想去逆向算法,不想扣JS补环境:
- 使用 autoDecode 的接口加解密 配合 JSRPC 提高工作效率。
对于复杂的加密算法,例如需要判断数据包来源时,或是扣出JS函数想直接调用时:
- 使用Galaxy实现,支持JS 和 Python脚本,可在Hook函数中直接执行JS 函数,也可发起http请求配合JSRPC进行加解密。
扩展插件
autoDecode
最常用的解密插件,通过内置算法与接口加解密配合,基本可以覆盖80%的加解密需求。
数据解密流向:
Proxy 历史:浏览器客户端 -> Burp autoDecode栏 显示解密包 -> 发送到服务端 -> 服务端响应加密包 -> 返回 Burp -> Burp autoDecode栏 显示解密包 -> 浏览器返回密文
代理数据全程对数据包无修改,解密过程仅UI中展示。
Repeater 重放:编辑明文包 -> Burp 调用加密 -> 发送到服务端 -> 返回 Burp -> burp autoDecode栏 显示解密包 -> 浏览器
区别是请求时直接是密文,需要在加密时判断当前是否已加密,已加密则避免重复加密,未加密需要自动加密。
部分缺点:
- 加解密条件和密文名文条件判断是通过纯字符串实现,难以覆盖复杂场景。
Galaxy
Galaxy 加密解密接口类似于 autoDecode 的接口加解密,通过引出四个hook点,分别对应实现对数据包的修改。
hookRequestToBurp
:HTTP请求从客户端到达Burp时被调用。在此处完成请求解密的代码就可以在Burp中看到明文的请求报文hookRequestToServer
:HTTP请求从Burp将要发送到Server时被调用。在此处完成请求加密的代码就可以将加密后的请求报文发送到ServerhookResponseToBurp
:HTTP请求从Server到达Burp时被调用。在此处完成响应解密的代码就可以在Burp中看到明文的响应报文hookResponseToClient
:HTTP请求从Burp将要发送到Client时被调用。在此处完成响应加密的代码就可以将加密后的响应报文返回给Client
数据会依次经过四个hook点
客户端请求 -> hookRequestToBurp -> hookRequestToServer -> 服务端
服务端响应 -> hookResponseToBurp -> hookResponseToClient -> 客户端
hook模板代码
/**
* HTTP请求从Burp将要发送到Server时被调用。在此处完成请求加密的代码就可以将加密后的请求报文发送到Server。
* @param {Request} request 请求对象
* @returns 经过处理后的request对象,返回null代表从当前节点开始流量不再需要处理
*/
function hook_request_to_burp(request) {
return request
}
/**
* HTTP请求从Burp将要发送到Server时被调用。在此处完成请求加密的代码就可以将加密后的请求报文发送到Server。
* @param {Request} request 请求对象
* @returns 经过处理后的request对象,返回null代表从当前节点开始流量不再需要处理
*/
function hook_request_to_server(request) {
return request
}
/**
* HTTP请求从Server到达Burp时被调用。在此处完成响应解密的代码就可以在Burp中看到明文的响应报文。
* @param {Response} response 响应对象
* @returns 经过处理后的response对象,返回null代表从当前节点开始流量不再需要处理
*/
function hook_response_to_burp(response) {
return response
}
/**
* HTTP请求从Burp将要发送到Client时被调用。在此处完成响应加密的代码就可以将加密后的响应报文返回给Client。
* @param {Response} response 响应对象
* @returns 经过处理后的response对象,返回null代表从当前节点开始流量不再需要处理
*/
function hook_response_to_client(response) {
return response
}
Galaxy 比起 autoDcode,更强大,更灵活,但使用时存在几个不足之处:
- 扩展只对数据包进行修改,不涉及到UI操作,难以做到仅显示解密包而不修改原始包的效果。
- 没有API补全。
- 缺乏文档,大量内置API需要读Java代码查看原始定义。
BurpScript
老外写的,类似Galaxy,对于数据包的条件过滤强于 autoDecode,但是只引出了两个hook点,不好用。
自实现Burp扩展
Burpsuite 新版 API 对于数据包的过滤非常简单,只需要实现 HttpHandler, ProxyRequestHandler, ProxyResponseHandler
三个类的共六个接口,分别对应六个Hook点,分别是:
proxy.handleRequestReceived
- 在客户端请求到达Burp时被调用
http.handleHttpRequestToBeSent
- 请求从Burp发送到服务端时被调用
http.handleHttpResponseReceived
- 响应从服务端返回给Burp时被调用
proxy.handleResponseToBeSent
- 在响应从Burp返回到客户端时被调用
proxy.handleRequestToBeSent
- 在客户端请求流出Proxy模块时调用(用不上)
proxy.handleResponseReceived
- 在响应流入Proxy模块时调用(用不上)
对于来自客户端的请求,会经过两个模块 Proxy 和 Http。参考 Galaxy 中的流程图。
Client -> Proxy -> Http -> Server
Proxy 从四个方向分别流入和流出,如果数据在在Proxy模块中被修改,UI中展示效果类似 在 Match and replace
替换功能,会显示修改前后的数据包。
这种方案灵活性最高,适合应对比较复杂的加解密场景。
Yakit
Yakit 中内置了 yaklang,一种类似 golang 语法的 dsl脚本,内置的标准库中已经提供了一些Hook API,可以在 MITM交互式劫持
窗口中实现hook代码。
![[Pasted image 20241011172443.png]]
除了这两个,还存在两个文档中没有提到的热补丁API,一共四个API 组成了完整的数据流向。实现效果类似 Galaxy
。
// 客户端新请求到达yakit时hook
hijackHTTPRequest = func(isHttps, url, req, forward /*func(modifiedRequest []byte)*/, drop /*func()*/) {}
// 响应包到达yakit时hook
hijackHTTPResponse = func(isHttps, url, rsp, forward, drop) {}
// 在发送到服务端之前的hook
beforeRequest = func(ishttps, oreq/*原始请求*/, req/*hijack修改后的请求*/){}
// 在回复给浏览器之前的hook
afterRequest = func(ishttps, oreq/*原始请求*/ ,req/*hiajck修改之后的请求*/ ,orsp/*原始响应*/ ,rsp/*hijack修改后的响应*/){}
如果只需要修改请求或响应的显示,而不用修改原始报文时,可用这个API,Hook数据写入数据库时进行加解密
由于 yakit无法创建自定义窗口,且自定义加解密需要手动操作,效率较低,可通过直接修改显示数据实现展示解密明文的效果。
# hijackSaveHTTPFlow 是 Yakit 开放的 MITM 存储过程的 Hook 函数
# 这个函数允许用户在 HTTP 数据包存入数据库前进行过滤或者修改,增加字段,染色等
# 类似 hijackHTTPRequest
# 1. hijackSaveHTTPFlow 也采用了 JS Promise 的回调处理方案,用户可以在这个方法体内进行修改,修改完通过 modify(flow) 来进行保存
# 2. 如果用户不想保存数据包,使用 drop() 即可
#
hijackSaveHTTPFlow = func(flow /* *yakit.HTTPFlow */, modify /* func(modified *yakit.HTTPFlow) */, drop/* func() */) {}
JSRPC
对于复杂场景,对JS反调试,算法的分析和密钥的提取需要消耗大量时间,这种场景下比较适合使用 JSPC去远程调用,节约时间。
需要对前端JS调试有一定经验,通过调用栈、字符串、事件断点等方式快速定位加密解密函数,调用被测站点自带的加密/解密函数实现加解密。
可配合autoDecode、Galaxy 等扩展快速实现请求加密解密,提高效率。
分为以下步骤
- 定位加密 、解密函数
- 从闭包中暴露函数到全局范围
- 注入JSRPC脚本,连接到RPC服务端
- 编写autoDecode 脚本 / Galaxy 脚本,调用 RPC 函数实现加解密
案例:XX银行
不同于之前的方案,加密解密包使用不同的编码逻辑,逆向还原算法成本较高,适合JSRPC实现。
经过简单调试分析,发现数据包加密函数与解密函数使用不同的格式拼接逻辑,encrypt
加密的的数据无法直接 decrypt
。
加密解密函数传入两个参数,data是明文或加密的数据,key是请求头中的固定密钥,在请求头和响应头中均有,前端动态生成,每次请求都不同,响应包与请求包会使用相同密钥。
大致思路是: 对于解密包,可沿用 autoDecode 的思路,通过JSRPC解密响应包,展示到Burp中。 对于请求包,由于无法解密前端的加密请求包,需要Hook前端加密函数部分,使前端发送明文数据包,再由Burp调用RPC进行加密。
function E(e) {
if (e && (200 === e.status || 304 === e.status || 400 === e.status)) {
e.data && "string" === typeof e.data && p["a"].isEncrypt && (e.data = JSON.parse(m.Security.TripleSM.decrypt(e.data, e.config.headers["x-gw-msg-id"]))),
e.headers["content-type"].indexOf("multipart/form-data") > -1 ? O(e) : "blob" == e.config.responseType && M(e);
var t = e.data;
return t.head && t.head.STATUS && "CheckRepetitionCodeOp.02" === t.head.STATUS && Object(f["j"])(),
t.body && t.body["_REPEAT_API_TOKEN_STATUS"] && "1" === t.body["_REPEAT_API_TOKEN_STATUS"] && Object(f["j"])(),
e
}
首先,注入JSRPC,暴露闭包内容,实现远程调用,具体参考 JSRPC 文档
- 启动JSRPC
- 在页面初始化完成后,注入JSRPC函数的定义
- 在加密或解密部分下断点,进入作用域
- 暴露闭包函数,连接JSRPC客户端
然后这个标签页就不要动了,代码拼接的JS去调用浏览器中函数实现函数调用,例如:
> curl -X POST http://127.0.0.1:12080/execjs \
-H "Content-Type: application/json" \
-d '{"group": "zzz", "code": "1+2"}'
{"data":"3","group":"zzz","name":"d357feea-2505-4a22-a54c-cadbc6da15a3","status":"200"}
返回 1 + 2 的结果3,测试 m.Security.TripleSM.encrypt
加密函数,data中返回完成的加密。
> curl -X POST http://127.0.0.1:12080/execjs \
-H "Content-Type: application/json" \
-d '{"group": "zzz", "code": "m.Security.TripleSM.encrypt(\"helloworld\", \"testkey\")"}'
{"data":"#10C0265EB6F3086D32CBA8606890F74760BA6C4C98222AB1069D8D9DB82F68E1FA\u001d820653E1E3B5344C71586D0554506C836B92E65EC9C8D52E44D7EF5F7597865F\u001d3D1F84C330456053D8F0A05CDF7C6CE59DC201897B0AE3F0AF4E4BB964435145C6F5141FC74F35BB7564881BC12E78468F048741D8D11B6B89E8FA30B1C220F480B1BA5F7519C11C7CDEE485F8D6E154CF2E6F8E81642DF455A5D3EEF9279E305A7A1A49ECEB86","group":"zzz","name":"d357feea-2505-4a22-a54c-cadbc6da15a3","status":"200"}
实现Burp扩展,包含以下逻辑
对于响应包
- 在Response部分,创建一个新的Tab「DecryptView」,用于展示解密的内容
- 通过是否存在加密key和正则匹配,判断响应包是否是加密
- 如果加密,则发送JSRPC请求解密,并展示到DecryptView中
@Override
public void setRequestResponse(HttpRequestResponse requestResponse) {
this.httpRequestResponse = requestResponse;
String encData = requestResponse.response().bodyToString();
String decKey = requestResponse.response().header("x-gw-msg-id").value();
// 避免阻塞 EDT
new SwingWorker<String, Void>() {
@Override
protected String doInBackground() throws Exception {
return rpcrypt.decrypt(encData, decKey);
}
@Override
protected void done() {
try {
// 在 EDT 中运行,更新 UI
String data = get();
// 判断是否为合法 JSON
byte[] bdata = null;
if (isValidJson(data)) {
bdata = beauty(data).getBytes();
} else {
// 处理 Unicode 转换
int i = 0;
while (needtoconvert(data) && i <= 3) {
data = StringEscapeUtils.unescapeJava(data);
i++;
}
if (i > 0) {
bdata = data.getBytes();
}
}
// 更新响应内容
if (bdata != null) {
ByteArray respData = ByteArray.byteArray(bdata);
responseEditor.setContents(respData);
}
} catch (Exception e) {
logging.logToError("Error decrypting response: " + e);
}
}
}.execute();
}
对于请求包
- 通过正则和其他条件判断是否需要进行加密操作
- 如果未加密则调用JSRPC加密,如果已加密则跳过
- 额外处理 Repeater 的加密
为什么要分成两个函数加密?
Repeater 模块的数据不会经过 Proxy 模块,需要单独处理,而Proxy模块修改数据包会经过 Match and replace
模块,同时展示 Edited request
和 Original request
选项卡,保留修改前和修改后的完整数据包,方便进行对比。
/*
* 在客户端请求到达Burp时被调用
*/
@Override
public ProxyRequestReceivedAction handleRequestReceived(InterceptedRequest interceptedRequest) {
// 请求来源于Proxy,如果存在加密key但未加密,则修改Proxy包,将其加密
if ( requestHasEnable(interceptedRequest)) {
String enckey = interceptedRequest.headerValue("x-gw-msg-id");
String body = interceptedRequest.bodyToString();
String encBody = rpcrypt.encrypt(body, enckey);
return ProxyRequestReceivedAction.continueWith(interceptedRequest.withBody(encBody));
}
return ProxyRequestReceivedAction.continueWith(interceptedRequest);
}
/*
* 请求从Burp发送到服务端时被调用
*/
@Override
public RequestToBeSentAction handleHttpRequestToBeSent(HttpRequestToBeSent httpRequestToBeSent) {
// 请求来自 Repeater 工具,如果未加密,则加密
if (httpRequestToBeSent.toolSource().isFromTool(ToolType.REPEATER)) {
if (requestHasEnable(httpRequestToBeSent)) {
String enckey = httpRequestToBeSent.headerValue("x-gw-msg-id");
String body = httpRequestToBeSent.bodyToString();
String encBody = rpcrypt.encrypt(body, enckey);
return RequestToBeSentAction.continueWith(httpRequestToBeSent.withBody(encBody));
}
}
return RequestToBeSentAction.continueWith(httpRequestToBeSent);
}
private boolean requestHasEnable(HttpRequestToBeSent httpRequestToBeSent) {
// x-gw-msg-id 存在加密头,且内容未加密
boolean hasXGwMsgId = httpRequestToBeSent.hasHeader("x-gw-msg-id");
boolean hasEncryptBody = Optional.ofNullable(httpRequestToBeSent.bodyToString())
.filter(body -> !body.isEmpty())
.map(body -> body.charAt(0) == '#')
.orElse(false);
boolean hasPostReq = httpRequestToBeSent.method().equals("POST");
return hasPostReq && hasXGwMsgId && !hasEncryptBody;
}
最后,需要替换加密的JS,hook前端加密函数,使前端发起的请求为到Burp时是明文。
// 保留原有的函数供程序调用
window.encrypt=A.Security.TripleSM.encrypt
// Patch加密函数为返回明文
A.Security.TripleSM.encrypt=(data,key)=>data;
实现效果
明文请求 -> 密文响应解密
加密请求 -> 密文响应未解密
总结
对于常见加密算法,且容易拿到密钥或公私钥,适合用 autoDecode
的内置加密方案。
对于常见加密算法,且存在复杂的数据包条件判断,适合用 Galaxy
实现。
特殊情况下,例如
- 加密与解密算法不同
- 加密与解密使用的密钥不同
- 存在多种算法的组合加密
- 存在复杂的数据包格式,例如加密属于与明文数据混合
利用 autoDecode
的远程调用,或是 Galaxy
脚本,配合JSRPC,可以覆盖部分场景,对于复杂场景还是需要自己编写扩展实现。