一次js逆向实践

本文最后更新于:2024年6月20日 晚上

最近有个朋友拜托我帮忙刷一门课程,但是那个课程没有油猴脚本可以用,虽说可以手点,不用等待视频播放什么的,但还是很繁琐。所以决定通过找到接口的方式,直接使用爬虫取代手动点击方式。

注:写此篇文章是为了记录实践过程,并未对网站进行违法操作,文章已经隐去和此网站有关的信息。

0x01 逆向过程

课程是有很多个大章节,大章节里面有很多小章节,小章节有几个小节点构成的,任务就是点击小节点完成任务,任务完成方式通过点击小点,等待一会,未完成状态会变成已完成状态。

通过点击多个不同的任务测试,发现调用了一个 add 接口方法,标记次节点为已完成。通过 F12 拿到了此接口的请求头与请求参数。

请求头:其中,Authorization 是当前登录用户的令牌,可以在短期内使用,目前来说够用。Sign 比较特殊,后面会提到。Cookie 直接在 F12 控制台复制,目前测试 Cookie 可以反复使用。Referer 是当前从哪个地址访问过来的,以防万一,带上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Authorization": "Bearer xxxx"
"Cache-Control": "no-cache",
"Content-Length": "85",
"Content-Type": "application/json;charset=UTF-8",
"Cookie": "xxxx",
"Origin": "xxxxx",
"Pragma": "no-cache",
"Priority": "u=1, i",
"Referer": "xxxx",
"Sign": "24808a85cc2af714bb670f43ad00a68c",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
}

请求参数:viewStatus 标记此任务标记为完成状态,catalogueId 是此节点的唯一 ID,randomNum 貌似是一个随机数,

1
2
3
4
5
data = {
"viewStatus": "3",
"catalogueId": "1655388287586213889",
"randomNum": 0.608132423106152
}

下面是测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
data = {
"viewStatus": "3",
"catalogueId": "1655388287586213889",
"randomNum": 0.608132423106152
}

headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Authorization": "Bearer xxxx"
"Cache-Control": "no-cache",
"Content-Length": "85",
"Content-Type": "application/json;charset=UTF-8",
"Cookie": "xxxx",
"Origin": "xxxxx",
"Pragma": "no-cache",
"Priority": "u=1, i",
"Referer": "xxxx",
"Sign": "24808a85cc2af714bb670f43ad00a68c",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
}
# 防止发送频繁
time.sleep(5)

response = requests.post("https://xxxxx/xxx/xxx/learningRecords/add", headers=headers, json=data, verify=False)

发送几次后,首次得到正确的响应,后面几次都是响应都是下面的。

1
{"msg": "请勿频繁调用此接口", "code": 400}

猜测 random 参数有问题,名字已经很明显的告诉我,这是个随机数,那肯定不是每次请求都是一样的。打开 F12 ,在源代码/来源那一块,使用 Ctrl + Shift + F 搜索关键字 random ,有很多处使用到,使用打断点的方式验证是否会执行到搜索到的位置,找到如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function o(q) {
Dt(q.attaUrl) === "word" && h.value && h.value.id && setTimeout(()=>{
const j = Math.random()
, J = `catalogueId=${q.id}&viewStatus=3&randomNum=${j}`;
v({
catalogueId: q.id,
randomNum: j
}, {
sign: ue.hashStr(J)
}).then(()=>{
we(q, {
viewStatus: "3"
}),
Ge()
}
)
}
, 2e3)
}

得到 random 的生成方式,使用 Python 代码替换 Javascript 的生成方式。

1
2
3
4
5
data = {
"viewStatus": "3",
"catalogueId": id,
"randomNum": random.random()
}

再次发送请求,发现还是会出现 400 的问题,通过多次页面点击节点,发现 sign 每次请求都不一样,确定 sign 也不是固定值。根据上面的 Javascript 代码,发现 ua.hashStr 是生成 sign 的关键,首先确定了这个方法的参数是通过 catalogueId=${q.id}&viewStatus=3&randomNum=${j} 生成的。通过全局搜索 ue.hashStr 找到这个方法的定义位置。

把找到的代码写入一个 encrypt.js 中 ,从 hashStr 方法入手,找到定义的位置,下面就是定义的位置,剔除不需要的代码,通过调试的方式一点一点补充缺失的代码,最终是下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
class ue {
constructor() {
this._dataLength = 0,
this._bufferLength = 0,
this._state = new Int32Array(4),
this._buffer = new ArrayBuffer(68),
this._buffer8 = new Uint8Array(this._buffer,0,68),
this._buffer32 = new Uint32Array(this._buffer,0,17),
this.start()
}
static hashStr(C, _=!1) {
return this.onePassHasher.start().appendStr(C).end(_)
}

start() {
return this._dataLength = 0,
this._bufferLength = 0,
this._state.set(ue.stateIdentity),
this
}

appendStr(C) {
const _ = this._buffer8
, p = this._buffer32;
let g = this._bufferLength, h, v;
for (v = 0; v < C.length; v += 1) {
if (h = C.charCodeAt(v),
h < 128)
_[g++] = h;
else if (h < 2048)
_[g++] = (h >>> 6) + 192,
_[g++] = h & 63 | 128;
else if (h < 55296 || h > 56319)
_[g++] = (h >>> 12) + 224,
_[g++] = h >>> 6 & 63 | 128,
_[g++] = h & 63 | 128;
else {
if (h = (h - 55296) * 1024 + (C.charCodeAt(++v) - 56320) + 65536,
h > 1114111)
console.log("error");
_[g++] = (h >>> 18) + 240,
_[g++] = h >>> 12 & 63 | 128,
_[g++] = h >>> 6 & 63 | 128,
_[g++] = h & 63 | 128
}
g >= 64 && (this._dataLength += 64,
ue._md5cycle(this._state, p),
g -= 64,
p[0] = p[16])
}
return this._bufferLength = g,
this
}

end(C=!1) {
const _ = this._bufferLength
, p = this._buffer8
, g = this._buffer32
, h = (_ >> 2) + 1;
this._dataLength += _;
const v = this._dataLength * 8;
if (p[_] = 128,
p[_ + 1] = p[_ + 2] = p[_ + 3] = 0,
g.set(ue.buffer32Identity.subarray(h), h),
_ > 55 && (ue._md5cycle(this._state, g),
g.set(ue.buffer32Identity)),
v <= 4294967295)
g[14] = v;
else {
const V = v.toString(16).match(/(.*?)(.{0,8})$/);
if (V === null)
return;
const ie = parseInt(V[2], 16)
, we = parseInt(V[1], 16) || 0;
g[14] = ie,
g[15] = we
}
return ue._md5cycle(this._state, g),
C ? this._state : ue._hex(this._state)
}
static _md5cycle(C, _) {
let p = C[0]
, g = C[1]
, h = C[2]
, v = C[3];
p += (g & h | ~g & v) + _[0] - 680876936 | 0,
p = (p << 7 | p >>> 25) + g | 0,
v += (p & g | ~p & h) + _[1] - 389564586 | 0,
v = (v << 12 | v >>> 20) + p | 0,
h += (v & p | ~v & g) + _[2] + 606105819 | 0,
h = (h << 17 | h >>> 15) + v | 0,
g += (h & v | ~h & p) + _[3] - 1044525330 | 0,
g = (g << 22 | g >>> 10) + h | 0,
p += (g & h | ~g & v) + _[4] - 176418897 | 0,
p = (p << 7 | p >>> 25) + g | 0,
v += (p & g | ~p & h) + _[5] + 1200080426 | 0,
v = (v << 12 | v >>> 20) + p | 0,
h += (v & p | ~v & g) + _[6] - 1473231341 | 0,
h = (h << 17 | h >>> 15) + v | 0,
g += (h & v | ~h & p) + _[7] - 45705983 | 0,
g = (g << 22 | g >>> 10) + h | 0,
p += (g & h | ~g & v) + _[8] + 1770035416 | 0,
p = (p << 7 | p >>> 25) + g | 0,
v += (p & g | ~p & h) + _[9] - 1958414417 | 0,
v = (v << 12 | v >>> 20) + p | 0,
h += (v & p | ~v & g) + _[10] - 42063 | 0,
h = (h << 17 | h >>> 15) + v | 0,
g += (h & v | ~h & p) + _[11] - 1990404162 | 0,
g = (g << 22 | g >>> 10) + h | 0,
p += (g & h | ~g & v) + _[12] + 1804603682 | 0,
p = (p << 7 | p >>> 25) + g | 0,
v += (p & g | ~p & h) + _[13] - 40341101 | 0,
v = (v << 12 | v >>> 20) + p | 0,
h += (v & p | ~v & g) + _[14] - 1502002290 | 0,
h = (h << 17 | h >>> 15) + v | 0,
g += (h & v | ~h & p) + _[15] + 1236535329 | 0,
g = (g << 22 | g >>> 10) + h | 0,
p += (g & v | h & ~v) + _[1] - 165796510 | 0,
p = (p << 5 | p >>> 27) + g | 0,
v += (p & h | g & ~h) + _[6] - 1069501632 | 0,
v = (v << 9 | v >>> 23) + p | 0,
h += (v & g | p & ~g) + _[11] + 643717713 | 0,
h = (h << 14 | h >>> 18) + v | 0,
g += (h & p | v & ~p) + _[0] - 373897302 | 0,
g = (g << 20 | g >>> 12) + h | 0,
p += (g & v | h & ~v) + _[5] - 701558691 | 0,
p = (p << 5 | p >>> 27) + g | 0,
v += (p & h | g & ~h) + _[10] + 38016083 | 0,
v = (v << 9 | v >>> 23) + p | 0,
h += (v & g | p & ~g) + _[15] - 660478335 | 0,
h = (h << 14 | h >>> 18) + v | 0,
g += (h & p | v & ~p) + _[4] - 405537848 | 0,
g = (g << 20 | g >>> 12) + h | 0,
p += (g & v | h & ~v) + _[9] + 568446438 | 0,
p = (p << 5 | p >>> 27) + g | 0,
v += (p & h | g & ~h) + _[14] - 1019803690 | 0,
v = (v << 9 | v >>> 23) + p | 0,
h += (v & g | p & ~g) + _[3] - 187363961 | 0,
h = (h << 14 | h >>> 18) + v | 0,
g += (h & p | v & ~p) + _[8] + 1163531501 | 0,
g = (g << 20 | g >>> 12) + h | 0,
p += (g & v | h & ~v) + _[13] - 1444681467 | 0,
p = (p << 5 | p >>> 27) + g | 0,
v += (p & h | g & ~h) + _[2] - 51403784 | 0,
v = (v << 9 | v >>> 23) + p | 0,
h += (v & g | p & ~g) + _[7] + 1735328473 | 0,
h = (h << 14 | h >>> 18) + v | 0,
g += (h & p | v & ~p) + _[12] - 1926607734 | 0,
g = (g << 20 | g >>> 12) + h | 0,
p += (g ^ h ^ v) + _[5] - 378558 | 0,
p = (p << 4 | p >>> 28) + g | 0,
v += (p ^ g ^ h) + _[8] - 2022574463 | 0,
v = (v << 11 | v >>> 21) + p | 0,
h += (v ^ p ^ g) + _[11] + 1839030562 | 0,
h = (h << 16 | h >>> 16) + v | 0,
g += (h ^ v ^ p) + _[14] - 35309556 | 0,
g = (g << 23 | g >>> 9) + h | 0,
p += (g ^ h ^ v) + _[1] - 1530992060 | 0,
p = (p << 4 | p >>> 28) + g | 0,
v += (p ^ g ^ h) + _[4] + 1272893353 | 0,
v = (v << 11 | v >>> 21) + p | 0,
h += (v ^ p ^ g) + _[7] - 155497632 | 0,
h = (h << 16 | h >>> 16) + v | 0,
g += (h ^ v ^ p) + _[10] - 1094730640 | 0,
g = (g << 23 | g >>> 9) + h | 0,
p += (g ^ h ^ v) + _[13] + 681279174 | 0,
p = (p << 4 | p >>> 28) + g | 0,
v += (p ^ g ^ h) + _[0] - 358537222 | 0,
v = (v << 11 | v >>> 21) + p | 0,
h += (v ^ p ^ g) + _[3] - 722521979 | 0,
h = (h << 16 | h >>> 16) + v | 0,
g += (h ^ v ^ p) + _[6] + 76029189 | 0,
g = (g << 23 | g >>> 9) + h | 0,
p += (g ^ h ^ v) + _[9] - 640364487 | 0,
p = (p << 4 | p >>> 28) + g | 0,
v += (p ^ g ^ h) + _[12] - 421815835 | 0,
v = (v << 11 | v >>> 21) + p | 0,
h += (v ^ p ^ g) + _[15] + 530742520 | 0,
h = (h << 16 | h >>> 16) + v | 0,
g += (h ^ v ^ p) + _[2] - 995338651 | 0,
g = (g << 23 | g >>> 9) + h | 0,
p += (h ^ (g | ~v)) + _[0] - 198630844 | 0,
p = (p << 6 | p >>> 26) + g | 0,
v += (g ^ (p | ~h)) + _[7] + 1126891415 | 0,
v = (v << 10 | v >>> 22) + p | 0,
h += (p ^ (v | ~g)) + _[14] - 1416354905 | 0,
h = (h << 15 | h >>> 17) + v | 0,
g += (v ^ (h | ~p)) + _[5] - 57434055 | 0,
g = (g << 21 | g >>> 11) + h | 0,
p += (h ^ (g | ~v)) + _[12] + 1700485571 | 0,
p = (p << 6 | p >>> 26) + g | 0,
v += (g ^ (p | ~h)) + _[3] - 1894986606 | 0,
v = (v << 10 | v >>> 22) + p | 0,
h += (p ^ (v | ~g)) + _[10] - 1051523 | 0,
h = (h << 15 | h >>> 17) + v | 0,
g += (v ^ (h | ~p)) + _[1] - 2054922799 | 0,
g = (g << 21 | g >>> 11) + h | 0,
p += (h ^ (g | ~v)) + _[8] + 1873313359 | 0,
p = (p << 6 | p >>> 26) + g | 0,
v += (g ^ (p | ~h)) + _[15] - 30611744 | 0,
v = (v << 10 | v >>> 22) + p | 0,
h += (p ^ (v | ~g)) + _[6] - 1560198380 | 0,
h = (h << 15 | h >>> 17) + v | 0,
g += (v ^ (h | ~p)) + _[13] + 1309151649 | 0,
g = (g << 21 | g >>> 11) + h | 0,
p += (h ^ (g | ~v)) + _[4] - 145523070 | 0,
p = (p << 6 | p >>> 26) + g | 0,
v += (g ^ (p | ~h)) + _[11] - 1120210379 | 0,
v = (v << 10 | v >>> 22) + p | 0,
h += (p ^ (v | ~g)) + _[2] + 718787259 | 0,
h = (h << 15 | h >>> 17) + v | 0,
g += (v ^ (h | ~p)) + _[9] - 343485551 | 0,
g = (g << 21 | g >>> 11) + h | 0,
C[0] = p + C[0] | 0,
C[1] = g + C[1] | 0,
C[2] = h + C[2] | 0,
C[3] = v + C[3] | 0
}
static _hex(C) {
const _ = ue.hexChars
, p = ue.hexOut;
let g, h, v, V;
for (V = 0; V < 4; V += 1)
for (h = V * 8,
g = C[V],
v = 0; v < 8; v += 2)
p[h + 1 + v] = _.charAt(g & 15),
g >>>= 4,
p[h + 0 + v] = _.charAt(g & 15),
g >>>= 4;
return p.join("")
}
};

ue.stateIdentity = new Int32Array([1732584193, -271733879, -1732584194, 271733878]);
ue.buffer32Identity = new Int32Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
ue.hexChars = "0123456789abcdef";
ue.hexOut = [];
ue.onePassHasher = new ue;


// 自己写的方法,用于下面调用
function getHash(str) {
return ue.hashStr(str, false);
}

现在需要做的是通过 Python 执行 Javascript 代码,通过搜索找到一个库 execjs

下面是具体代码,通过 complie 编译 Javascript 代码,然后使用 call 调用任意方法。

1
2
3
4
5
6
ctx = execjs.compile("""
js code
""")


ctx.call("getHash", "param")

0x02 完整代码

下面是完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import requests
import json
import time
import random
import execjs


# 读取JSON文件并解析为字典
with open('data.json', 'r', encoding='utf-8') as file:
data = json.load(file)


# 使用列表推导式收集所有的id
all_ids = [
resource['id']
for item in data['data']
if 'children' in item
for child in item['children']
if 'resourceList' in child
for resource in child['resourceList']
]

ctx = execjs.complie("""
js code
""")

for id in all_ids:
data = {
"viewStatus": "3",
"catalogueId": id,
"randomNum": random.random()
}

string = "catalogueId={0}&viewStatus=3&randomNum={1}".format(data["catalogueId"], data["randomNum"])


headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Authorization": "Bearer xxxx"
"Cache-Control": "no-cache",
"Content-Length": "85",
"Content-Type": "application/json;charset=UTF-8",
"Cookie": "xxxx",
"Origin": "xxxxx",
"Pragma": "no-cache",
"Priority": "u=1, i",
"Sign": ctx.call("getHash", string),
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
}

time.sleep(random.randint(10, 20))

response = requests.post("https://xxxx/xxxx/xxxx/learningRecords/add", headers=headers, json=data, verify=False)
print("current Id: ", id)
print(response.status_code)
print(response.json())

0x03 后记

经过发现其实 Sign 参数就是 md5 生成的,其实根本不需要用到 execjs 库去生成 Sign 值。可以直接用 pythonhashlib 生成 md5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import hashlib

# 请求参数
data = {
"viewStatus": "3",
"catalogueId": id,
"randomNum": random.random()
}

m = hashlib.md5()

string = "catalogueId={0}&viewStatus=3&randomNum={1}".format(data["catalogueId"], data["randomNum"])


m.update(string.encode())

headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Authorization": "Bearer xxxx"
"Cache-Control": "no-cache",
"Content-Length": "85",
"Content-Type": "application/json;charset=UTF-8",
"Cookie": "xxxx",
"Origin": "xxxxx",
"Pragma": "no-cache",
"Priority": "u=1, i",
"Sign": m.hexdigest(),
"Referer": "xxx",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
}

这个网站的加密没有那么复杂,在逆向的过程中也学到了一种防止接口重刷的方法,还是比较有意思的。


一次js逆向实践
http://aim467.github.io/2024/06/04/一次js逆向实践/
作者
Dedsec2z
发布于
2024年6月4日
更新于
2024年6月20日
许可协议