在平时的日常测站过程中,经常遇到前端加密相关的对抗,对请求包及响应包进行加密、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栏 显示解密包 -> 浏览器

区别是请求时直接是密文,需要在加密时判断当前是否已加密,已加密则避免重复加密,未加密需要自动加密。

部分缺点:

  1. 加解密条件和密文名文条件判断是通过纯字符串实现,难以覆盖复杂场景。

Galaxy

Galaxy 加密解密接口类似于 autoDecode 的接口加解密,通过引出四个hook点,分别对应实现对数据包的修改。

hookRequestToBurp:HTTP请求从客户端到达Burp时被调用。在此处完成请求解密的代码就可以在Burp中看到明文的请求报文 hookRequestToServer:HTTP请求从Burp将要发送到Server时被调用。在此处完成请求加密的代码就可以将加密后的请求报文发送到Server hookResponseToBurp: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,更强大,更灵活,但使用时存在几个不足之处:

  1. 扩展只对数据包进行修改,不涉及到UI操作,难以做到仅显示解密包而不修改原始包的效果。
  2. 没有API补全。
  3. 缺乏文档,大量内置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 替换功能,会显示修改前后的数据包

bp

这种方案灵活性最高,适合应对比较复杂的加解密场景。

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 等扩展快速实现请求加密解密,提高效率。

分为以下步骤

  1. 定位加密 、解密函数
  2. 从闭包中暴露函数到全局范围
  3. 注入JSRPC脚本,连接到RPC服务端
  4. 编写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 文档

  1. 启动JSRPC
  2. 在页面初始化完成后,注入JSRPC函数的定义
  3. 在加密或解密部分下断点,进入作用域
  4. 暴露闭包函数,连接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扩展,包含以下逻辑

对于响应包

  1. 在Response部分,创建一个新的Tab「DecryptView」,用于展示解密的内容
  2. 通过是否存在加密key和正则匹配,判断响应包是否是加密
  3. 如果加密,则发送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();
    }

对于请求包

  1. 通过正则和其他条件判断是否需要进行加密操作
  2. 如果未加密则调用JSRPC加密,如果已加密则跳过
  3. 额外处理 Repeater 的加密

为什么要分成两个函数加密?

Repeater 模块的数据不会经过 Proxy 模块,需要单独处理,而Proxy模块修改数据包会经过 Match and replace 模块,同时展示 Edited requestOriginal 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 实现。

特殊情况下,例如

  1. 加密与解密算法不同
  2. 加密与解密使用的密钥不同
  3. 存在多种算法的组合加密
  4. 存在复杂的数据包格式,例如加密属于与明文数据混合

利用 autoDecode 的远程调用,或是 Galaxy 脚本,配合JSRPC,可以覆盖部分场景,对于复杂场景还是需要自己编写扩展实现。