跳转到内容
123xiao | 无名键客

《Web逆向实战:基于浏览器开发者工具定位并还原前端加密请求参数的完整方法》

字数: 0 阅读时长: 1 分钟

背景与问题

很多同学第一次做 Web 逆向时,最容易卡住的不是“接口在哪”,而是“参数怎么来的”。

表面上看,浏览器发出的请求只是一个普通 POST,但你一打开抓包工具就会发现:

  • 请求体里出现了看不懂的 signtokenciphertext
  • 同一个接口,参数每次刷新都不一样
  • 直接复制请求重放,服务端返回“签名错误”或“非法请求”
  • 页面代码被打包、混淆,搜索关键字几乎找不到入口

这类问题的本质,不是“接口不能调”,而是前端在发请求前,对原始参数做了加工。常见加工形式包括:

  • 时间戳拼接
  • 字段排序
  • MD5/SHA/HMAC 摘要
  • AES/RSA 加密
  • Base64/Hex 编码
  • 动态 token 注入
  • 指纹、环境校验参与签名

这篇文章我会从浏览器开发者工具出发,带你完整走一遍定位链路:

  1. 先在 Network 里确认哪个请求有加密参数
  2. 再在 Sources 里断到“参数生成前后”
  3. 识别是编码、摘要还是对称/非对称加密
  4. 最后用可运行代码把参数还原出来

重点不是“背某个站的答案”,而是掌握一套可迁移的方法论


前置知识与环境准备

建议你至少具备这些基础:

  • 会使用 Chrome/Edge 开发者工具
  • 了解 HTTP 请求结构
  • 会读一些 JavaScript
  • 知道常见加密/编码概念:MD5、SHA、AES、RSA、Base64

建议准备环境:

  • 浏览器:Chrome 最新版
  • Node.js:18+
  • Python:3.10+(可选,用于辅助验证)
  • 编辑器:VS Code
  • 一个练手页面或你自己的测试站点

说明:本文讨论的是合法授权范围内的技术分析,例如自研系统调试、接口联调、安全测试、学习研究。不要将方法用于未授权目标。


核心原理

前端“加密参数”的生成,通常可以抽象成一个固定流水线:

flowchart LR
A[用户输入/业务参数] --> B[字段整理]
B --> C[附加动态值<br/>timestamp nonce token]
C --> D[排序/拼接/序列化]
D --> E[摘要或加密]
E --> F[编码转换]
F --> G[发送请求]

真正实战时,不要一上来就盯着“加密算法”。我更建议按下面顺序判断:

  1. 先看参数形态

    • 32 位十六进制:可能是 MD5
    • 40 位:可能是 SHA1
    • 很长的 Base64:可能是 AES/RSA 输出
    • JSON 包一层后再整体编码:可能是自定义封装
  2. 再看是否有动态因子

    • 时间戳是否参与
    • 随机数是否参与
    • Cookie / localStorage / sessionStorage 中的 token 是否参与
    • 浏览器指纹是否参与
  3. 最后才看具体实现

    • 是库函数调用,还是项目自定义封装
    • 是纯前端生成,还是先请求一个 seed/token 再签名

一个常见参数生成链路

sequenceDiagram
participant U as 用户操作
participant P as 页面脚本
participant S as 签名函数
participant N as Network请求
U->>P: 点击查询
P->>P: 收集 keyword/page
P->>P: 读取 timestamp/token
P->>S: buildSign(payload, token, ts)
S-->>P: 返回 sign/data
P->>N: 发起 POST 请求
N-->>P: 服务端响应

方法总览:如何用 DevTools 定位加密参数

我通常会按下面这条路径走,效率比较高。

第一步:在 Network 确认目标请求

打开开发者工具,切到 Network

  • 勾选 Preserve log
  • 勾选 Disable cache
  • 触发一次页面操作
  • 找到对应接口,重点看:
    • Request URL
    • Method
    • Query String Parameters
    • Payload / Form Data
    • Request Headers

你需要先回答两个问题:

1)哪些字段“像加密结果”?

例如请求体长这样:

{
  "data": "U2FsdGVkX1+8L2l2Wk...",
  "sign": "5f4dcc3b5aa765d61d8327deb882cf99",
  "t": 1721900000000
}

这里通常可以初步判断:

  • t 是时间戳
  • sign 像 MD5
  • data 像 Base64 编码后的密文

2)原始业务参数还在不在?

如果你输入的关键字是 "laptop",而请求里完全没有 "laptop" 的明文,那说明它大概率被封进了 data 里。


第二步:从发起请求的位置反查调用栈

在 Network 中点开请求,看 Initiator 或右键:

  • Open in Sources panel
  • Break on request
  • 查看调用栈 Call Stack

如果站点没严重混淆,这一步往往能直接把你带到:

  • axios.interceptors.request.use(...)
  • fetch(...) 包装函数
  • encrypt(data)sign(params) 之类的工具函数

这一步的目标不是一次找到算法,而是找到发请求前最后一跳


第三步:在 Sources 里打断点,观察“加密前”和“加密后”

这一招非常关键,也是很多人容易忽略的点:
不要试图直接读懂整个混淆文件,先断住再看变量。

推荐的断点位置:

  • 请求发送函数前
  • JSON.stringify(...) 附近
  • CryptoJS.*encryptsigndigest 附近
  • setRequestHeaderfetchXMLHttpRequest.send 附近

你要重点观察三类变量:

  1. 原始参数
  2. 中间态字符串
  3. 最终请求参数

例如:

payload = { keyword: "laptop", page: 1 }
baseStr = "keyword=laptop&page=1&t=1721900000000"
sign = md5(baseStr + secret)

只要能看到这三者之间的关系,还原基本就成功一半了。


第四步:识别算法类型

这里给一个实战中非常好用的判断表。

现象高概率类型排查方式
32位小写十六进制MD5md5 / hex_md5 / CryptoJS.MD5
固定长度更长哈希SHA 系列SHA1 / SHA256 / digest
明显 Base64 字符串Base64/AES/RSA 输出先尝试 Base64 解码
很长十六进制密文AES/自定义字节流编码看是否有 key/iv
每次都变但业务数据一致加了时间戳/随机数Date.now() / Math.random()
请求头里带 sign摘要验签看拦截器或 header 注入逻辑

实战演示:从页面请求还原 sign 参数

下面我用一个简化但贴近真实项目的例子演示。假设页面发送的请求如下:

{
  "keyword": "laptop",
  "page": 1,
  "t": 1721900000000,
  "sign": "e2d8f3c6b7d5a1c3..."
}

经过 DevTools 断点,我们观察到前端逻辑是:

  1. 收集业务参数:keywordpage
  2. 加上时间戳 t
  3. 按 key 排序后拼接为查询字符串
  4. 在末尾追加固定密钥 secret
  5. 计算 MD5,生成 sign

也就是说,签名规则是:

sign = MD5(sort(params).join("&") + secret)

页面中的原始逻辑(示意)

function buildSign(params, secret) {
  const keys = Object.keys(params).sort();
  const str = keys.map(k => `${k}=${params[k]}`).join("&");
  return md5(str + secret);
}

用 Node.js 还原签名

先安装依赖:

npm init -y
npm install crypto-js

编写 sign.js

const CryptoJS = require("crypto-js");

function buildSign(params, secret) {
  const sortedKeys = Object.keys(params).sort();
  const base = sortedKeys.map(key => `${key}=${params[key]}`).join("&");
  const sign = CryptoJS.MD5(base + secret).toString();
  return {
    base,
    sign
  };
}

const params = {
  keyword: "laptop",
  page: 1,
  t: 1721900000000
};

const secret = "my_private_key_123";

const result = buildSign(params, secret);

console.log("base string:", result.base);
console.log("sign:", result.sign);

运行:

node sign.js

如果你的输出和浏览器请求里的 sign 一致,就说明你已经完成了签名还原


实战演示:还原被 AES 加密的 data 参数

真实站点中,除了 sign,还有一类常见情况是把整个业务 JSON 加密后塞进一个 data 字段。

假设我们在断点中看到如下逻辑:

  • 原始对象:{ keyword: "laptop", page: 1 }
  • JSON.stringify 后得到明文
  • 使用 AES-CBC 加密
  • key 和 iv 写在前端代码里
  • 最后转为 Base64

那么还原时只要把这条链路复刻出来即可。


前端逻辑示意

function encryptData(obj) {
  const key = CryptoJS.enc.Utf8.parse("1234567890abcdef");
  const iv = CryptoJS.enc.Utf8.parse("abcdef1234567890");
  const text = JSON.stringify(obj);
  const encrypted = CryptoJS.AES.encrypt(text, key, {
    iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  });
  return encrypted.toString();
}

Node.js 可运行还原代码

const CryptoJS = require("crypto-js");

function encryptData(obj) {
  const key = CryptoJS.enc.Utf8.parse("1234567890abcdef");
  const iv = CryptoJS.enc.Utf8.parse("abcdef1234567890");
  const text = JSON.stringify(obj);

  const encrypted = CryptoJS.AES.encrypt(text, key, {
    iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  });

  return encrypted.toString();
}

function decryptData(cipherText) {
  const key = CryptoJS.enc.Utf8.parse("1234567890abcdef");
  const iv = CryptoJS.enc.Utf8.parse("abcdef1234567890");

  const decrypted = CryptoJS.AES.decrypt(cipherText, key, {
    iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  });

  return decrypted.toString(CryptoJS.enc.Utf8);
}

const payload = {
  keyword: "laptop",
  page: 1
};

const cipherText = encryptData(payload);
const plainText = decryptData(cipherText);

console.log("cipherText:", cipherText);
console.log("plainText:", plainText);

参数定位到还原的完整路径

flowchart TD
A[Network 发现 data/sign 异常字段] --> B[查看 Initiator]
B --> C[Sources 断点到请求发送前]
C --> D[观察原始对象与中间变量]
D --> E[识别算法与 key/iv/secret]
E --> F[在 Node.js 复刻逻辑]
F --> G[对比浏览器请求结果]
G --> H[完成参数还原]

逐步验证清单

实战里我很少一口气把整条链路写完,而是会按下面清单逐步验证。这样出错时更容易定位。

验证 1:业务参数是否一致

先确认你自己代码里的原始参数,与浏览器断点看到的一致:

{
  keyword: "laptop",
  page: 1
}

不要忽略:

  • 空字符串和 null
  • 数字和字符串类型差异
  • 布尔值序列化差异
  • 字段是否缺省

验证 2:动态参数是否一致

例如:

  • 时间戳 t
  • nonce
  • session token
  • cookie 中某个字段

这一步我踩过很多坑。你明明算法写对了,但时间戳取值不同,最终签名就完全不一样。


验证 3:拼接规则是否一致

重点检查:

  • 是否按 key 排序
  • 是否过滤空值
  • 是否 URL 编码
  • 是否用 & 拼接
  • 是否末尾多拼了 secret
  • 是否有固定前缀/后缀

验证 4:编码格式是否一致

例如:

  • UTF-8 还是 UTF-16
  • Base64 还是 Hex
  • AES key 是否先 Utf8.parse
  • RSA 明文是否分段

很多“差一个字符”的问题,本质上就是编码不一致。


常见坑与排查

这一节很重要。很多同学不是不会写代码,而是卡在细节里。

1. 只看 Network,不看调用栈

很多人抓到请求后,开始手动搜 sign。如果代码混淆严重,这样效率很低。
更好的办法是直接从 Initiator / Call Stack 倒推。

建议: 永远先找“谁发了这个请求”,再找“这个参数怎么算的”。


2. 把“编码”误判成“加密”

比如一个字段看起来很乱:

eyJrZXl3b3JkIjoibGFwdG9wIiwicGFnZSI6MX0=

这其实很可能只是 Base64,不是 AES。
先尝试解码,再判断是不是密文。

可以用浏览器控制台快速试:

atob("eyJrZXl3b3JkIjoibGFwdG9wIiwicGFnZSI6MX0=")

3. 忽略拦截器

很多项目不会在业务函数里直接写签名,而是统一在:

  • Axios request interceptor
  • fetch wrapper
  • 公共请求模块

里追加 header、token、sign。

现象: 你在页面代码里看不到 sign 的生成逻辑,但 Network 中它确实存在。
排查方向: 全局搜索 interceptors.request.usesetRequestHeaderfetch =XMLHttpRequest.prototype.send


4. 混淆后函数名不可读

比如你看到的是:

a.b(c(d(e)))

别硬啃全文。我的经验是:

  • 在关键调用处打断点
  • 看实参和返回值
  • 给变量改“心里名字”
  • 一层层缩小范围

真正有价值的是数据流,不是函数名。


5. 时间戳单位搞错

常见有两种:

  • 秒级:Math.floor(Date.now() / 1000)
  • 毫秒级:Date.now()

差 1000 倍,签名必错。


6. AES 模式、填充方式、iv 任一不一致

AES 不是只知道 key 就行,还要确认:

  • ECB / CBC / CTR
  • Pkcs7 / ZeroPadding
  • iv 是否固定
  • 输出是否 Base64

有一次我就踩在这里:key 对了,但漏了 iv,结果解出来全是乱码。


7. 参数顺序不一致

有些签名逻辑对字段顺序极其敏感:

a=1&b=2

b=2&a=1

哈希结果完全不同。


安全/性能最佳实践

这一部分既适合做逆向分析时提高效率,也适合前端/后端做防护时理解边界。

1. 不要把“前端加密”当成真正安全边界

只要算法、密钥、调用路径都在前端执行,理论上就可以被观察、复刻。
所以:

  • 前端加密更像“增加门槛”
  • 真正的权限控制、风控校验必须在服务端完成
  • 不要把核心密钥硬编码在前端当作最终防线

2. 优先分析数据流,不要迷信反混淆

大文件、压缩代码、webpack 打包很常见。
如果你一上来就想“全部反编译看懂”,很容易陷进去。

更高效的方式:

  • 从请求出发
  • 沿调用栈回溯
  • 只抓关键中间变量
  • 复刻最小闭环逻辑

3. 复刻代码时尽量保持“同构”

比如浏览器里用的是 CryptoJS,那你在 Node.js 里也优先用 crypto-js
这样可以减少:

  • 编码差异
  • 输出格式差异
  • mode / padding 差异

等拿到稳定结果后,再考虑换成原生 crypto 做性能优化。


4. 建议做“最小验证样本”

我一般会先固定一组参数:

{
  keyword: "laptop",
  page: 1,
  t: 1721900000000
}

只要这组样本能稳定复现,就说明链路正确。
之后再扩展到动态请求。


5. 关注浏览器存储与运行时环境

有些参数不直接写在代码里,而是来自:

  • localStorage
  • sessionStorage
  • document.cookie
  • window.__INITIAL_STATE__
  • 某个接口预先下发的 seed

所以不要只看函数体,还要看它依赖的上下文。


一个更完整的实战模板

如果你已经定位出规则,可以用下面这个模板快速拼装自己的还原脚本。

const CryptoJS = require("crypto-js");

function normalizeParams(params) {
  const result = {};
  for (const key of Object.keys(params)) {
    const value = params[key];
    if (value !== undefined && value !== null && value !== "") {
      result[key] = value;
    }
  }
  return result;
}

function buildBaseString(params) {
  const normalized = normalizeParams(params);
  return Object.keys(normalized)
    .sort()
    .map(key => `${key}=${normalized[key]}`)
    .join("&");
}

function buildSign(params, secret) {
  const base = buildBaseString(params);
  return CryptoJS.MD5(base + secret).toString();
}

function encryptPayload(obj, keyText, ivText) {
  const key = CryptoJS.enc.Utf8.parse(keyText);
  const iv = CryptoJS.enc.Utf8.parse(ivText);
  const text = JSON.stringify(obj);

  return CryptoJS.AES.encrypt(text, key, {
    iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  }).toString();
}

function buildRequestParams(input) {
  const t = input.t || Date.now();

  const biz = {
    keyword: input.keyword,
    page: input.page
  };

  const data = encryptPayload(biz, "1234567890abcdef", "abcdef1234567890");

  const signParams = {
    data,
    t
  };

  const sign = buildSign(signParams, "my_private_key_123");

  return {
    data,
    t,
    sign
  };
}

const req = buildRequestParams({
  keyword: "laptop",
  page: 1,
  t: 1721900000000
});

console.log(req);

这个模板覆盖了两种常见场景:

  • 业务参数整体加密成 data
  • 再对 data + t 生成签名 sign

很多站点虽然细节不同,但结构上就是这个思路。


排查思路图:请求失败时怎么快速定位

stateDiagram-v2
    [*] --> 检查原始参数
    检查原始参数 --> 检查动态参数: 原始参数正确
    检查原始参数 --> 修正业务字段: 原始参数错误

    检查动态参数 --> 检查拼接规则: 动态参数正确
    检查动态参数 --> 修正时间戳nonce: 动态参数错误

    检查拼接规则 --> 检查编码加密: 拼接正确
    检查拼接规则 --> 修正排序过滤规则: 拼接错误

    检查编码加密 --> 请求成功: 算法一致
    检查编码加密 --> 修正keyivmode: 算法不一致

总结

前端加密请求参数的还原,核心不是“会某一种算法”,而是掌握一条稳定路径:

  1. 在 Network 找到异常参数
  2. 从 Initiator / Call Stack 反查请求入口
  3. 在 Sources 关键位置打断点
  4. 观察原始值、中间值、最终值
  5. 识别排序、拼接、摘要、加密、编码规则
  6. 用 Node.js 复刻最小闭环

如果你让我把整篇文章压缩成一句实战建议,那就是:

不要先研究混淆代码,要先抓住数据是怎么流到请求里的。

最后给几个可执行建议:

  • 第一次还原时,优先选一个参数简单、触发链路短的接口
  • 每次只验证一个环节:原始参数、动态值、拼接串、sign、data
  • 复刻代码尽量与前端使用相同库,先求“结果一致”,再谈优化
  • 如果请求仍失败,优先怀疑动态参数、排序规则、编码格式,而不是马上怀疑算法错了

边界条件也要明确:

  • 如果签名依赖硬件指纹、浏览器环境、WASM、服务端下发一次性令牌,难度会显著上升
  • 如果密钥完全不在前端,而是服务端参与协商,那么前端复刻空间会小很多
  • 单纯“前端加密”不能替代后端鉴权,但对调试、联调、分析链路非常有帮助

只要你能在浏览器里把“加密前”和“加密后”都看见,剩下的工作通常只是耐心复刻。这个思路,我自己在很多项目里反复验证过,基本都能落地。


分享到:

上一篇
《区块链中智能合约安全审计实战:从常见漏洞识别到自动化检测流程搭建》
下一篇
《前端中级实战:基于 React 与 TypeScript 构建可维护的权限控制与动态路由方案》