Web逆向实战:基于浏览器开发者工具定位并还原前端加密请求参数的完整方法
前端请求参数加密,几乎是 Web 逆向里绕不过去的一关。很多同学一开始会卡在同一个地方:明明抓到了请求,但参数完全看不懂,复制到脚本里也复现不出来。
这篇文章不讲“玄学经验”,而是按一条能落地的方法线来走:用浏览器开发者工具定位加密入口、分析参数生成链路、还原签名逻辑,并最终在脚本里复现请求。我会尽量按“带你做一遍”的方式写,适合已经有一定 JavaScript、HTTP 基础,但还没形成完整逆向套路的中级读者。
说明:本文内容用于安全测试、接口联调、自动化研究与合法授权场景。不要将方法用于未授权目标。
背景与问题
现在很多站点不会直接把业务参数明文发送,而是会在前端做一层或多层处理,比如:
- 时间戳拼接
- 参数排序
- 哈希签名(MD5 / SHA1 / SHA256)
- AES / DES / RSA 加密
- 混淆、Webpack 打包、动态加载
- 请求头里附带 token、nonce、sign
表面上看只是一个 POST 请求,实际上真正发出去的参数可能长这样:
{
"data": "U2FsdGVkX1+XW9....",
"sign": "e9e7c7f4d3...",
"t": 1716420000
}
而你在页面上输入的原始参数可能只是:
{
"keyword": "测试",
"page": 1
}
问题就来了:
- 加密逻辑藏在哪?
- 是纯哈希签名,还是先序列化再加密?
- 参数顺序有没有影响?
- 是否依赖浏览器环境,如
window、document、navigator? - 脚本复现失败时,差在哪一步?
如果没有一套系统方法,很容易陷入“全局搜索 sign”“到处下断点”“复制一堆混淆代码仍跑不通”的低效循环。
前置知识
建议你至少具备这些基础:
- 会使用 Chrome DevTools
- 理解 HTTP 请求结构:URL、Query、Body、Header、Cookie
- 会读基础 JavaScript
- 知道常见加密/摘要概念:MD5、SHA、AES、RSA
- 能用 Node.js 跑脚本
环境准备
本文演示思路以 Chrome + Node.js 为主。
浏览器侧
- Chrome 或 Edge
- DevTools 重点面板:
- Network
- Sources
- Console
- Application
脚本侧
- Node.js 18+
- 可选库:
crypto-jsaxios
安装依赖:
npm init -y
npm install axios crypto-js
核心原理
前端“加密请求参数”通常不是一个黑盒动作,而是一条从业务参数到最终请求体的加工链:
- 收集原始参数
- 做标准化处理(排序、去空、拼接)
- 加入时间戳、随机串、版本号
- 计算签名或加密
- 组装请求体
- 发起请求
可以把它理解为:
flowchart LR
A[用户输入/业务参数] --> B[参数标准化]
B --> C[加入时间戳/nonce]
C --> D[签名或加密]
D --> E[封装请求体/请求头]
E --> F[XHR/fetch 发送]
真正做逆向时,我们的目标不是“读懂所有混淆代码”,而是尽快回答以下几个关键问题:
- 最终发送的是哪个字段?
- 这个字段在哪一步生成?
- 生成它依赖哪些输入?
- 是否依赖运行时环境?
- 能否独立抽出最小复现逻辑?
常见前端加密模式
1. 纯签名
原始参数还是明文,只多了一个 sign:
sign = md5(path + sortedQuery + timestamp + secret)
特点:
- Network 面板里能看到明文参数
- 额外多一个 sign / token / signature
- 相对最好还原
2. 整体加密
原始参数先序列化再加密:
data = AES(JSON.stringify(payload), key, iv)
特点:
- 请求体只有一个
data - 明文参数在 Network 里看不到
- 要定位 key、iv、mode、padding
3. 混合模式
先签名,后加密;或 body 加密、header 签名。
这是最常见也最容易踩坑的情况。
一条实战方法线:从请求到加密函数
先给出整条定位路线图:
flowchart TD
A[在 Network 找到目标请求] --> B[确认请求方法、Body、Headers]
B --> C[从 Initiator/调用栈定位发送位置]
C --> D[在 Sources 搜索关键字段 sign/data/timestamp]
D --> E[对 XHR/fetch 下断点]
E --> F[回溯参数生成函数]
F --> G[识别摘要/加密算法]
G --> H[提取最小可运行代码]
H --> I[Node 脚本复现并对比结果]
下面进入具体操作。
背景与问题:为什么只看抓包不够
很多人第一反应是抓包工具一开,看见请求就开始复制。问题是:
- 抓包只能看到结果
- 看不到“参数是如何从明文变成密文的”
- 一旦参数中包含动态时间戳、随机串、环境指纹,就无法稳定复现
所以前端加密参数还原,本质上是一个浏览器端动态分析问题。而浏览器开发者工具,就是最轻量、最直接的分析入口。
实战案例设计
为了让代码可运行,我这里用一个简化但贴近真实场景的案例:
前端发送如下业务参数:
{
"keyword": "laptop",
"page": 1
}
但实际请求会变成:
{
"data": "<AES密文>",
"sign": "<MD5签名>",
"t": 1716400000000
}
其规则如下:
- 业务参数转 JSON
- 使用 AES-CBC-Pkcs7 加密
- 使用
md5(ciphertext + "|" + timestamp + "|" + secret)生成签名 - 最终发起 POST
我们先模拟一个前端实现,再演示如何定位与还原。
实战代码(可运行)
1)模拟前端加密逻辑
新建 frontend-sim.js:
const CryptoJS = require('crypto-js');
const AES_KEY = CryptoJS.enc.Utf8.parse('1234567890abcdef');
const AES_IV = CryptoJS.enc.Utf8.parse('abcdef1234567890');
const SIGN_SECRET = 'my_sign_secret';
function encryptPayload(payload) {
const plaintext = JSON.stringify(payload);
const encrypted = CryptoJS.AES.encrypt(
plaintext,
AES_KEY,
{
iv: AES_IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
return encrypted.toString();
}
function buildSign(ciphertext, timestamp) {
return CryptoJS.MD5(`${ciphertext}|${timestamp}|${SIGN_SECRET}`).toString();
}
function buildRequestBody(payload) {
const t = Date.now();
const data = encryptPayload(payload);
const sign = buildSign(data, t);
return { data, sign, t };
}
const payload = {
keyword: 'laptop',
page: 1
};
console.log(buildRequestBody(payload));
运行:
node frontend-sim.js
你会得到一组动态结果。
2)模拟服务端验签与解密
新建 server-verify.js:
const CryptoJS = require('crypto-js');
const AES_KEY = CryptoJS.enc.Utf8.parse('1234567890abcdef');
const AES_IV = CryptoJS.enc.Utf8.parse('abcdef1234567890');
const SIGN_SECRET = 'my_sign_secret';
function buildSign(ciphertext, timestamp) {
return CryptoJS.MD5(`${ciphertext}|${timestamp}|${SIGN_SECRET}`).toString();
}
function decryptPayload(ciphertext) {
const decrypted = CryptoJS.AES.decrypt(
ciphertext,
AES_KEY,
{
iv: AES_IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8));
}
function verifyRequestBody(body) {
const expectedSign = buildSign(body.data, body.t);
if (expectedSign !== body.sign) {
throw new Error('签名不匹配');
}
return decryptPayload(body.data);
}
// 把这里替换成 frontend-sim.js 生成的数据
const body = {
data: '请替换为实际密文',
sign: '请替换为实际签名',
t: 1716400000000
};
try {
const payload = verifyRequestBody(body);
console.log('验签成功,解密结果:', payload);
} catch (err) {
console.error('失败:', err.message);
}
这段代码主要用于帮助你理解:逆向的目标其实就是找出服务端预期的同一套规则。
用浏览器开发者工具定位加密入口
下面进入重点:假设你面对的是一个真实网页,而不是上面的模拟代码,该怎么定位?
第一步:在 Network 里锁定目标请求
打开 DevTools,进入 Network 面板,完成一次页面操作,比如点击搜索按钮。
重点看:
- 请求 URL
- Request Method
- Query String Parameters
- Request Payload / Form Data
- Request Headers
- Response
你要先判断:
- 加密内容是在 Body 里还是 Header 里?
- 是否存在典型字段:
signtokennoncetimestampdataenc
- 请求发起方式是:
fetchXMLHttpRequest- 某个二次封装的请求库
我个人的经验是,别一上来就搜全局
md5。先看清楚目标请求长什么样,很多时候你会少走一半弯路。
第二步:利用 Initiator 追踪调用来源
在 Network 中选中目标请求,查看 Initiator 或调用栈信息。
它能告诉你:
- 请求是从哪个 JS 文件发起的
- 哪一行调用了
fetch/xhr.send - 是否经过了统一请求封装
如果项目是打包后的,文件名可能像:
app.8f3a12.js
chunk-vendors.34d9f.js
这很正常,不影响定位。
第三步:对 XHR/fetch 下断点
在 Sources 面板中:
- 打开右侧的 XHR/fetch Breakpoints
- 添加一个关键字,例如接口路径的一部分:
/api/search - 重新触发请求
请求发出前,调试器会暂停。这时你就能观察:
- 当前作用域中的参数
- 调用栈
- 函数入参和局部变量
- 最终传给
fetch或send的内容
这个步骤非常关键,因为它直接把你带到“请求发送现场”。
第四步:回溯参数生成过程
当断点停住后,不要只盯着当前一行。你需要沿调用栈往上看:
- 哪个函数组装了请求体?
- 哪个函数生成了
sign? - 哪个函数生成了
data? t是哪里来的?Date.now()还是服务端同步时间?
常见迹象:
看到 JSON.stringify
说明原始对象可能先被序列化了。
看到 CryptoJS
大概率是常规前端加密库。
看到这些函数名或特征
encryptsigngetSignencodemd5sha1sha256parsestringifysortjoin
看到难懂的混淆代码
比如:
a[_0x1234(0x1f)](b,c,d)
不要慌,先观察输入输出。逆向时,函数名不重要,数据流最重要。
一个典型调用时序
sequenceDiagram
participant U as 用户操作
participant P as 页面业务代码
participant E as 参数加密模块
participant N as 网络层
participant S as 服务端
U->>P: 点击搜索
P->>E: 传入 {keyword,page}
E->>E: JSON序列化
E->>E: AES加密 data
E->>E: 生成 sign
E->>P: 返回 {data,sign,t}
P->>N: fetch/XHR 发送
N->>S: POST 请求
S-->>N: 响应结果
N-->>P: 返回业务数据
这个时序图背后的核心点是:你不用先理解整站,只要拿下 E 这个“加密模块”就够了。
如何识别具体算法
实际定位到函数后,下一步是判断它到底做了什么。
1. 哈希签名的特征
如果看到:
CryptoJS.MD5(...)
CryptoJS.SHA1(...)
CryptoJS.SHA256(...)
那通常是摘要签名,不可逆。你要做的是复现输入字符串的拼接规则,而不是“解密”。
重点核对:
- 参数是否排序
- 分隔符是什么
- 是否拼接固定密钥
- 是否包含路径、UA、token
- 是否转小写/大写
- 是否做了 URL 编码
示例:
const signStr = `keyword=${keyword}&page=${page}&t=${t}&secret=${secret}`;
const sign = md5(signStr);
一个字符差了,结果就完全不同。
2. 对称加密的特征
如果看到:
CryptoJS.AES.encrypt(...)
CryptoJS.DES.encrypt(...)
重点提取:
- key
- iv
- mode(CBC / ECB)
- padding(Pkcs7 / ZeroPadding)
- 输入格式
- 输出格式(Base64 / Hex)
这几项任何一项不一致,脚本就复现不出来。
3. 非对称加密的特征
如果看到:
JSEncrypt
RSA
publicKey
encryptLong
通常是用公钥加密敏感字段,如密码。它一般用于登录,不太常用于整包业务参数。
从浏览器里“验证猜想”
定位函数之后,不要急着抄代码。先在 Console 里做最小验证。
比如你在断点处拿到了一个加密函数 buildSign,可以直接试:
buildSign("abc", 1716400000000)
或者查看函数源码:
buildSign.toString()
如果页面作用域里能直接访问到目标函数,这一步效率极高。
也可以临时 Hook
比如 Hook fetch,打印请求参数:
const rawFetch = window.fetch;
window.fetch = async function (...args) {
console.log('fetch args:', args);
debugger;
return rawFetch.apply(this, args);
};
或者 Hook XMLHttpRequest.send:
const rawSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function (body) {
console.log('xhr body:', body);
debugger;
return rawSend.call(this, body);
};
这类 Hook 很适合在站点代码复杂、调用链太深时使用。
将前端逻辑抽离到 Node.js
真正的目标不是在浏览器里看懂,而是能在脚本里稳定复现。
抽离原则
- 只提取必要函数
- 去掉 DOM 依赖
- 把环境相关变量显式传参
- 先保证结果一致,再考虑优雅重构
下面是一个可直接运行的复现脚本。
新建 replay-request.js:
const axios = require('axios');
const CryptoJS = require('crypto-js');
const AES_KEY = CryptoJS.enc.Utf8.parse('1234567890abcdef');
const AES_IV = CryptoJS.enc.Utf8.parse('abcdef1234567890');
const SIGN_SECRET = 'my_sign_secret';
function encryptPayload(payload) {
return CryptoJS.AES.encrypt(
JSON.stringify(payload),
AES_KEY,
{
iv: AES_IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
).toString();
}
function buildSign(ciphertext, timestamp) {
return CryptoJS.MD5(`${ciphertext}|${timestamp}|${SIGN_SECRET}`).toString();
}
function buildBody(payload) {
const t = Date.now();
const data = encryptPayload(payload);
const sign = buildSign(data, t);
return { data, sign, t };
}
async function main() {
const payload = {
keyword: 'laptop',
page: 1
};
const body = buildBody(payload);
console.log('request body:', body);
// 如果你有真实接口,把 url 换掉即可
// const resp = await axios.post('https://example.com/api/search', body, {
// headers: {
// 'Content-Type': 'application/json'
// }
// });
// console.log(resp.data);
}
main().catch(console.error);
逐步验证清单
这里给你一个很实用的验证顺序。不要一步到位发请求,按层验证更稳。
验证 1:原始参数一致
确认你传入脚本的业务参数,和浏览器里完全一致:
- 字段名
- 字段类型
- 是否有默认值
- 是否包含空字段
验证 2:序列化结果一致
打印:
JSON.stringify(payload)
看是否和浏览器里一样。
我踩过一个坑:浏览器里字段顺序是固定的,但我在脚本里重组对象时顺序变了,签名直接不同。
验证 3:时间戳一致
有些站点要求秒级时间戳,有些要求毫秒级,还有些会校验时间窗口。
验证 4:加密结果一致
在浏览器断点处拿到某次明文输入,用同样参数在 Node 中执行,确认密文是否一致。
验证 5:签名结果一致
如果密文一致但 sign 不一致,问题基本就在:
- 拼接字符串
- 编码格式
- 大小写
- secret 不完整
验证 6:请求头一致
有些站点真正校验的不只是 body,还包括:
User-AgentOriginRefererAuthorization- 自定义头
常见坑与排查
这是最容易浪费时间的一部分,我直接按高频问题列。
1. 参数顺序不一致
很多签名逻辑会先对参数 key 排序:
Object.keys(params).sort()
如果你脚本里没有照做,结果一定不同。
排查方式:
console.log('sign input =', signInput);
拿浏览器里的拼接字符串逐字符对比。
2. 时间戳单位弄错
常见情况:
- 秒:
Math.floor(Date.now() / 1000) - 毫秒:
Date.now()
差 1000 倍,看着像对了,其实完全不对。
3. Base64 / Hex 混淆
有些加密结果在页面里显示为字符串,但底层格式不同。
例如:
encrypted.toString()
encrypted.ciphertext.toString(CryptoJS.enc.Hex)
这两个结果不是一回事。
4. IV 或 Key 经过二次处理
看起来 key 是一段字符串,实际上可能先经过:
- UTF-8 parse
- Base64 decode
- 截断
- 补位
例如:
CryptoJS.enc.Utf8.parse(key.slice(0, 16))
你要复现的是“最终参与加密的 key”,不是肉眼看到的原始字符串。
5. 依赖浏览器环境
有些签名会用到:
navigator.userAgentlocation.hrefdocument.cookielocalStoragecanvas/webgl 指纹
此时 Node 里直接跑会报错或签名不同。
解决思路:
- 显式补环境变量
- 使用
jsdom - 或者直接在浏览器控制台执行并导出结果
6. Webpack 打包后变量难读
打包压缩后代码很丑,但并不代表无法分析。
技巧:
- 用 Pretty Print 美化代码
- 搜索接口路径关键字
- 搜索固定字段名:
sign、data - 从请求发送点反向追踪,而不是全局乱搜
7. 请求发起前又被二次封装
有时你看到的 body 还不是最终 body。比如中间请求拦截器又统一加了一层签名。
可重点看:
- axios interceptors
- fetch wrapper
- request middleware
8. 随机数导致无法复现
比如 nonce 使用:
Math.random().toString(16).slice(2)
如果请求里包含 nonce,你必须把浏览器同一次请求的 nonce 也一起纳入比较。否则会误判“算法不对”。
安全/性能最佳实践
这部分不只是给“站点开发者”,也给做逆向分析和自动化复现的人。
1. 不要把前端加密等同于真正安全
前端加密更多是:
- 增加分析门槛
- 防止明文裸传
- 提高批量滥用成本
但只要密钥、算法、参数拼接逻辑在前端可执行,理论上就能被分析和复现。
所以真正的安全边界仍然应该在服务端:
- 服务端验签
- 时效控制
- 风控限流
- 行为检测
- 权限校验
2. 对分析者来说,优先做最小复现
不要把整站 JS 全搬到 Node。这样维护成本很高,也容易因环境依赖崩掉。
正确思路:
- 只抽取加密相关核心函数
- 把外部依赖写清楚
- 建立输入输出测试用例
3. 做一致性快照
当你成功复现一次后,建议立刻保存:
- 原始业务参数
- 时间戳
- nonce
- sign 输入串
- 最终请求体
- 请求头
这样后面站点升级时,你能快速判断是:
- 算法变了
- key 变了
- 拼接顺序变了
- 还是仅仅多了字段
4. 控制调试开销
如果页面请求很多,建议:
- 在 Network 里按接口名过滤
- 只对特定 URL 添加 XHR/fetch 断点
- 避免全局 Hook 太多函数
否则调试器频繁停住,效率会很差。
一个推荐的实战工作流
当你面对一个陌生站点时,可以按这个流程走:
stateDiagram-v2
[*] --> 抓目标请求
抓目标请求 --> 确认加密字段
确认加密字段 --> 发送点断点
发送点断点 --> 回溯生成链路
回溯生成链路 --> 识别算法
识别算法 --> 浏览器内验证
浏览器内验证 --> Node最小复现
Node最小复现 --> 请求联调
请求联调 --> [*]
这套流程的价值在于:把“碰运气式逆向”变成“可重复的方法”。
一份可执行的排查模板
如果你现在手上就有一个站点,可以直接照着核对:
请求层
- 找到目标接口
- 确认方法、URL、Body、Headers
- 识别可疑字段:
data/sign/timestamp/nonce
定位层
- 看 Initiator
- 对接口路径加 XHR/fetch 断点
- 找到发送函数
- 回溯到加密/签名函数
还原层
- 记录原始参数
- 记录序列化结果
- 记录时间戳/随机串
- 记录 sign 输入字符串
- 记录 key/iv/mode/padding
复现层
- 在 Node 中最小提取
- 对比中间结果
- 补齐浏览器环境依赖
- 联调接口
总结
前端加密请求参数的还原,最重要的不是“记住多少算法”,而是掌握一套稳定的定位方法:
- 先从 Network 锁定目标请求
- 用 Initiator 和 XHR/fetch 断点找到发送现场
- 沿调用栈回溯参数生成链
- 识别签名/加密算法及其输入
- 先在浏览器验证,再抽离到 Node 复现
- 逐层对比中间结果,别只盯最终请求
如果你只能记住一句话,我建议记这个:
Web 逆向不是先读懂全部代码,而是先抓住“数据是怎么变的”。
实际做项目时,我最常用的策略也是这个:先拿下一个成功样本,把每一步中间值钉住,再逐步脚本化。这样比“上来就全量迁移代码”稳定得多。
最后也提醒一句边界条件:如果站点把关键逻辑放到 WebAssembly、Native Bridge、动态风控环境或强依赖浏览器指纹中,分析成本会显著上升。这时本文的方法仍然有效,但你可能还需要结合更深的 Hook、环境补齐或运行时插桩手段。
如果你正在练习,建议先挑“CryptoJS + fetch”的站点做,最容易建立正反馈。等你把这条链路走顺了,再去碰更复杂的混淆和环境对抗,心里会稳很多。