视频加载失败

第 10 关:RSA 传密钥 + AES 加密报文(顺带一层 JavaScript 混淆)

1498 字
7 分钟
第 10 关:RSA 传密钥 + AES 加密报文(顺带一层 JavaScript 混淆)

本文是 LearnSpider 第 10 关的解密教程。这一关把「RSA + AES 混合加密」和「JavaScript 混淆」凑在一起:请求头 sm 是每次现算的,报文也不是明文,靶场还只放你书单里的前四页。

靶场:/books?challenge=CH-010(登录后打开) 参考解法:参考代码/s12.py

现象:请求头里有个看不懂的长串#

打开靶场,F12 → Network,只有一条 POST /api/challenges/CH-010/data:

sm: NBUhZ9tR...(很长的 base64)@P1q8oX...(另一段 base64)
  • 没有请求体:参数全在 sm 里;
  • 响应体不是 JSON:一串 base64 文本;
  • 页面上的 5 个页码里最后 1 个是灰的,怎么点都不动;
  • 把这条请求「Copy as cURL」重放一次 —— 大概率报 请求已过期。

辨型:两段字符 = RSA 传密钥、AES 传报文#

真实站点(spa16.scrape.center 那一类)就是这套组合拳,原因很实在:

算法用途为什么
RSA只加密对称密钥非对称加密慢、还有长度上限,塞不下整页数据
AES加密报文本体对称加密快,能加密任意长度
JS 混淆让密钥/算法不好直接在源码里搜到抬高静态分析成本

所以 sm 的两段是:

sm = base64( RSA_PKCS1_v1_5(AES 密钥明文) ) + "@" + base64( AES-128-CBC(明文 JSON) )

定位资源:脚本里躺着的三件套#

<head> 里依次加载三个脚本:

/static/vendor/crypto-js.js # AES(和第 11 关共用一份)
/static/vendor/jsencrypt.min.js # RSA
/static/books/security.js # 混淆过的业务逻辑:生成 sm + 解密响应

security.js 是 JavaScript Obfuscator 的产物:字符串全部收进数组、控制流被平坦化、还塞了死代码。别人怎么挖它的?两条路:

  1. 动态 hook(最快)——在控制台劫持几个函数,刷新页面,参数自己就打印出来了:
const raw = JSEncrypt.prototype.setPublicKey
JSEncrypt.prototype.setPublicKey = function (key) { console.log('PUBLIC KEY =\n' + key); return raw.call(this, key) }
const enc = CryptoJS.AES.encrypt
CryptoJS.AES.encrypt = function (plain, key, cfg) { console.log('明文:', plain, '密钥:', key.toString(CryptoJS.enc.Utf8)); return enc.apply(this, arguments) }
  1. 静态解混淆——用 de4js / synchrony 之类的工具还原字符串数组。注意:混淆器不只做了 base64,还把结果大小写翻转了一遍,所以直接 grep BEGIN 是搜不到的。把数组元素抓出来,swapcase() 之后再 base64 解码,才能看到 PEM:
for token in re.findall(r"['\"]([A-Za-z0-9+/=]{40,})['\"]", security_source):
for candidate in (token, token.swapcase()):
text = base64.b64decode(candidate + "=" * (-len(candidate) % 4)).decode("utf-8")
if "BEGIN PUBLIC KEY" in text:
print(text) # ← RSA 公钥到手(公钥本来就是公开的)

还原公式#

key_text = 16 个 0-9a-f 的随机串 # 每次请求现造一个
plain = json.dumps({username, path, timestamp, pageNumber, pageSize})
aes_part = base64(AES_CBC_PKCS7(plain, key_text, iv="LS2026CH010IV000"))
rsa_part = base64(RSA_PKCS1_v1_5(key_text, public_key))
sm = rsa_part + "@" + aes_part

服务端会逐项核对(顺序大致如下),这就是为什么”复用同一个 sm 重放”必然失败:

检查不通过时的表现
能否解出 RSA 段 + AES 段400 报文格式不正确(此时它还没法用你的密钥加密回复,只能明文说)
username 等于当前登录账号请求与登录账号不一致
path 等于接口路径请求路径与签名不一致
abs(now - timestamp) <= 300 秒请求已过期,请重新生成凭据(注意时间戳是毫秒)
pageNumber 在 1..5 范围内页码超出范围(一共 5 页)

页码只有序号:GET /api/challenges/CH-010/init(页面初始化时就会请求)只返回 username、page_count、visible_pages、page_size —— 不告诉你数据库里到底是哪五页。页面只放出前 visible_pages(=4)页,第 5 页只能自己写脚本取。

序号 n 对应哪一页数据由服务端按账号决定(每个账号一套,同人稳定、人人不同),所以在本地自己造一个 sm 去请求序号 1、2、3、4、5,就是你该拿的全部数据。

响应体同样是 AES:base64(AES_CBC_PKCS7(JSON, key_text, 同一个 IV)),用这次请求的 keyText 解。

完整脚本#

见 参考代码/s12.py(grab_public_key + build_sm + decrypt + 逐页汇总)。核心就三行:

sm, key_text = build_sm(public_key, username, page, page_size) # 现造密钥并封装(page 是序号)
body = decrypt(key_text, session.post(DATA_PATH, headers={"sm": sm}).text)
total += sum(row["id"] for row in body["data"]["list"]) # 五页取齐后求和

跑出来大概长这样:

账号 xiajiao 的书单:共 5 页(页面只放出前 4 页),每页 18 条
第 1 页:18 条,本页 id 合计 123642696
...
五页书籍 id 总和:601884792(提交这个数字)

排查清单#

  1. RSA.import_key 报 “RSA key format is not supported”:PEM 的换行丢了。从混淆产物里解出来的字符串里是 \n 转义,解码后要保留换行(s12.py 里直接 strip() 即可)。
  2. 解出来是乱码:IV 不对(必须是脚本里那个 16 字节常量)、密钥长度不对(16 字符 ≠ 16 位十六进制数,是按 UTF-8 取字节)、或者你用了 AES-ECB。
  3. 请求已过期:时间戳单位写成了秒,Date.now() 是毫秒。
  4. 页码超出范围:序号只有 1..5(/init 的 page_count),别把数据库页码当参数传。
  5. 400 报文格式不正确:sm 的分隔符不是 @、RSA 段没做 base64、或者公钥抄错了。
  6. 响应解不开:注意用的是本次请求的 keyText,不是第一次那个;每次请求都要重新生成。

常见问题#

Q:为什么不能只 RSA、或只 AES? A:只 RSA 传不了长报文;只 AES 就得把密钥写在前端(等于公开)。混合加密是两者的折中:密钥每次随机、且只能被持有私钥的服务端读到。

Q:公钥是公开的,那这套加密还有意义吗? A:有。它防的是「别人拿到你的报文改改就能用」——服务端核对 username/path/timestamp/页码,等于给请求绑上了会话与时效。

Q:为什么不干脆用浏览器自动化点五页? A:可以,但那是把「每页都要重算密钥」的成本交给浏览器,而且不方便汇总求和。真实项目里更常见的是脚本直连。

最后提醒#

这套东西只在授权范围内的练习环境里练手。真实站点有服务条款和法律法规,未经允许的批量抓取可能违规甚至违法。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

第 10 关:RSA 传密钥 + AES 加密报文(顺带一层 JavaScript 混淆)
https://jsnote.top/posts/ls-ch10/
作者
xiajiao
发布于
2026-10-02
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
第 14 关:Cookie 签名 —— 七层复合加密,和「密钥写在前端」这件事
爬虫靶场本文是 LearnSpider 第 14 关的解密教程。这一关的数据分 5 页、每页 10 个数字,但接口只认一个签过名的 sign Cookie:这个 Cookie 是前端一段混淆过的 JS连着过七层算法算出来的(拼盐 → MD5 派生密…
2
第 13 关:点选验证码 + 一次一密的轨迹报文 + 行为风控
爬虫靶场本文是 LearnSpider 第 13 关的解密教程。这一关的靶场是一张点选验证码:把图上所有文字点掉。看着像「识图题」,真正的坎却在后面 —— 提交的不是「点了哪几个坐标」,而是一整段鼠标轨迹(时间、坐标、速度),用 AES+RSA 加…
3
Python 爬虫验证码实战:AES 加密报文 + 每页算术验证码(CH-009)
爬虫靶场本文是 LearnSpider 第 9 关的解密教程。这一关把政务站点常见的一套反爬凑齐了:请求/响应都是 AES-ECB 加密的 hex,还要带一个前端写死的 Adam-AppKey,而且每翻一页都得先过一道算术验证码。目标是取 10 页…
4
第 11 关:无限 debugger + VM 混淆 + 签名请求头
爬虫靶场本文是 LearnSpider 第 11 关的解密教程。这一关的 JS 防守最"脏":无限 debugger(关键字还是拼出来的)、base64 源码交给 new Function 在运行时编译、请求头 m 要现算签名,票价则靠 base6…
5
第 12 关:接口下发 JS + 字符串混淆 + 内置 MD5 + 反调试
爬虫靶场本文是 LearnSpider 第 12 关的解密教程。这一关换了个思路:页面本身几乎是空的,真正的防守逻辑不在静态文件里,而是登录后由接口现发一段 JS 下来。这段 JS 里:关键字全被十六进制转义映射($dbsm_0x5d57)、一段字…
随机文章随机推荐

评论区

Profile Image of the Author
xiajiao
写爬虫,也写防爬虫的靶场。
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
站点统计
文章
15
分类
1
标签
33
总字数
22,317
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录