对 https://www.zhongjianjiantong.com 网站的 API 接口进行逆向分析,解决以下两个问题:
sign 请求头的生成方式data 字段)的解密方式POST https://www.zhongjianjiantong.com/Api/OrderRatingGoods/webDetail
Content-Type: application/json;charset=UTF-8
请求体示例:
{"rating_no":"519220343"}
| 请求头 | 示例值 | 说明 |
|---|---|---|
sign |
0e576e116bec064c34fb28f15906d6b3 |
32位hex,动态生成的签名 |
datetime |
2026-02-02 11:57:50 |
当前时间,与sign计算关联 |
{
"code": 200,
"msg": "",
"data": "leUdchIlW5WHRTVdKEVqc0En...(Base64密文)",
"iv": "894CA3EE5756EDF4"
}
data:Base64 编码的 AES 密文iv:16 字符的初始化向量(AES-CBC 模式使用)目标文件:https://www.zhongjianjiantong.com/web/static/js/index.e181df9a.js
该文件为 webpack 打包的业务代码(单行压缩格式),通过以下方式定位关键逻辑:
webDetail → 找到 API 接口定义位置interceptors.response → 找到 axios 响应拦截器JSON.parse → 找到解密函数 decryptedDataRESPONSE_KE → 找到密钥常量定义8fc7)在 JS 源码中找到以下常量定义:
// 模块 "8fc7" — 常量定义
var v = "3bd48ea5e910b195843941351be7cbae"; // RESPONSE_KE — 响应解密密钥
e.RESPONSE_KE = v;
var g = "1ba48ea2e910b666843941351be7cbad"; // REQUESR_KEY — 请求签名密钥(原文拼写错误)
e.REQUESR_KEY = g;
var p = "sign"; // e.SIGN — sign请求头名
var h = "datetime"; // e.DATETIME — datetime请求头名
d.interceptors.request.use(function(t) {
// 生成当前时间字符串
var o = moment(new Date().getTime()).format("YYYY-MM-DD HH:mm:ss");
// sign = MD5(REQUESR_KEY + 秒级时间戳)
t.headers[r.SIGN] = md5(r.REQUESR_KEY + new Date(o).getTime() / 1e3);
// datetime = 当前时间字符串
t.headers[r.DATETIME] = o;
return t;
});
签名算法:
sign = MD5("1ba48ea2e910b666843941351be7cbad" + 秒级时间戳)
其中秒级时间戳 = new Date("YYYY-MM-DD HH:mm:ss").getTime() / 1000
在 axios 响应拦截器中,接收到响应后调用 decryptedData 解密:
// 解密函数
var r = function(t) {
var e = t.encryptedData; // Base64密文(即 response.data)
var n = t.iv_data; // IV(即 response.iv)
// 密钥:UTF-8编码 RESPONSE_KE
var r = CryptoJS.enc.Utf8.parse("3bd48ea5e910b195843941351be7cbae");
// IV:UTF-8编码
var o = CryptoJS.enc.Utf8.parse(n);
// AES-CBC 解密
var s = CryptoJS.AES.decrypt(
{ ciphertext: CryptoJS.enc.Base64.parse(e) },
r,
{
iv: o,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
var c = s.toString(CryptoJS.enc.Utf8);
return c ? JSON.parse(c) : c;
};
| 项目 | 值 |
|---|---|
| 算法 | MD5 |
| 输入 | REQUEST_KEY + 秒级时间戳(整数) |
| REQUEST_KEY | 1ba48ea2e910b666843941351be7cbad |
| 输出 | 32位小写hex字符串 |
| 项目 | 值 |
|---|---|
| 算法 | AES |
| 模式 | CBC |
| 填充 | PKCS7 |
| 密钥 | 3bd48ea5e910b195843941351be7cbae(UTF-8编码,16字节) |
| IV | 响应JSON中的 iv 字段(UTF-8编码,16字节) |
| 密文 | 响应JSON中的 data 字段(Base64编码) |
| 明文 | JSON字符串 |
请求方向:
┌──────────┐ sign = MD5(KEY + timestamp) ┌──────────┐
│ 客户端 │ ──────────────────────────────────→ │ 服务端 │
│ │ headers: { sign, datetime } │ │
└──────────┘ └──────────┘
响应方向:
┌──────────┐ { data: Base64(AES_CBC(json)), iv: "..." } ┌──────────┐
│ 客户端 │ ←────────────────────────────────────────────── │ 服务端 │
│ │ AES.decrypt(data, RESPONSE_KEY, iv) │ │
└──────────┘ └──────────┘
pip install requests pycryptodome
import hashlib
import json
import base64
from datetime import datetime
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
RESPONSE_KEY = b"3bd48ea5e910b195843941351be7cbae"
REQUEST_KEY = "1ba48ea2e910b666843941351be7cbad"
def make_sign():
"""生成sign和datetime请求头"""
now = datetime.now()
dt_str = now.strftime("%Y-%m-%d %H:%M:%S")
timestamp = int(datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S").timestamp())
raw = REQUEST_KEY + str(timestamp)
sign = hashlib.md5(raw.encode()).hexdigest()
return sign, dt_str
def decrypt_response(data_b64, iv_hex):
"""AES-CBC解密响应数据"""
iv = iv_hex.encode("utf-8")
cipher = AES.new(RESPONSE_KEY, AES.MODE_CBC, iv)
ciphertext = base64.b64decode(data_b64)
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
return json.loads(plaintext.decode("utf-8"))
| JS(CryptoJS) | Python(pycryptodome) |
|---|---|
CryptoJS.enc.Utf8.parse(key) |
key.encode("utf-8") 或直接 b"..." |
CryptoJS.enc.Base64.parse(data) |
base64.b64decode(data) |
CryptoJS.AES.decrypt(...) |
AES.new(key, AES.MODE_CBC, iv).decrypt(...) |
CryptoJS.pad.Pkcs7 |
unpad(data, AES.block_size) |
CryptoJS.MD5(str).toString() |
hashlib.md5(str.encode()).hexdigest() |
sign 与 datetime 必须配对,且时间不能与服务器时间偏差过大,否则请求会被拒绝"894CA3EE5756EDF4" 就是 16 个 ASCII 字符 = 16 字节RESPONSE_KEY 为 32 个 hex 字符,但作为 UTF-8 字符串使用时是 32 字节,实际上 CryptoJS 默认会将超过 16 字节的密钥用于 AES-256;此处密钥恰好为 32 ASCII 字符 = 32 字节,对应 AES-256goods_uni_str)包含非 UTF-8 编码的字符,在 Python 中可能显示为乱码,这是原始数据问题而非解密错误