目录
免责声明:本文内容仅用于合法授权范围内的技术学习、安全研究、逆向分析方法交流与风控防护理解,不针对任何 网站、产品或服务提供绕过、攻击、滥用或破坏性使用建议。文中涉及的接口分析、参数加解密、调试定位、代码复现、数据请求等内容,仅用于说明相关技术原理和分析流程。读者应在遵守相关法律法规、平台规则、robots 协议、用户协议以及获得合法授权的前提下进行学习和实验。请勿将本文中的方法、脚本或思路用于未授权访问、批量采集、账号撞库、绕过风控、破坏验证码体系、规避平台限制、侵犯数据权益、商业化滥用或影响线上系统稳定性的行为。对于真实网站案例,读者不应直接复制代码对线上服务进行高频请求或非授权调用。若相关网站、产品方、权利方或平台认为本文内容存在不适宜公开展示之处,可通过评论区、私信或作者主页提供的联系方式联系我;核实后将及时删除、替换或调整相关内容。读者因不当使用本文内容造成的任何法律责任、业务风险或经济损失,均由使用者自行承担,与作者无关。
一、分析
目标地址:https://cnpub.com.cn/information.html#/search?nav=1
抓取行业动态下的最新资讯,解析标题、作者、出自以及发布日期。
F12 打开 DevTools,切到 Network → Fetch/XHR,清空后翻页,捕获到数据接口:
text
POST https://cnpub.com.cn/prod-api/api/index/newsList
观察发现 Request Payload 和 Response 都是密文,看着像十六进制编码(全是 0-9 a-f 字符)。
精简请求头 :逐步删除 Cookie 和多余 Header,测试发现必须保留 'Content-Type': 'application/json;charset=UTF-8',否则请求失败。
定位思路 :请求体只有一个 密文字符串,没有明显的 key 名可以搜索。换个角度------从响应解密入手。服务端返回密文后,前端一定会解密再 JSON.parse() 成对象。所以 Hook JSON.parse 来反向定位解密函数。
Hook 代码(在 Sources 面板打 Script 断点 → Ctrl+F5 刷新 → 断住后在 Console 注入):
javascript
(function () {
const nativeParse = JSON.parse;
JSON.parse = function (text, reviver) {
debugger;
const result = nativeParse.call(JSON, text, reviver);
console.log("JSON.parse 拦截:", result);
return result;
};
JSON.parse.toString = function () {
return "function parse() { [native code] }";
};
console.log("JSON.parse Hook 已注入");
})();
注入的时机 :

注入后取消 Script 断点,F8 恢复执行。触发翻页后 debugger 命中,观察 text 变量确认是页面数据。
回溯堆栈:向上一层找到关键代码:
javascript
"string" == typeof e.data && (e.data = JSON.parse(Object(o["b"])(e.data)));
e.data 是密文,Object(o["b"]) 就是解密函数。点击 [[FunctionLocation]] 跳转到定义处:
javascript
r = (e, t="16weizifuchuan16", i="1suibianshurude6") => {
const a = s["enc"].Utf8.parse(t),
o = s["enc"].Utf8.parse(i),
r = s["AES"].decrypt(s["format"].Hex.parse(e), a, {
iv: o,
mode: s["mode"].CBC,
padding: s["pad"].Pkcs7
});
return s["enc"].Utf8.stringify(r).toString()
}
算法确认 :AES / CBC / PKCS7,密文格式为 Hex。密钥 "16weizifuchuan16",IV "1suibianshurude6",都是硬编码的默认参数。s 就是 CryptoJS。
在同一闭包中找到加密函数:
javascript
const o = (e, t="16weizifuchuan16", i="1suibianshurude6") => {
const a = s["enc"].Utf8.parse(t),
o = s["enc"].Utf8.parse(i);
let r = s["enc"].Utf8.parse(JSON.stringify(e));
const n = s["AES"].encrypt(r, a, {
iv: o,
mode: s["mode"].CBC,
padding: s["pad"].Pkcs7
});
return n.ciphertext.toString() // 输出 hex
}
断点观察加密函数的输入 e:
json
{"newsType":1,"pageNum":3,"pageSize":10,"title":""}
pageNum 是页码,其他字段固定。
二、JS 复现与验证
javascript
// cnpub.js 这里我用的是离线的 CryptoJS
let CryptoJS = require('../../CryptoJS')
let t = "16weizifuchuan16"
let i = "1suibianshurude6"
function encrypt(obj) {
const a = CryptoJS["enc"].Utf8.parse(t)
const o = CryptoJS["enc"].Utf8.parse(i)
let r = CryptoJS["enc"].Utf8.parse(JSON.stringify(obj))
const n = CryptoJS["AES"].encrypt(r, a, {
iv: o,
mode: CryptoJS["mode"].CBC,
padding: CryptoJS["pad"].Pkcs7
});
return n.ciphertext.toString()
}
function decrypt(data) {
const a = CryptoJS["enc"].Utf8.parse(t)
const o = CryptoJS["enc"].Utf8.parse(i)
const r = CryptoJS["AES"].decrypt(CryptoJS["format"].Hex.parse(data), a, {
iv: o,
mode: CryptoJS["mode"].CBC,
padding: CryptoJS["pad"].Pkcs7
});
return CryptoJS["enc"].Utf8.stringify(r).toString()
}
加密结果与浏览器一致,验证通过。注意服务端返回的密文字符串两端带引号 ",解密前需要 .replace('"', '') 去掉。
三、Python 实现
方式一:execjs 调用 JS
python
import json
import subprocess
from functools import partial
import requests
subprocess.Popen = partial(subprocess.Popen, encoding='utf-8')
import execjs
headers = {
'Content-Type': 'application/json;charset=UTF-8',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36',
}
ctx = execjs.compile(open('./cnpub.js', 'r', encoding='utf-8').read())
for page in range(1, 3):
data = {"newsType": 1, "pageNum": page, "pageSize": 10, "title": ""}
encrypted_data = ctx.call('encrypt', data)
response = requests.post(
'https://cnpub.com.cn/prod-api/api/index/newsList',
headers=headers, data=encrypted_data
)
res_encrypted_data = response.text.replace('"', '')
json_data = json.loads(ctx.call('decrypt', res_encrypted_data))
for row in json_data.get('list', {}).get('rows', []):
print({
"标题": row.get('title'),
"作者": row.get('author', ''),
"出自": row.get('platformName', ''),
"发布时间": row.get('publishTime').split(' ')[0],
})
方式二:纯 Python(pycryptodome)
文件名:cnpub_pure_crypto.py
python
import json
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import requests
KEY = b"16weizifuchuan16"
IV = b"1suibianshurude6"
headers = {
'Content-Type': 'application/json;charset=UTF-8',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36',
}
for page in range(1, 3):
# 加密: 每次必须新建 AES 对象(CBC 模式有状态,不能复用)
cipher_enc = AES.new(KEY, AES.MODE_CBC, IV)
plaintext = json.dumps(
{"newsType": 1, "pageNum": page, "pageSize": 10, "title": ""},
separators=(',', ':') # 必须紧凑格式,否则加密结果与服务端不一致
)
encrypted_data = cipher_enc.encrypt(pad(plaintext.encode('utf-8'), AES.block_size)).hex()
response = requests.post(
'https://cnpub.com.cn/prod-api/api/index/newsList',
headers=headers, data=encrypted_data
)
# 解密: 同样需要新建 AES 对象
cipher_dec = AES.new(KEY, AES.MODE_CBC, IV)
res_hex = response.text.replace('"', '')
decrypted = unpad(cipher_dec.decrypt(bytes.fromhex(res_hex)), AES.block_size).decode('utf-8')
json_data = json.loads(decrypted)
for row in json_data.get('list', {}).get('rows', []):
print({
"标题": row.get('title'),
"作者": row.get('author', ''),
"出自": row.get('platformName', ''),
"发布时间": row.get('publishTime')
})
四、踩坑记录
坑一:json.dumps 的空格问题
Python 的 json.dumps 默认输出带空格:{"key": "value"}(冒号和逗号后有空格)。但 JS 的 JSON.stringify 默认输出紧凑格式:{"key":"value"}。加密时输入不同,密文就不同,服务端解密后校验会失败。
python
# 默认格式,加密结果与网页不一致
json.dumps(data) # '{"newsType": 1, "pageNum": 1, ...}'
# 紧凑格式,与 JS 的 JSON.stringify 一致
json.dumps(data, separators=(',', ':')) # '{"newsType":1,"pageNum":1,...}'
坑二:AES CBC 模式不能复用对象
CBC 模式内部维护一个状态(上一个密文块作为下一轮的 IV)。加密完成后对象的内部状态已经变了,不能再拿来解密。每次加密/解密都必须 AES.new() 创建新对象。
python
# 复用同一个对象,解密会得到乱码
aes = AES.new(KEY, AES.MODE_CBC, IV)
ct = aes.encrypt(...)
pt = aes.decrypt(...) # 错误!内部 IV 已经变了
# 分别创建
cipher_enc = AES.new(KEY, AES.MODE_CBC, IV)
cipher_dec = AES.new(KEY, AES.MODE_CBC, IV)
坑三:响应密文带引号
服务端返回的响应体是 "81eff789..." 这样带双引号的字符串(因为 JSON 序列化了一次)。解密前需要去掉引号,否则 bytes.fromhex() 会报错。
五、总结
| 环节 | 要点 |
|---|---|
| 抓包 | POST 请求,请求体和响应都是 hex 密文,必须带 Content-Type 头 |
| 定位 | 请求体无明显 key 名,改从响应入手 Hook JSON.parse 反向定位 |
| 算法 | AES / CBC / PKCS7,密文格式为 Hex(不是 Base64) |
| 密钥 | Key "16weizifuchuan16"、IV "1suibianshurude6",硬编码明文 |
| 复现 | 标准 CryptoJS 无魔改,直接引库或 Python 重写 |
本案例的核心收获:
- 当请求体没有明显的参数名可搜索时,从响应解密方向入手(Hook
JSON.parse)是一个有效的替代策略。 - Python 重写对称加密时注意两个坑:
json.dumps的separators参数、CBC 模式 AES 对象不能复用。 - 密文格式不一定是 Base64,也可能是 Hex------看密文字符集(全是 0-9 a-f 就是 Hex)。