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

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

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

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
}

问题就来了:

  1. 加密逻辑藏在哪?
  2. 是纯哈希签名,还是先序列化再加密?
  3. 参数顺序有没有影响?
  4. 是否依赖浏览器环境,如 windowdocumentnavigator
  5. 脚本复现失败时,差在哪一步?

如果没有一套系统方法,很容易陷入“全局搜索 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-js
    • axios

安装依赖:

npm init -y
npm install axios crypto-js

核心原理

前端“加密请求参数”通常不是一个黑盒动作,而是一条从业务参数到最终请求体的加工链:

  1. 收集原始参数
  2. 做标准化处理(排序、去空、拼接)
  3. 加入时间戳、随机串、版本号
  4. 计算签名或加密
  5. 组装请求体
  6. 发起请求

可以把它理解为:

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
}

其规则如下:

  1. 业务参数转 JSON
  2. 使用 AES-CBC-Pkcs7 加密
  3. 使用 md5(ciphertext + "|" + timestamp + "|" + secret) 生成签名
  4. 最终发起 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

你要先判断:

  1. 加密内容是在 Body 里还是 Header 里?
  2. 是否存在典型字段:
    • sign
    • token
    • nonce
    • timestamp
    • data
    • enc
  3. 请求发起方式是:
    • fetch
    • XMLHttpRequest
    • 某个二次封装的请求库

我个人的经验是,别一上来就搜全局 md5。先看清楚目标请求长什么样,很多时候你会少走一半弯路。


第二步:利用 Initiator 追踪调用来源

在 Network 中选中目标请求,查看 Initiator 或调用栈信息。

它能告诉你:

  • 请求是从哪个 JS 文件发起的
  • 哪一行调用了 fetch / xhr.send
  • 是否经过了统一请求封装

如果项目是打包后的,文件名可能像:

app.8f3a12.js
chunk-vendors.34d9f.js

这很正常,不影响定位。


第三步:对 XHR/fetch 下断点

Sources 面板中:

  • 打开右侧的 XHR/fetch Breakpoints
  • 添加一个关键字,例如接口路径的一部分:/api/search
  • 重新触发请求

请求发出前,调试器会暂停。这时你就能观察:

  • 当前作用域中的参数
  • 调用栈
  • 函数入参和局部变量
  • 最终传给 fetchsend 的内容

这个步骤非常关键,因为它直接把你带到“请求发送现场”。


第四步:回溯参数生成过程

当断点停住后,不要只盯着当前一行。你需要沿调用栈往上看:

  • 哪个函数组装了请求体?
  • 哪个函数生成了 sign
  • 哪个函数生成了 data
  • t 是哪里来的?Date.now() 还是服务端同步时间?

常见迹象:

看到 JSON.stringify

说明原始对象可能先被序列化了。

看到 CryptoJS

大概率是常规前端加密库。

看到这些函数名或特征

  • encrypt
  • sign
  • getSign
  • encode
  • md5
  • sha1
  • sha256
  • parse
  • stringify
  • sort
  • join

看到难懂的混淆代码

比如:

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

真正的目标不是在浏览器里看懂,而是能在脚本里稳定复现。

抽离原则

  1. 只提取必要函数
  2. 去掉 DOM 依赖
  3. 把环境相关变量显式传参
  4. 先保证结果一致,再考虑优雅重构

下面是一个可直接运行的复现脚本。

新建 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-Agent
  • Origin
  • Referer
  • Authorization
  • 自定义头

常见坑与排查

这是最容易浪费时间的一部分,我直接按高频问题列。

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.userAgent
  • location.href
  • document.cookie
  • localStorage
  • canvas/webgl 指纹

此时 Node 里直接跑会报错或签名不同。

解决思路:

  • 显式补环境变量
  • 使用 jsdom
  • 或者直接在浏览器控制台执行并导出结果

6. Webpack 打包后变量难读

打包压缩后代码很丑,但并不代表无法分析。

技巧:

  • Pretty Print 美化代码
  • 搜索接口路径关键字
  • 搜索固定字段名:signdata
  • 从请求发送点反向追踪,而不是全局乱搜

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 中最小提取
  • 对比中间结果
  • 补齐浏览器环境依赖
  • 联调接口

总结

前端加密请求参数的还原,最重要的不是“记住多少算法”,而是掌握一套稳定的定位方法

  1. 先从 Network 锁定目标请求
  2. 用 Initiator 和 XHR/fetch 断点找到发送现场
  3. 沿调用栈回溯参数生成链
  4. 识别签名/加密算法及其输入
  5. 先在浏览器验证,再抽离到 Node 复现
  6. 逐层对比中间结果,别只盯最终请求

如果你只能记住一句话,我建议记这个:

Web 逆向不是先读懂全部代码,而是先抓住“数据是怎么变的”。

实际做项目时,我最常用的策略也是这个:先拿下一个成功样本,把每一步中间值钉住,再逐步脚本化。这样比“上来就全量迁移代码”稳定得多。

最后也提醒一句边界条件:如果站点把关键逻辑放到 WebAssembly、Native Bridge、动态风控环境或强依赖浏览器指纹中,分析成本会显著上升。这时本文的方法仍然有效,但你可能还需要结合更深的 Hook、环境补齐或运行时插桩手段。

如果你正在练习,建议先挑“CryptoJS + fetch”的站点做,最容易建立正反馈。等你把这条链路走顺了,再去碰更复杂的混淆和环境对抗,心里会稳很多。


分享到:

上一篇
《Spring Boot + MyBatis 实战:构建可维护的 Java Web 后台接口与统一异常处理体系》
下一篇
《Java 中基于 CompletableFuture 的异步编排实战:并行调用、超时控制与异常处理优化》