0x01 前言
随着前端防护手段日益复杂,参数加密场景广泛且生成逻辑经过多层混淆与封装,人工逆向方法耗时费力且高度依赖经验。本文旨在结合 chrome-devtools-mcp 的能力与 Skill 规范,实现基于 JSRPC、Flask 和 autoDecoder 的前端 JS 逆向自动化分析方案,提升逆向效率。
本文内容仅供技术学习与交流使用,请遵守相关法律法规,因违规使用产生的一切后果由使用者自行承担。
0x02 漏洞详情
传统 JS 逆向方法回顾
传统 JS 逆向工程实践中已形成一系列技术路径,主要归纳为四类:
- 直接修改变量法:进入调试模式,在参数加密前直接在作用域中修改参数。
- 中间劫持法(JS-forward):通过 JS-forward 实现在明文点处插入代码,将请求内容发送给 Burp Suite 拦截并修改后返回。
- 远程调用法(JS-RPC):注入到浏览器页面中的客户端,通过 WebSocket 与本地 Python 服务端相连,直接调用浏览器上下文中预先注册的 JavaScript 函数。
- 硬核对抗法(JS 原生):通过反混淆分析代码逻辑研究参数作用添加补环境,最终通过本地运行 JS 来加解密。
在接下来的方案中,选择远程调用法(JS-RPC)作为 MCP 赋能 JS 逆向的底座。它能将浏览器环境直接转化为加密服务接口,避免纯手动逆向的低效,又绕开了纯 JS 还原的高复杂度,适配复杂场景下的 JS 逆向对抗。
MCP 赋能的集成方案
工具链介绍
- JSRPC:注入到浏览器页面中的客户端,建立通过 WebSocket 协议的远程过程调用机制,直接调用在浏览器上下文中预先注册好的 JavaScript 函数。
- Flask:接收来自自动化工具如 Burp Suite 插件的请求,提取出需要加密的参数,然后通过 JSRPC 调用浏览器中的函数进行加密,最后重构请求并转发至目标服务器。
- autoDecoder:Burp Suite 的插件,用于自动化处理应用中的加密/编码接口,自动将明文参数发送至该服务器,并完成加密请求的构建。
- chrome-devtools-mcp:通过 Chrome DevTools Protocol 与浏览器实例建立连接,将浏览器的底层控制能力封装成一系列标准化的工具,并通过 MCP 协议暴露给 AI 客户端。
流程设计
针对远程调用法初始配置阶段中定位加密函数、编写注册代码等繁琐操作,引入 AI 技术进行赋能,让 AI 自动完成函数发现与注册代码生成,最终实现从'半自动'到'高自动'的跨越。
MCP 配置
- 使用 codex 来调用 MCP 服务,首先将 MCP 服务器写进 Codex 的配置。
codex mcp add chrome-devtools npx -y chrome-devtools-mcp@latest
- 修改 Codex 的配置文件(MAC 在 ~/.codex/config.toml),添加如下字段。
[mcp_servers.chrome-devtools]
-
检测是否生效。
-
启动 MCP 服务,当看到打开浏览器后 MCP 服务就配置好了。
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome
Skill 配置
Skill 是能否一次性完成 JS 分析和代码生成的关键,创建 3 个 markdown 文件用于规范 AI 的操作以及输出。
1、主技能模板 (js-reverse-automation)
负责整体流程控制和协调,规范 AI 通过 MCP 协议连接和控制浏览器环境。
- name: js-reverse-automation description: 通过 MCP 连接浏览器,为目标自动化搭建 JS 环境,定位签名/加密函数入口,生成 JSRPC 代码,创建 Flask 代理,并输出 Burp autoDecoder 配置。
...
2、JSRPC 注入模板 (jsrpc-injection-template)
提供通用的函数拦截和远程调用能力,确保代码注入的稳定性和安全性。
// 1) 建立 JSRPC 连接
var client = new Hlclient("ws://127.0.0.1:12080/ws?group=fausto&name=burp");
// 2) 配置区
var JSRPC_CONFIG = {
actionName: "generate_sign",
entry: { type: "global", path: "signFunction" },
bindThis: null,
async: false,
normalizeInput: function(param) { return param; },
normalizeOutput: function(result) { return result; },
onError: function(err) { return "ERROR_" + Date.now(); }
};
...
3、Flask 代理模板 (flask-jsrpc-proxy-template)
面向 Burp autoDecoder 的 Flask 代理服务模板,用于将请求体交给 JSRPC 生成加密后的结果并回填。
from flask import Flask, request
import requests
import json
import logging
logging.basicConfig(level=logging.DEBUG)
app = Flask(__name__)
JSRPC_URL = "http://127.0.0.1:12080/go"
JSRPC_GROUP = "fausto"
JSRPC_ACTION = "generate_sign"
SIGN_FIELD = "sign"
TIMEOUT = 5
@app.route('/encode', methods=['POST'])
def handle_encode():
app.logger.info("- 收到来自 Burp/autoDecoder 的编码请求 -")
data_body = request.form.get('dataBody', '')
data_headers = request.form.get('dataHeaders', '')
if not data_body:
app.logger.error("未接收到 dataBody")
return data_headers + "\r\n\r\n\r\n\r\n" + data_body if data_headers else data_body
try:
json_data = json.loads(data_body)
except json.JSONDecodeError as e:
app.logger.error(f"解析 JSON 失败:{e}")
return data_headers + "\r\n\r\n\r\n\r\n" + data_body if data_headers else data_body
old_sign = json_data.pop(SIGN_FIELD, None)
app.logger.info(f"已移除旧签名:{old_sign}")
params_for_jsrpc = {
"group": JSRPC_GROUP,
"action": JSRPC_ACTION,
"param": json.dumps(json_data, ensure_ascii=False, separators=(',', ':'))
}
try:
jsrpc_response = requests.get(JSRPC_URL, params=params_for_jsrpc, timeout=TIMEOUT)
jsrpc_response.raise_for_status()
result = jsrpc_response.json()
new_sign = result.get('data', '')
except requests.exceptions.RequestException as e:
app.logger.error(f"调用 JSRPC 失败:{e}")
new_sign = ""
except json.JSONDecodeError as e:
app.logger.error(f"解析 JSRPC 返回失败:{e}")
new_sign = ""
json_data[SIGN_FIELD] = new_sign
new_data_body = json.dumps(json_data, ensure_ascii=False, separators=(',', ':'))
if data_headers:
new_content_length = len(new_data_body)
new_headers = data_headers.replace(
f"Content-Length: {len(data_body)}",
f"Content-Length: {new_content_length}"
)
return new_headers + "\r\n\r\n\r\n\r\n" + new_data_body
return new_data_body
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888, debug=True)
实战测试
以某大学人事管理系统为例,其 password 字段被加密,全程使用 AI 进行分析,并全程使用 AI 生成的代码进行测试。
- 输入触发 Skill 的提示词,AI 会提示我们需要输入的内容。
按 skill.md 的流程执行
-
在网页中提取我们所需的参数。
-
然后输入提示我们需要输入的所需参数至 codex。
目标址(URL):https://xxx/login/index
- 等待代码执行完成发现所需的代码均已创建完成。
Using skill js-reverse-automation (and its JSRPC/Flask/Burp templates) because the task matches the skill description.
-
启动 JSRPC。
-
在浏览器开发者工具的 Console 中,执行 JSRpc 项目中的
JsEnv_Dev.js文件内容。 -
在控制台注入 AI 生成的
jsrpc_password_md5.js。
// JSRPC injection for hr.ncu.edu.cn login password MD5
// Requires Hlclient to be available in the page context.
var client = new Hlclient("ws://127.0.0.1:12080/ws?group=fausto&name=burp");
var JSRPC_CONFIG = {
actionName: "generate_password_md5",
entry: {
type: "resolver",
resolver: function () {
if (window.$ && typeof $.md5 === "function") return $.md5;
if (window.jQuery && typeof jQuery.md5 === "function") return jQuery.md5;
return null;
},
},
bindThis: null,
async: false,
normalizeInput: function (param) {
if (param && typeof param === "object") {
if (param.password != null) return String(param.password);
if (param.pwd != null) return String(param.pwd);
}
return String(param == null ? "" : param);
},
normalizeOutput: function (result) {
return result;
},
onError: function (err) {
return "ERROR_" + Date.now();
},
};
function resolveEntry(config) {
if (config.entry.type === "resolver" && typeof config.entry.resolver === "function") {
return config.entry.resolver();
}
return null;
}
client.regAction(JSRPC_CONFIG.actionName, function (resolve, param) {
try {
var fn = resolveEntry(JSRPC_CONFIG);
if (typeof fn !== "function") throw new Error("md5 function not found");
var input = JSRPC_CONFIG.normalizeInput(param);
var ctx = JSRPC_CONFIG.bindThis || null;
var result = fn.call(ctx, input);
if (JSRPC_CONFIG.async && result && typeof result.then === "function") {
result.then(function (res) {
resolve(JSRPC_CONFIG.normalizeOutput(res));
}).catch(function (err) {
resolve(JSRPC_CONFIG.onError(err));
});
return;
}
resolve(JSRPC_CONFIG.normalizeOutput(result));
} catch (error) {
resolve(JSRPC_CONFIG.onError(error));
}
});
- 测试 JSRPC 调用函数是否正常。
http://127.0.0.1:12080/go?group=fausto&action=generate_password_md5¶m=111111
- 运行
flask_proxy_password.py。
from flask import Flask, request
import requests
import json
import logging
from urllib.parse import parse_qs, urlencode
import re
logging.basicConfig(level=logging.DEBUG)
app = Flask(__name__)
JSRPC_URL = "http://127.0.0.1:12080/go"
JSRPC_GROUP = "fausto"
JSRPC_ACTION = "generate_password_md5"
PASSWORD_FIELD = "password"
TIMEOUT = 5
CONTENT_LENGTH_RE = re.compile(r"Content-Length:\s*\d+", re.IGNORECASE)
def call_jsrpc(raw_password: str) -> str:
params_for_jsrpc = {
"group": JSRPC_GROUP,
"action": JSRPC_ACTION,
"param": raw_password,
}
try:
jsrpc_response = requests.get(JSRPC_URL, params=params_for_jsrpc, timeout=TIMEOUT)
jsrpc_response.raise_for_status()
result = jsrpc_response.json()
return result.get("data", "")
except requests.exceptions.RequestException as e:
app.logger.error(f"JSRPC request failed: {e}")
return ""
except json.JSONDecodeError as e:
app.logger.error(f"JSRPC response parse failed: {e}")
return ""
@app.route('/encode', methods=['POST'])
def handle_encode():
app.logger.info("- incoming autoDecoder request -")
data_body = request.form.get('dataBody', '')
data_headers = request.form.get('dataHeaders', '')
if not data_body:
app.logger.error("missing dataBody")
return data_headers + "\r\n\r\n\r\n\r\n" + data_body if data_headers else data_body
# Try JSON first
new_body = None
try:
json_data = json.loads(data_body)
raw_pwd = str(json_data.get(PASSWORD_FIELD, ""))
if raw_pwd:
json_data[PASSWORD_FIELD] = call_jsrpc(raw_pwd)
new_body = json.dumps(json_data, ensure_ascii=False, separators=(',', ':'))
except json.JSONDecodeError:
# Fallback to x-www-form-urlencoded
form = parse_qs(data_body, keep_blank_values=True)
raw_pwd = form.get(PASSWORD_FIELD, [""])[0]
if raw_pwd:
form[PASSWORD_FIELD] = [call_jsrpc(raw_pwd)]
new_body = urlencode(form, doseq=True)
if data_headers:
new_len = len(new_body)
new_headers = CONTENT_LENGTH_RE.sub(f"Content-Length: {new_len}", data_headers)
return new_headers + "\r\n\r\n\r\n\r\n" + new_body
return new_body
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888, debug=True)
- 测试 Flask 是否可以正常加密。
curl -X POST http://127.0.0.1:8888/encode
- 最后参考 Burp autoDecoder 配置说明配置 Burp 的 autoDecoder 插件,也成功加密了参数,整体完美运行下来了!
0x03 总结
本文成功实现了全自动化的 JS 分析和代码生成。对于复杂场景的 JS 代码分析可能仍需一定调试,但 AI 的优势在于可以反复迭代优化。期待更多思路交流和代码优化,共同提升安全测试能力。


