【JS 逆向实战】Python + Node.js 突破 RSA/AES 混合加密与接口动态签名
环境介绍
电脑上必须同时具备 Python 和 Node.js 两种运行环境。
Python 环境: 建议使用 Python 3.7 或以上版本。它是整个爬虫的“主控中心”,负责发送请求、调度 JS 脚本以及将数据保存为 CSV 文件。
官网:https://www.python.org/downloads/
点击跳转官网
Node.js 环境: 这是必不可少的底层环境。因为 Python 的 execjs 库在底层需要依赖 Node.js 来执行 看鸟.js 中的 JavaScript 逻辑。
官网:https://nodejs.org/zh-tw/download
点击跳转官网
第三方依赖包
总结:Python + JS 逆向爬虫项目的环境配置与技术栈梳理
这套代码是一个非常典型的“Python 爬虫 + JavaScript 逆向”实战项目。它通过 Python 模拟网络请求,并利用本地环境执行解密脚本,从而绕过目标网站的接口加密。
为了让你在本地顺利跑通这套代码,或者为之后的逆向安全进阶做准备,以下是该项目所需的环境配置和核心技术栈总结。
一、 运行环境配置
电脑上必须同时具备 Python 和Node.js两种运行环境。
Python 环境: 建议使用Python 3.7 或以上版本。它是整个爬虫的“主控中心”,负责发送请求、调度 JS 脚本以及将数据保存为 CSV 文件。
Node.js 环境: 这是必不可少的底层环境。因为 Python 的 execjs 库在底层需要依赖 Node.js 来执行看鸟.js中的 JavaScript 逻辑。
二、 必须安装的第三方依赖包
在配置好基础环境后,你需要通过包管理器安装以下第三方依赖。
- Python 端依赖 (通过 pip 安装)
用于发送 HTTP POST 请求、管理 Session 会话以及处理响应数据。
PyExecJS: 在代码中体现为 import execjs,用于在 Python 进程中加载并运行编译好的 JavaScript 代码。
pip3 install requests
pip3 install execjs
- Node.js 端依赖 (通过 npm 安装)
你需要在此脚本所在的同级目录下,打开终端并安装以下依赖,否则 JS 代码在被 Python 调用时会报错“找不到模块”:
node-encrypt-js: 用于处理 RSA 长文本加密(对应 JS 代码中的 JSEncrypt)。
crypto-js: 非常核心的密码学前端库,在此代码中用于执行 AES 对称解密。
(注:代码中引入的 crypto 是 Node.js 的内置模块,用于 MD5 摘要计算,无需额外通过 npm 安装。)
npm install crypto-js
npm install node-encrypt-js
npm install node-jsencrypt
项目描述
本项目是一个基于 Python 与 Node.js 跨环境交互构建的进阶网络爬虫系统。针对“中国观鸟记录中心”的动态数据接口,项目进行了深度的 Web 逆向分析,成功突破了目标服务器的请求鉴权与数据混淆机制,实现了全网观鸟活动记录(包含观测点位、时间、用户标识等核心维度)的自动化采集、解密与结构化持久存储
核心功能与技术亮点包括:多重前端加密体系突破,跨语言算法环境调度,响应数据流逆向解析,健壮的数据清洗与持久化。
技术栈
HTTP/HTTPS 协议与会话管理: 使用 requests.Session() 维持连接状态,并动态更新请求头(如动态注入 JS 生成的 timestamp、requestId 和 sign)。
跨环境代码执行: 熟练使用execjs.compile()将复杂的 JS 逆向逻辑包装为黑盒,通过 Python 传递参数并接收处理结果。
RSA 非对称加密: 提取了目标网站的公钥 (paramPublicKey),对请求的明文参数进行了加密处理。
MD5 摘要算法: 将请求数据、时间戳和 UUID 拼接后进行 MD5 Hash,生成用于防篡改的签名字段 (sign)。
AES 对称解密: 解析目标服务器返回的密文,使用硬编码的 Key (C8EB5514…) 和 IV (55DD79…),采用 CBC 模式和 Pkcs7 填充方式还原出 JSON 明文。
数据清洗与持久化: 使用 Python 的内置 json 模块反序列化数据,并利用标准的文件 I/O 操作将数据按特定格式追加写入 CSV 文件中。
逆向的完整流程
确定目标URL
- 进入数据页面,打开F12抓包工具,点击
下一页进行数据包抓取,观察到参数进行加密处理了,返回值也进行加密处理了,请求头中sign值和requestid都也进行了加密处理

- 到调用栈中去观察一下,发现是
ajax请求,则进入发送ajax请求调用栈下断点去观察,发现还是明文
- 再进行下一个跟栈发现
r的值多了sign值
- 继续往前找r的值变化,来确定r的生成过程,采用单步调试,每走一步观察r的变化

- 此时在
if (k.beforeSend && (k.beforeSend.call(l, v, k) === !1 || 2 === t))这一步的下一步,r就有被加密的值了,故r的值加密一定发生在这一步,进入k.beforeSend函数中
- 很明显是一个
RSA加密,接下来扣代码,缺少函数则进入对应函数补下来
var JSEncrypt = require("node-encrypt-js");
var crypto = require("crypto");
// 补 sort_ASCII
function sort_ASCII(obj)
{
var arr = new Array;
var num = 0;
for (var i in obj) {
arr[num] = i;
num++
}
var sortArr = arr.sort();
var sortObj = {};
for (var i in sortArr) {
sortObj[sortArr[i]] = obj[sortArr[i]]
}
return sortObj
}
// 补 dataTojson
function dataTojson(data) {
var arr = [];
var res = {};
arr = data.split("&");
for (var i = 0; i < arr.length; i++) {
if (arr[i].indexOf("=") != -1) {
var str = arr[i].split("=");
if (str.length == 2) {
res[str[0]] = str[1]
} else {
res[str[0]] = ""
}
} else {
res[arr[i]] = ""
}
}
return res
}
// 补 getUuid()
function getUuid() {
var s = [];
var hexDigits = "0123456789abcdef";
for (var i = 0; i < 32; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 16), 1)
}
s[14] = "4";
s[19] = hexDigits.substr(s[19] & 3 | 8, 1);
s[8] = s[13] = s[18] = s[23];
var uuid = s.join("");
return uuid
}
// 加密
var paramPublicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCvxXa98E1uWXnBzXkS2yHUfnBM6n3PCwLdfIox03T91joBvjtoDqiQ5x3tTOfpHs3LtiqMMEafls6b0YWtgB1dse1W5m+FpeusVkCOkQxB4SZDH6tuerIknnmB/Hsq5wgEkIvO5Pff9biig6AyoAkdWpSek/1/B7zYIepYY0lxKQIDAQAB";
var encrypt = new JSEncrypt;
encrypt.setPublicKey(paramPublicKey);
// 创建一个MD5加密器 --> 标准MD5加密就不扣代码了,直接用库
function MD5(s){
return crypto.createHash("md5").update(s).digest("hex");
}
function fn(d) {
var timestamp = Date.parse(new Date);
var requestId = getUuid();
var data = JSON.stringify(sort_ASCII(dataTojson(d || "{}")));
var final_data = encrypt.encryptLong(data);
var sign = MD5(data + requestId + timestamp);
// xhr.setRequestHeader("timestamp", timestamp);
// xhr.setRequestHeader("requestId", requestId);
// xhr.setRequestHeader("sign", sign)
return {
headers : {
"timestamp" : timestamp+"",
requestId,
data: final_data
},
data
}
}
// 测试
// console.log(fn('page=1&limit=20&taxonid=&startTime=&endTime=&province=%E9%9D%92%E6%B5%B7%E7%9C%81&city=&district=&pointname=&username=&serial_id=&ctime=&taxonname=&state=&mode=0&outside_type=0'))
- 接下来写Python代码
import requests
import execjs
from urllib.parse import urlencode
session = requests.Session()
# 设置头信息
session.headers = {
"accept": "application/json, text/javascript, */*; q=0.01",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"connection": "keep-alive",
"content-length": "172",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"host": "api.birdreport.cn",
"origin": "http://birdreport.cn",
"pragma": "no-cache",
"referer": "http://birdreport.cn/",
"sec-ch-ua": "Not:A-Brand;v=99, Google Chrome;v=145, Chromium;v=145",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "Windows",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"
}
# 预加载js代码
f = open("看鸟.js",mode="r", encoding="utf-8")
js = execjs.compile(f.read())
f.close()
# 数据url
url = "https://api.birdreport.cn/front/activity/search"
# 参数需要进行urlencode才能去加密
data = {
"page": "2",
"limit": "20"
}
params = urlencode(data)
# print(params) # %7B=page%3A+2%2C&limit=20
# 拿加密的数据
ret = js.call("fn", params)
ret_header = ret['headers']
ret_data = ret['data']
# 将加密数据放入请求头中
session.headers.update(ret_header)
# 发请求
resp = session.post(url=url, data=ret_data, verify=False)
print(resp.text)
部分数据展示

8. 解密过程,jQuery的ajax请求可直接找success
9. 找到解密函数
10. 继续复刻,断点进入解密函数,将混淆的参数全部在浏览器控制台跑出结果还原代码
var CryptoJS = require("crypto-js");
function decrypt(_0x291626) {
var _0x3c6fa1 = CryptoJS['enc']['Utf8']['parse']('C8EB5514AF5ADDB94B2207B08C66601C')
, _0x3ec027 = CryptoJS['enc']['Utf8']['parse']('55DD79C6F04E1A67');
return CryptoJS['AES']['decrypt'](_0x291626, _0x3c6fa1, {
'iv': _0x3ec027,
'mode': CryptoJS['mode']['CBC'],
'padding': CryptoJS['pad']['Pkcs7']
})['toString'](CryptoJS['enc']['Utf8']);
}
- Python中得到的密文去进行解密拿数据
mi = resp.json()['data']
result = js.call("decrypt", mi)
print(result)
- 分页操作
# 将原代码中的data字典替换成如下
# 并且data字典代码下面的缩进全部在for循环下即可
for i in range(1, 21):
data = {
"page": i,
"limit": "20"
}
部分数据展示

13. 将数据写入如csv文件
#判断文件夹路径是否存在,如果存在,才可以创建
if not os.path.exists("./观鸟信息合集"):
os.makedirs("./观鸟信息合集")
import json
print(result)
# 打开文件夹
f = open(f"./观鸟信息合集/第{i}页.csv", "a", encoding='utf-8')
# js解密拿到的是json字符串
result_data = json.loads(result)
# 数据分条
for j in result_data:
s = f"{j['address']},{j['endTime']},{j['pointName']},{j['startTime']},{j['userId']},{j['username']}"
# 写入数据
f.write(s)
f.write("\n")
print(f"第{i}页数据写入成功!!!")
f.close()
- 部分数据展示


更多推荐



所有评论(0)