-
-
[原创][求助]数位观察 swguancha 响应 AES 解密逆向实战(JS 逆向 / 补环境 / execjs)
-
发表于: 2天前 448
-
数位观察 swguancha 响应 AES 解密逆向实战(JS 逆向 / 补环境 / execjs)
本文是「JS 逆向实战 30 例」系列第 6 篇,项目源码:41dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6K6j5$3!0G2L8o6p5I4x3W2)9J5c8X3!0H3k6h3&6v1f1%4u0W2N6X3g2J5M7$3f1`. (
Project6)
一、目标与背景
数位观察(swguancha)是一个行业数据资讯站,我的目标是抓它的文章列表接口。
- 站点:
64eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2K6N6$3N6#2j5h3&6U0K9r3q4Q4x3X3g2U0L8$3#2Q4x3V1j5`. - 目标接口:
ccaK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2M7s2m8Q4x3X3g2K6N6$3N6#2j5h3&6U0K9r3q4Q4x3X3g2U0L8$3#2Q4x3V1k6U0L8r3W2W2L8Y4c8Q4x3V1k6$3x3g2)9J5c8X3q4J5N6r3W2U0L8r3g2Q4x3V1k6U0L8r3W2W2L8Y4c8Q4x3V1k6H3j5h3N6W2 - 需还原字段:响应解密。请求参数是明文的(
queryType/current/size),但返回体不是普通 JSON,而是一段加密字符串,必须解密后才能拿到文章列表。 - 难度:⭐⭐(两星,属于响应解密里最温和的一类,适合作为“响应加密”入门练手)
这个项目的“逆向点”很纯粹:不涉及签名、不涉及 Cookie 风控,只解决一件事——把服务端返回的密文还原成明文 JSON。
二、抓包分析
打开 eabK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2K6N6$3N6#2j5h3&6U0K9r3q4Q4x3X3g2U0L8$3#2Q4x3V1j5`.,按 F12 切到 Network(网络) 面板,筛选 Fetch/XHR,然后在页面上翻页 / 切换栏目,就能看到 client/v1/article/client/page 这个请求被触发。
点开它看 Payload(请求负载),请求体是:
{"queryType":3,"current":2,"size":20}
完全是明文,没有任何混淆,current 就是页码。再切到 Response(响应),发现返回的不是我们期待的 {"code":0,"data":[...]},而是一个整体加密的字符串(形如一段 Base64 / 十六进制混合的密文)。这时我就判断:要还原的是响应解密,而不是请求签名。
再看请求头:Content-Type: application/json;charset=UTF-8,Origin 是 14aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2K6N6$3N6#2j5h3&6U0K9r3q4Q4x3X3g2U0L8$3@1`.,接口域名却是 app.swguancha.com(同站跨子域)。这类“前端展示域名 + 数据接口子域名”的结构很常见。
【图1:Network 面板中 article/client/page 请求的 Payload 与 Response —— 建议截图】
结论很明确:请求照抄即可,重点是把响应解密函数抠出来。
三、定位加密入口
既然是响应解密,我不需要去猜请求参数,而是直接找“谁在解密响应”。
最有效的办法是在 Sources(源代码) 面板里全局搜索关键字。因为接口返回的是一个字段,前端一定会去读它,所以我会搜一些典型词:
- 先搜接口路径的一部分:
article/client/page,能定位到发起请求的那个 ajax 封装; - 再顺着它的
.then()/success回调往里看,回调里对res做了什么处理; - 如果字符串被压缩得看不清,就搜解密库的典型特征:
AES、decrypt、CryptoJS、Pkcs7、ECB。
在这个站点里,我最终定位到一段类似这样的解密逻辑(核心就是 CryptoJS 的 AES):
// 站点前端里定位到的解密调用(示意)
var t = { data: res.encryptedString };
var n = CryptoJS.enc.Utf8.parse(key);
var r = CryptoJS.AES.decrypt(t.data, n, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
var i = r.toString(CryptoJS.enc.Utf8);
var s = JSON.parse(i);
模式是 AES / ECB / Pkcs7,密钥是一段写死的 32 位字符串,Key 直接用 Utf8.parse 解析(等价于 AES-256)。我把这段逻辑和密钥一起抠到本地,重写成仓库里的 js/swguancha1.js:
// 取自仓库真实 js:Project6/swguancha/js/swguancha1.js
var cryptoJS = require('crypto-js');
function Get_result(Data){
u = cryptoJS
var l = "QV1f3nHn2qm7i3xrj3Y9K9imDdGTjTu9"
var t = { data: Data }
var n = u.enc.Utf8.parse(l)
var r = u.AES.decrypt(t.data, n, {
mode: u.mode.ECB,
padding: u.pad.Pkcs7
})
i = r.toString(u.enc.Utf8)
s = JSON.parse(i);
return s.data.data
}
注意最后一行 return s.data.data:解密出来的 JSON 顶层还有个 data 包装,真正的文章列表在 data.data 里,这需要实际打印一次解密结果才能确认层级。
【图2:Sources 面板里 AES.decrypt 断点 / 调用栈 —— 建议截图】
四、JS 还原与补环境
这个项目几乎没有补环境成本,属于“抠代码”里最省心的类型:
- 站点前端本来就用 CryptoJS,所以我只需要一个能跑
require('crypto-js')的 Node 环境; - 我保留了原始函数签名
Get_result(Data),参数就是服务端返回的那一整段密文; - 因为密钥、IV、模式都在函数内部写死,调用方不需要传任何额外参数,Python 侧只负责把响应原文喂进来。
关于 AES 参数,几个容易搞错的点:
- ECB 不需要 IV:所以代码里没有
iv字段,不要自己硬加一个,会解密失败; - Key 用 Utf8.parse:
"QV1f3nHn2qm7i3xrj3Y9K9imDdGTjTu9"是 32 个可见字符,parse 后就是 32 字节,对应 AES-256; - padding 是 Pkcs7:这是 CryptoJS 默认的填充方式。
把这个逻辑用 Python 的 execjs 串起来之前,我先单独在 Node 里跑一遍验证,确认解密结果可读:
// 本地自测(示意)
var r = require('./swguancha1.js'); // 略
// 或者直接 node -e 分段验证
确认无误后,才交给 Python 侧封装。
五、Python 实现
仓库里的采集脚本 py/swguancha.py 结构非常直白:读 JS → execjs.compile → 发请求 → 把响应文本交给 Get_result:
# 取自仓库真实 py:Project6/swguancha/py/swguancha.py
import requests
import json
import os
import execjs
def Spider(page:int)->None:
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
JsPath = os.path.join(BASE_DIR,'..','js','swguancha1.js')
with open(JsPath, encoding="utf-8") as f:
Js_code = f.read()
Js = execjs.compile(Js_code)
headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Content-Type": "application/json;charset=UTF-8",
"Origin": "dacK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2K6N6$3N6#2j5h3&6U0K9r3q4Q4x3X3g2U0L8$3@1`.",
"Referer": "3d3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2K6N6$3N6#2j5h3&6U0K9r3q4Q4x3X3g2U0L8$3#2Q4x3V1j5`.",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"deviceType": "1",
"dnt": "1",
"sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Google Chrome\";v=\"138\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-gpc": "1"
}
url = "30dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2M7s2m8Q4x3X3g2K6N6$3N6#2j5h3&6U0K9r3q4Q4x3X3g2U0L8$3#2Q4x3V1k6U0L8r3W2W2L8Y4c8Q4x3V1k6$3x3g2)9J5c8X3q4J5N6r3W2U0L8r3g2Q4x3V1k6U0L8r3W2W2L8Y4c8Q4x3V1k6H3j5h3N6W2"
data = {
"queryType": 3,
"current": page,
"size": 20
}
data = json.dumps(data, separators=(',', ':'))
response = requests.post(url, headers=headers, data=data).text
res = Js.call("Get_result", response)
print(res)
Spider(2)
几个实现细节值得说明:
- 路径用
os.path拼:JsPath = os.path.join(BASE_DIR,'..','js','swguancha1.js'),保证无论在哪个目录执行脚本都能找到 JS; - 文件读写指定
encoding="utf-8":Windows 下默认是 GBK,不加会乱码或报错; json.dumps(..., separators=(',', ':')):去掉空格,和浏览器实际发的紧凑 JSON 保持一致,减少被识别的概率;Js.call("Get_result", response):直接把requests拿到的响应.text传进去,返回值已经是解析好的 Python 对象。
六、运行结果
执行 python swguancha.py,终端会打印出解密后的文章列表数据(列表 / 字典结构),能看到 title、id、发布时间等字段。大概长这样:
[{'id': 123456, 'title': '某行业观察文章标题', 'publishTime': '2024-xx-xx', ...},
{'id': 123457, 'title': '另一篇观察', 'publishTime': '2024-xx-xx', ...}]
只要字段是中文正常显示、不再是乱码或密文,就说明 AES 解密链路完全打通。
【图3:终端打印出的文章列表数据 —— 建议截图】
七、踩坑与注意
密钥长度与 parse 方式必须和站点一致。 我一开始把 32 位 Key 直接当字节数组手敲,结果长度对不上导致解密出空串。正确做法是把前端原样的
key字符串用CryptoJS.enc.Utf8.parse处理,别自己改写。ECB 不要加 IV。 很多人习惯性给 AES 都塞一个 iv,这里加了反而解不出来。
返回层级
s.data.data要确认。 第一版我写的是s.data,打印出来还是被包了一层,实际数据在data.data。响应可能变化。 如果某天解密报错,先确认接口返回体结构有没有变(比如外面又套了字段),再确认密钥是否被换过。
execjs 编码问题。 页面返回的都是 UTF-8,读取 JS 文件和传参时要保证编码一致,否则中文标题会花掉。
频率控制。 这是公开接口但没有明确授权,务必控制请求频率,遵守站点
robots.txt与服务条款。
八、总结
- 响应解密的核心是“找到解密调用”,而不是猜参数。 搜接口路径顺回调、搜
AES/decrypt/CryptoJS特征即可定位。 - AES 三要素要照抄:模式(ECB)、填充(Pkcs7)、Key 的解析方式(Utf8.parse),任意一处改动都会失败。
execjs+require('crypto-js')是响应解密最快的落地方式,几乎零补环境成本。
免责声明:本文仅供网络安全技术学习交流,请勿用于任何商业或非法用途。
[招生]科锐逆向工程师培训(2026年11月13日实地,远程教学同时开班, 第57期)
赞赏
- [原创]山西省政府采购电子卖场 nsssjss/sign 请求头签名逆向实战(JS 逆向 / RSA / 嵌套哈希 / execjs) 0
- [原创]36Kr 登录接口 sign 签名 + 密码 RSA 加密逆向实战(JS 逆向 / MD5 签名 / JSEncrypt / execjs) 0
- [原创]五矿电商 ec.minmetals 请求 param 参数 RSA 长加密逆向实战(JS 逆向 / JSEncrypt / 补环境 / subprocess node) 251
- [原创]中国观鸟记录中心 birdreport 请求 RSA 加密 + 响应 AES 解密逆向实战(JS 逆向 / JSEncrypt / 补环境 / execjs) 277
- [原创][求助]数位观察 swguancha 响应 AES 解密逆向实战(JS 逆向 / 补环境 / execjs) 446