简介......

不是所有加密都能过 WAF 的,有些在都走的网关加密,在安全设备里会卸掉加密。

目录

Web

首先需要熟练用 DeveloperTools:

调试 JS:
有时无法格式化,文件大在线工具也不支持,可以在 “Sources -> Snippets” 选项卡中粘贴,点 Pretty print 格式化查看。

如何调试现有页面 JS?打开开发者工具,找到源代码选项卡下的 Override(覆盖),勾选后刷新页面,就可以编辑对应 JS,编辑完保存后会再指定文件夹中写入编辑后的脚本,后续网站则使用编辑后的脚本执行 JS。

分析加密特征-了解常见的加密特征

加密:对称或非对称对传输数据做加密。常见对称加密(AES/DES/SM4)的加密结果大多都是十六进制或者 Base64 编码,非对称加密(RSA/SM2)每次加密结果不同,对称每次加密结果相同。有些加密结果传递的可能是二进制数据,而不是加密结果。

哈希:采用哈希摘要算法(MD5/SHA-1/SHA-2/SM3)对整个请求包做签名,或者摘取请求数据、请求头、请求时间戳拼接后签名,有了时间戳后,可以在一定时间内保证请求有效,超过指定时间通过比较时间戳可以避免重放攻击。在实际哈希摘要算中有的也有用 HMAC,通过 在计算哈希的过程中把 Key 当做数据的一部分融入,对方也拿到数据也可以通过明文数据和 Key 来校验哈希是否正确。

编码:URL、Base、Unicode

定位加密位置(寻找加密方法)

手动调试阶段加解密推荐使用离线工具 CyberChef

关键字定位:

  1. Ctrl + Shift + F 全局搜索全局搜索加解密关键字或加密库关键字,通用关键字 encrypt、crypt,如对称加密:aes、sm4、ecb、cbc、key、mode,非对称加密:sm2、rsa。
  2. 搜索 URL Pattern 接口名称,例如:/api/username
  3. 搜索加密数据参数名
  4. 有些会用 Axios 拦截器前置加解密,搜 interceptor,interceptor.request.use,interceptor.response.use
  5. 比较老的前端应用可能还存在用 JQuery,这种通过取标签中值加密的情况,可以搜索 HTM id/class 找到哪里获取的 value,上下文去翻最终存在那个变量,应该会有请求时会调用此变量。

断点分析:

找到疑似加密的方法后,下断点后上下文分析,再找不到就堆栈回溯找调用链,来确定加密前穿过来的参数和我们输入的参数是否一致,最后再次确定加密后的参数匹配和真实请求中的加密参数是否一致。

  1. 请求断点:XHR/fetch Breakpoints
  2. 网络面板选中具体请求,找到启动器面板,通过请求调用堆栈回溯
  3. JS 表达式断点:对指定 JS 指定行数断点
  4. 事件断点:如 mouseclick

反调试:

Q:遇到无限 Debugger 怎么解决
A:尝试更换浏览器,有时候 Chrome/Edge 拦但 Firefox 就没拦,或者也可右键对 debugger 表达式启用 Never pause here、Conditional breakpoint 来避免跳转到此处,更有甚者直接底层去除 Chrome 的 debugger。最后的大杀器是 Firefox 有一个功能 Pause on debugger statement,取消勾选就能绕过暂停。

Q:遇到JS打包成一个文件,存储大小特大,还没办法点击 {} 去 Pretty Print,这种情况怎么办?
A:可以先把源文件下载下来,进行格式化,这里我用的是 VSCode 的 Prettier,处理完成后在Source 中启用 Overwrite,刷新完页面后原有文件写入到指定路径上,用格式化的好内容进行替换。这时候刷新就能断点。

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 成功解密。

CyberChef 解密成功.png

也遇到过 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');  

人工手动修改数据

加密数据修改:

  1. 对于加密的数据,如果是对称加密,可以自己编写脚本搞个 Flask 做代理服务器对数据进行加解密转发。如果是非对称每次测试都要先拿到明文在篡改,接着手工加密发。
  2. 要么就是在浏览器加密数据前在调试过程中对参数修改。比如断点在加密前的参数,在 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 整个流程架构是。

JS-Forward.png

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(将明文响应加密返回)
  ↓
浏览器(请求结束)

如果不想写脚本加密响应,可以把前端处理后端数据的逻辑搞清楚,再去篡改它们的控制流,让前端直接处理我们返回的明文。

参考文章:

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&timestamp=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}&timestamp={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}&timestamp={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 没发现此问题。

mitmproxy 配合 jsrpc 解决前端签名案例

在以前遇到复杂加密分析时,需要手动定位位置,分析这个加密/加签这个方法是对称、非对称、自定义,对应Key是什么,完成这一套流程使用 BurpSuite 插件或者是自己编写脚本,完成自动化处理。

而 jsrpc 省去了完整分析加密方法这个过程,只需要定位到加密/加签位置和搞清楚传入的参数格式即可,通过 WebSocket 来调用 JS 作用域中加密/加签方法得到结果。

0.定位签名位置

某金融官网登录页 https://xx.xx.com.cn/bcbs/butler/#/login 每个请求都存在签名,下面代码是定位到对应签名计算位置。

k = (e, t, n) => {
            let o = t;
            if (Object(_["w"])(t))
                return;
            const r = "" + (new Date).getTime();
            "get" === e.method && (e.params ? o = JSON.stringify(Object(_["K"])(Object(_["t"])(S.a.stringify(e.params)))) : (-1 === e.url.indexOf("?") && (e.url += "?_t=" + r),
            o = JSON.stringify(Object(_["K"])(Object(_["t"])(e.url))))),
            "string" === typeof o && o.includes("$JR_") ? o = JSON.stringify(Object(_["I"])(JSON.parse(o), /\$JR_/)) : "get" === e.method || o || (e.data = JSON.stringify({
                _t: r
            }),
            o = e.data),
            o && "string" !== typeof o && (o = JSON.stringify(o)),
            o && (e.headers["Sign"] = x(o, n))
        }

k 函数的形参 e 是完整请求头对象及请求相关设置(像是 axios 对象),t 是请求体,n 是通过 sessionStorage.getItem("token") 存储的值(未登录的情况下 token 不存在,所以获取的是 null)。其中 o && (e.headers["Sign"] 在设置请求头,这个请求头的值是通过 x(o, n) 进行 HMAC 签名。

const j = "B9A55A9E93473DFG"
          , x = (e, t) => v(t ? t + e : e, {
            key: j
        })

x 方法形参 o,处理过程是这样的,GET请求参数为/parameter/view?paracode=SM4_FLAG&b=123 k 方法最终会处理程下面格式进行传入 x 方法形参 o。

'{"b":"123","paracode":"SM4_FLAG"}'

POST 参数最终会变成下面格式进行传入传入 x 方法形参 o。

'{"deviceId":"20cb8761-33ef-4389-a317-454bcbcfcfd2","userid":"13412341234","password":"iSmy4ofbc8C0WWA1tpB3DA==","codeInput":"376540","code":"","passwordInput":"123123"}'

1.运行 jsrpc 服务端

根据当前 CPU 架构从 Release 下载编译好的 Golang 程序。

raingray@mini WebApplicationPenetrationTesting % chmod +x jsrpc_arm64
raingray@mini WebApplicationPenetrationTesting % ./jsrpc_arm64
zsh: killed     ./jsrpc_arm64
raingray@mini WebApplicationPenetrationTesting % ./jsrpc_arm64
       __       _______..______      .______     ______
      |  |     /       ||   _  \     |   _  \   /      |
      |  |    |   (----`|  |_)  |    |  |_)  | |  ,----'
.--.  |  |     \   \    |      /     |   ___/  |  |
|  `--'  | .----)   |   |  |\  \----.|  |      |  `----.
 \______/  |_______/    | _| `._____|| _|       \______|

WARN[2026-01-17 16:29:38] 使用默认配置运行 config path not found
配置参考 https://github.com/jxhczhl/JsRpc/blob/main/config.yaml
INFO[2026-01-17 16:29:38] 当前监听地址::12080 ssl启用状态:false

2.连接 jsrpc 服务端

在目标站点控制台粘贴并运行 JsEnv_Dev.js 文件内容。

https://github.com/jxhczhl/JsRpc/blob/main/resouces/JsEnv_Dev.js

继续粘贴运行下面 JS。这个 group 就是分组的意思,方便后续调用,建议取个目标应用名称方便后续回顾。

// 注入环境后连接通信
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=zzz");

此时客户端日志提示连接成功。

INFO[2026-01-17 17:48:17] [新上线group:zzz,clientId:->d84c5da2-6db4-47b0-bbf1-ec0b664be1de]
INFO[2026-01-17 17:48:17] [客户端 d84c5da2-6db4-47b0-bbf1-ec0b664be1de 注册了actions: [_execjs]]

要是懒得每次都手动注入,可以油猴写个脚本,自动注入。

3.在控制台执行 JS

编写脚本jsrpc.py

import requests

js_code = """
(function(){
    console.log("test")
    return "执行成功"
})()
"""

url = "http://localhost:12080/execjs"
data = {
    "group": "zzz",
    "code": js_code
}
res = requests.post(url, data=data)
print(res.text)

运行 Python 脚本,此时请求发送到 jsrpc 服务端,由服务端通过 WebSocket 与目标浏览器通信,而浏览器网页控制台会执行 Python 脚本中的 js_code 成功在控制台输出 test。

raingray@mini WebApplicationPenetrationTesting % python3 jsrpc.py
/Users/raingray/Library/Python/3.9/lib/python/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020
  warnings.warn(
{"data":"执行成功(无返回值)","group":"zzz","name":"d84c5da2-6db4-47b0-bbf1-ec0b664be1de","status":"200"}

这并不满足我们的需求,我们需要调用目标签名函数 x 怎么办?

有个更为简单的办法,先在控制台在 x 函数处下断点,将这个局部方法提升到全局作用域方便调用。注意,设置全局变量名不要跟现有的重复,避免覆盖。

// 1.设置签名为全局变量方便调用
sign_action = x // 设置要签名的方法,不然一旦出了作用域就可能调用不到

再注册下方法。注册完成后不要处于断点状态,要把断点禁用掉,否则后面调用到 x 函数时会在控制台自动中断,导致调用超时。

// 2.注册签名方法(如果内容有变动,可以随时区控制台覆盖)
demo.regAction("sign",function (resolve,param) {
    console.log("传入的数据是: " + param)
    sign_result = sign_action(param) // 使用网站的方法对 JSRPC 服务端传过来的数据进行签名
    console.log("数据签名结果是: " + sign_result)
    resolve(sign_result); // 把浏览器签名结果通过回调函数返回给 JSRPC
})

此时请求 jsrpc 服务端 http://127.0.0.1:12080/go?group=zzz&action=sign&param=1 就会调用注册的 sign 方法对参数 1 进行签名。这里的 gourp 就是前面连接 JSRPC 服务端手动设置的 zzz,action 则是刚刚注册的签名方法名称 sign,param 是你要传给目标站点待签名的参数。jsrpc 响应中的 data 是 sign 方法通过 resolve 回调函数返回的签名结果。

raingray@mini ~ % curl "http://127.0.0.1:12080/go?group=zzz&action=sign&param=1"
{"clientId":"d84c5da2-6db4-47b0-bbf1-ec0b664be1de","data":"8299cdf994f5e5ef6a943c8857abab32a7b5b24d4af84ef91a032c4663bc42c7","group":"zzz","status":200}

既然知道通过 API 如何调用 jsrpc 服务端注册的方法后,通过 Python 快速发送请求就能获取结果,这块比较简单就不重复写了。

4.联动 mitmproxy 自动更新请求头 Sign

能够用 Python 方便调用签名方法后,只要搞清楚 x 方法传入的参数如何构建,我们就可以使用 mitmproxy 来更新请求头 Sign 的值,具体规则在本文开头已经分析过,不再赘述。直接写代码。

先搞清楚流量走向方便调试脚本:

  • 运行 mitmdump -p 8081 -s jsrpc.py --ssl-insecure

    • 浏览器 -> BurpSuite -> mitmproxy -> 服务端

      • 需要自己打印修改后的请求头进行调试,或者自己搭建个后端 API方便查看服务端接收到的请求头是否有修改成功(这里现成的可以向 https://httpbin.org/headers 发请求,这个站点支持 HTTP 1.1 和 HTTP 2)。
  • 运行 mitmdump -p 8081 -s jsrpc.py --ssl-insecure --mode upstream:http://127.0.0.1:8080

    • 浏览器 -> mitmproxy -> BurpSuite -> 服务端

      • 这样的流量走向,BurpSuite 能看到 mitmproxy 修改后的请求头

这里我选择第一种流量走向。

jsrpc.py

import requests, json
from mitmproxy import ctx, http
from urllib.parse import urlparse, parse_qsl

# JSRPC 调用签名方法
def sign(param):
    url = "http://127.0.0.1:12080/go"
    params = {
        "group": "zzz",
        "action": "sign",
        "param": param
    }
    try:
        result = requests.get(url, params=params, timeout=15)

        # ctx.log.info(f"RPC Response: {result.text}")
        outer_data = result.json()['data']
        return outer_data
    except Exception as e:
        # ctx.log.error(f"Error in get_rpc: {str(e)}")
        return None

# 处理加签参数格式
def process_params(params):
    # 测试数据
    # data = {"paracode": "SM4_FLAG"} # 首页手机验证登录
    # data = {"deviceId":"02e71137-5a98-4ec7-9ab6-5198a82af828","userid":"13412341234","password":"iSmy4ofbc8C0WWA1tpB3DA==","codeInput":"376540","code":"","passwordInput":"123123"}
    # 第一步:把字典变成普通的 JSON 字符串
    # 避免格式化后冒号后面存在空格,比如 {"paracode": "SM4_FLAG"} 变成 {"paracode":"SM4_FLAG"}
    # 结果:'{"paracode":"SM4_FLAG"}'
    sign_raw_data = json.dumps(params, separators=(",", ":"), ensure_ascii=False)

    # 第二步:把上面那个字符串,再次变成 JSON 格式的字符串(这时候就会自动转义内部的双引号)
    # 结果:'"{\\"paracode\\":\\"SM4_FLAG\\"}"'
    final_sign_raw_data = json.dumps(sign_raw_data, ensure_ascii=False)

    # ctx.log.info(sign(final_sign_raw_data))
    return final_sign_raw_data

# mitmproxy 请求处理函数
def request(flow: http.HTTPFlow):
    ctx.log.info('——————————————————————————')
    # 获取原始请求体和请求方法
    original_body = flow.request.content
    request_method = flow.request.method
    request_path = flow.request.path
    sign_result = None

    ctx.log.info(f"Request URL: {request_path}")
    ctx.log.info(f"Request Method: {request_method}")
    ctx.log.info(f"Original Body: {original_body.decode('utf-8', errors='ignore')}")

    if request_method == "GET":
        # 对于 GET 请求,使用查询参数作为参数
        parsed_url = urlparse(request_path)
        params = dict(parse_qsl(parsed_url.query))
        json_output = process_params(params)
        ctx.log.info(f'sign source data: {json_output}')
        sign_result = sign(json_output)
        ctx.log.info(f'sign reuslt: {sign_result}')
        
    elif request_method == "POST":
        # 对于 POST 请求,使用请求体作为参数
        # ctx.log.info(f"post_body type: {original_body}")
        # ctx.log.info(f"post_body translate to dict: {json.loads(original_body)}")
        json_output = process_params(json.loads(original_body))
        ctx.log.info(f'sign source data: {json_output}')
        sign_result = sign(json_output)
        ctx.log.info(f'sign reuslt: {sign_result}')

    if sign_result is None:
        ctx.log.error("RPC failed")
        return

    try:
        flow.request.headers['Sign'] = sign_result
        # flow.request.headers["X-Custom-Header"] = "Success-Injected"

        # 遍历并打印所有 Header,确认请求头是否已经修改
        ctx.log.info("Modified Request Headers:")
        for k, v in flow.request.headers.items():
            ctx.log.warn(f"{k}: {v}")
    except Exception as e:
        ctx.log.error(f"Error in request: {str(e)}")
    ctx.log.info('——————————————————————————')

mitmproxy 运行结果。

[03:25:41.099][127.0.0.1:55167] TLS Error: (-1, 'Unexpected EOF')
[03:25:52.967][127.0.0.1:55229] client connect
[03:25:53.016][127.0.0.1:55229] server connect xx.xx.com.cn:443 (211.137.80.167:443)
[03:25:53.173] ——————————————————————————
[03:25:53.173] Request URL: /butler/parameter/view?paracode=SM4_FLAG
[03:25:53.173] Request Method: GET
[03:25:53.173] Original Body: 
[03:25:53.173] sign source data: "{\"paracode\":\"SM4_FLAG\"}"
[03:25:53.177] sign reuslt: fff1c87e372d34b6f3b156188c682a1011505f12582a85f16bcc8eba7e2420f5
[03:25:53.177] Modified Request Headers:
[03:25:53.177] Host: xx.xx.com.cn
[03:25:53.178] Sec-Ch-Ua-Platform: "macOS"
[03:25:53.178] Sign: fff1c87e372d34b6f3b156188c682a1011505f12582a85f16bcc8eba7e2420f5
[03:25:53.178] Sec-Ch-Ua: "Microsoft Edge";v="143", "Chromium";v="143", "Not A(Brand";v="24"
[03:25:53.178] Sec-Ch-Ua-Mobile: ?0
[03:25:53.178] User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0
[03:25:53.178] Accept: application/json, text/plain, */*
[03:25:53.178] Dnt: 1
[03:25:53.178] Sys-Version: null
[03:25:53.178] Sec-Fetch-Site: same-origin
[03:25:53.178] Sec-Fetch-Mode: cors
[03:25:53.178] Sec-Fetch-Dest: empty
[03:25:53.178] Referer: https://xx.xx.com.cn/bcbs/butler/
[03:25:53.178] Accept-Encoding: gzip, deflate, br
[03:25:53.178] Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
[03:25:53.178] Priority: u=1, i
[03:25:53.178] Connection: keep-alive
[03:25:53.178] X-Custom-Header: Success-Injected
[03:25:53.178] ——————————————————————————
127.0.0.1:55229: GET https://xx.xx.com.cn/butler/parameter/view?paracode=SM4_FLAG
              << 200 OK 579b
[03:27:15.232][127.0.0.1:55620] client connect
[03:27:15.284][127.0.0.1:55620] server connect xx.xx.com.cn:443 (211.137.80.167:443)
[03:27:15.478] ——————————————————————————
[03:27:15.478] Request URL: /butler/system/user/login
[03:27:15.478] Request Method: POST
[03:27:15.478] Original Body: {"deviceId":"4ca65d57-5779-4b70-950f-ea035b17b7bd","userid":"13412341234","password":"iSmy4ofbc8C0WWA1tpB3DA==","codeInput":"123123","code":"","passwordInput":"123123"}
[03:27:15.478] post_body type: b'{"deviceId":"4ca65d57-5779-4b70-950f-ea035b17b7bd","userid":"13412341234","password":"iSmy4ofbc8C0WWA1tpB3DA==","codeInput":"123123","code":"","passwordInput":"123123"}'
[03:27:15.478] post_body translate to dict: {'deviceId': '4ca65d57-5779-4b70-950f-ea035b17b7bd', 'userid': '13412341234', 'password': 'iSmy4ofbc8C0WWA1tpB3DA==', 'codeInput': '123123', 'code': '', 'passwordInput': '123123'}
[03:27:15.479] sign source data: "{\"deviceId\":\"4ca65d57-5779-4b70-950f-ea035b17b7bd\",\"userid\":\"13412341234\",\"password\":\"iSmy4ofbc8C0WWA1tpB3DA==\",\"codeInput\":\"123123\",\"code\":\"\",\"passwordInput\":\"123123\"}"
[03:27:15.491] sign reuslt: 1b811e09c37a98ad290eb052e68ab0bc346cbe6f0c89855ea61373cc4dae65e4
[03:27:15.491] Modified Request Headers:
[03:27:15.491] Host: xx.xx.com.cn
[03:27:15.491] Content-Length: 168
[03:27:15.491] Sec-Ch-Ua-Platform: "macOS"
[03:27:15.491] Sign: 1b811e09c37a98ad290eb052e68ab0bc346cbe6f0c89855ea61373cc4dae65e4
[03:27:15.491] Sec-Ch-Ua: "Microsoft Edge";v="143", "Chromium";v="143", "Not A(Brand";v="24"
[03:27:15.491] Sec-Ch-Ua-Mobile: ?0
[03:27:15.491] User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0
[03:27:15.491] Accept: application/json, text/plain, */*
[03:27:15.491] Dnt: 1
[03:27:15.491] Content-Type: application/json;charset=UTF-8
[03:27:15.491] Sys-Version: null
[03:27:15.491] Origin: https://xx.xx.com.cn
[03:27:15.491] Sec-Fetch-Site: same-origin
[03:27:15.491] Sec-Fetch-Mode: cors
[03:27:15.491] Sec-Fetch-Dest: empty
[03:27:15.491] Referer: https://xx.xx.com.cn/bcbs/butler/
[03:27:15.491] Accept-Encoding: gzip, deflate, br
[03:27:15.491] Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
[03:27:15.491] Priority: u=1, i
[03:27:15.491] Connection: keep-alive
[03:27:15.492] X-Custom-Header: Success-Injected
[03:27:15.492] ——————————————————————————
127.0.0.1:55620: POST https://xx.xx.com.cn/butler/system/user/login
              << 200 OK 139b

油猴

浏览器插件更为方便,直接 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 做案例。

最近更新:

发布时间:

摆哈儿龙门阵