猿人学内部练习平台第54~61题
第54题 无限debugger练习/入门js
本题打开控制台就会自动无限 debugger,解决无限 debugger 的最简单方式就是使用 Firefox 121 版本以上的版本,Firefox 121 以上的版本会对代码内部的 debugger 自动过滤,只有手动打的断点才会生效。
本题是无限 debugger 练习,尝试手动解决该问题。
首先是查看断点位置及调用堆栈,可以看到,这个无限 debugger 其实就是利用了 eval 的特性不断断点:
那么我们 hook eval 函数就可以了:
let _eval = eval;
eval= function(){
if (arguments[0].indexOf('debugger') != -1) {
return _eval('')
}
return _eval.apply(this, arguments)
}
将这段代码输入控制台回车,点击执行按钮,程序就会继续执行了,但是这种方式刷新页面就失效了,可以通过油猴脚本插件实现持久化,也可以使用 v_jstools 插件自带的 hook_eval 功能或注入代码功能。
这个点过了之后,程序又会在下一处无限 debugger,可以看到,有很多个 script 标签,里面有 debugger 命令,这时可以通过替换本地内容的方式:
可以右键点击 54 文件,选择“替换内容”(override content),如果是第一次进行此项操作的话还会让你选择本地的一个文件夹,作为替换的文件夹,允许相关权限即可。然后 54 文件就会被保存到本地,并被本地的该文件替换,接下来就可以把里面的 debugger 部分删掉即可。
删掉后保存并刷新页面,可以看到此处也没有无限 debugger 了。
接着是下一处无限 debugger,查看调用堆栈可知,该处是利用了 appendChild 方法不断添加含 debugger 内容的 script 标签并移除触发的,此时需要 hook appendChild 方法。
通过控制台打印可知,n 是 document,n 的原型是 HTMLDocument ,而 HTMLDocument 经查找没有 appendChild 方法:
那么继续从原型链上查找,最终发现 appendChild 方法是 Node 节点下的:
hook 该方法可以这么写:
let _appendChild = Node.prototype.appendChild
Node.prototype.appendChild = function(){
if (arguments[0].innerHTML && arguments[0].innerHTML.indexOf('debugger') != -1){
arguments[0].innerHTML = ''
}
return _appendChild.apply(this, arguments)
}
该处无限 debugger 过掉后,代码会在如下地方再次停下:
由于是我们已经本地替换的文件内容, 直接把此部分删掉并保存即可。这样该题所有的无限 debugger 就都过掉了。
接下来就是查看接口,逆向参数了,本题需找到参数 token 的由来,全局搜索即可找到:
很简单,是对页码的 base64 编码。
第55题 结果加密跟值
此题请求没有加密,但是返回的响应是加密的,通过调用堆栈很容易找到加密位置:
查看 decode 方法:
可以看到是一个 AES 加密,根据代码推测是一个 ECB 模式的,填充方式为 Pkcs7,密钥为 aiding6666666666 的 AES 加密,可以放到标准 AES 加密里对比:
这是一个网上找的标准 AES 加密,将密码、模式和填充模式选择为对应的之后,输入框输入该题中的加密值,这里我用的第三页的加密值,点击解密:
可以看到,成功解密出了值,说明这是一个未经魔改的 AES 加密,我们本地模拟调用就可以了,对于此题,将 decode 方法复制到本地就可以直接使用。
第56题 经典入门数据加密
该题和上题一样,请求没有加密,响应结果是加密的,通过调用堆栈也很容易找到对应位置,如下:
该文件代码是经过混淆的,不方便查看,可以先解混淆再查看,可以自己通过 AST 解混淆,也可以通过一些插件解混淆,这里使用 v_jstools 插件的解混淆功能:
将文件代码复制到source框中,点击普通解混淆即可,将code框中的解混淆代码复制到本地查看:
可以看到,有一个设置 private key 的操作,ctrl 点击 PVA 查看该变量,如下:
PVA = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAy5R1R2yM5jPPvkO2F47qVqMkYj7o92DF8y1yMkCSxY1WwqG0\ndCdUZTnaoBuAz99wGt55oGLcdalV71nPUiGWs/b6GzVN5v72baz/Q2OxHtkrFKqL\nVX16LW31cW9hAntN84RCbvTeB0MNV+SHmXjIf17OQLCtDKHBZWZ5NKyqFstO+KOd\nu32d2jsw+DT5lOBzDUBk/wUw2KyFJVx7eK6sSXEyWqBk2nxMRDNYixIEN1V1EBSq\nf+OwKK5Mxi04r38+Qog8z03/t/u6CfAOWVmi+MdrD1VHXv/P7bnFlgRcLzKwK1QL\nTSLBE1PrMmNNj0oRjByhMoI9tY5X6mRBqLyDhwIDAQABAoIBAGO++RmGO6D9CNAJ\n4Bm52eKaK5UBiubOIR8NiNLLZb5qinRxg3eX35d7Wb2xzBLNwOFBWSl21trFncfY\n4qY0s+C4ZYHYQ7Om/7nsFeQAYAOj1yJYj01TXf4NTsGGF2t+W8qxZlV0H6dCOLL0\nU2YkUmRp4Le8eQVj6dyTcVaYNPxWQBnb9ZOEIEvEjeoO/DD7CCmt7LDCey9KrTQl\nAvuc2nN6uRV1Wfm0P8conKPJtVdgzMvJujNdpz+bBDqwsqgeCICjs/hSCNO81VH3\nDD7J0mG2OHqowOVqagoDHpBprHOUKxAeTs9I0KEL+hEI4zXCDL69+Xs6azuts733\nzSOmwxkCgYEA25czfPVxxcK685LhaAvwbmzWHqNp07ytRNGf+Aww6OdgWkdgPy0n\n20Gkg0HAqsxGcgZJk6cAkOy5hBLNHpHlGbeWFi+62lVNYUv3hAxumtiPyBMu7avE\nZQCTXND1H1f/2enRDJRxQsR8y/SX1ivmC5U6fx7hbpKxnXyRHnvSlk8CgYEA7VWp\nhLNkn4AEaPPW0TknwKG40At/hjecX2zWAyZVt4ydDSeKgMEOUdmvGGlSCrefAl0n\nPTfM9SdIDcO5OTa2wUayKLIsrb6TDnG6KXXN6z3HR3Q4qKJbG83eaMYDqqziPPV+\nxzRVWShI3EGwkLczASmiYy+sEAT0OkxP59xTKUkCgYBgaGjFkukJfy4fJDxsNtmv\nUX9MYkhjGrIjxbjq6UdL6dGGsVGTSxr1i0NUETkqg5bmFtaUybxY5GWqk6qUok8o\nVE7DnN73Xn4jmnun8OFagHvXxnxTApeuFGueU2tbAIKmxJ3wXPfA7Y0w6kkDUbCl\nIzZUe1VT+3mZgAgijxBsxwKBgQDNytiJ62/V6hBo3P6pPtEcdF6nb0DtpazfBaVw\n572twaywqlermzsKeCIenbx49I1ZZGLQ72C2NpCA9vTWCn5fiyiSpyScp0ImZTDS\nIIckctYoPDug5d7wdgtjeEfXp78osopyuwtCmu7Kpd8vLNt6J5raPI0K+vC22FL1\nLpOhmQKBgQCFeU448fL87N1MjMyusi8wJ5MLcn+kHbLTtpskTpfQM2p3Cnp4oL+7\nBI4AlXlKItV37rJIjZxQgLWhGoTZPplZaW4ooJCFJbazce5ua5fnsFS0oXhDN7uw\njaq+v5t8G6gFS09hEa4kz9O53t/7UGuQqh0Bxb0cJ9iNeAlhagvBDQ==\n-----END RSA PRIVATE KEY-----";
可以看到,这是一个 RSA 加解密的私钥,将密文和私钥放入标准 RSA 中解密,如下:
成功解密得到了我们所需的数据,说明这是一个标准 RSA 加密,本地模拟调用即可,python 模拟 RSA 解密的参考代码:
import rsa
import base64
# 私钥
private_key_data = '''-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAy5R1R2yM5jPPvkO2F47qVqMkYj7o92DF8y1yMkCSxY1WwqG0
dCdUZTnaoBuAz99wGt55oGLcdalV71nPUiGWs/b6GzVN5v72baz/Q2OxHtkrFKqL
VX16LW31cW9hAntN84RCbvTeB0MNV+SHmXjIf17OQLCtDKHBZWZ5NKyqFstO+KOd
u32d2jsw+DT5lOBzDUBk/wUw2KyFJVx7eK6sSXEyWqBk2nxMRDNYixIEN1V1EBSq
f+OwKK5Mxi04r38+Qog8z03/t/u6CfAOWVmi+MdrD1VHXv/P7bnFlgRcLzKwK1QL
TSLBE1PrMmNNj0oRjByhMoI9tY5X6mRBqLyDhwIDAQABAoIBAGO++RmGO6D9CNAJ
4Bm52eKaK5UBiubOIR8NiNLLZb5qinRxg3eX35d7Wb2xzBLNwOFBWSl21trFncfY
4qY0s+C4ZYHYQ7Om/7nsFeQAYAOj1yJYj01TXf4NTsGGF2t+W8qxZlV0H6dCOLL0
U2YkUmRp4Le8eQVj6dyTcVaYNPxWQBnb9ZOEIEvEjeoO/DD7CCmt7LDCey9KrTQl
Avuc2nN6uRV1Wfm0P8conKPJtVdgzMvJujNdpz+bBDqwsqgeCICjs/hSCNO81VH3
DD7J0mG2OHqowOVqagoDHpBprHOUKxAeTs9I0KEL+hEI4zXCDL69+Xs6azuts733
zSOmwxkCgYEA25czfPVxxcK685LhaAvwbmzWHqNp07ytRNGf+Aww6OdgWkdgPy0n
20Gkg0HAqsxGcgZJk6cAkOy5hBLNHpHlGbeWFi+62lVNYUv3hAxumtiPyBMu7avE
ZQCTXND1H1f/2enRDJRxQsR8y/SX1ivmC5U6fx7hbpKxnXyRHnvSlk8CgYEA7VWp
hLNkn4AEaPPW0TknwKG40At/hjecX2zWAyZVt4ydDSeKgMEOUdmvGGlSCrefAl0n
PTfM9SdIDcO5OTa2wUayKLIsrb6TDnG6KXXN6z3HR3Q4qKJbG83eaMYDqqziPPV+
xzRVWShI3EGwkLczASmiYy+sEAT0OkxP59xTKUkCgYBgaGjFkukJfy4fJDxsNtmv
UX9MYkhjGrIjxbjq6UdL6dGGsVGTSxr1i0NUETkqg5bmFtaUybxY5GWqk6qUok8o
VE7DnN73Xn4jmnun8OFagHvXxnxTApeuFGueU2tbAIKmxJ3wXPfA7Y0w6kkDUbCl
IzZUe1VT+3mZgAgijxBsxwKBgQDNytiJ62/V6hBo3P6pPtEcdF6nb0DtpazfBaVw
572twaywqlermzsKeCIenbx49I1ZZGLQ72C2NpCA9vTWCn5fiyiSpyScp0ImZTDS
IIckctYoPDug5d7wdgtjeEfXp78osopyuwtCmu7Kpd8vLNt6J5raPI0K+vC22FL1
LpOhmQKBgQCFeU448fL87N1MjMyusi8wJ5MLcn+kHbLTtpskTpfQM2p3Cnp4oL+7
BI4AlXlKItV37rJIjZxQgLWhGoTZPplZaW4ooJCFJbazce5ua5fnsFS0oXhDN7uw
jaq+v5t8G6gFS09hEa4kz9O53t/7UGuQqh0Bxb0cJ9iNeAlhagvBDQ==
-----END RSA PRIVATE KEY-----'''
# Base64编码的密文
base64_ciphertext = 'gYBBFZr5uSZ1KtSDNjfnwdFgJ99bZvu6fALrExo/L1ceUQiSHmVkL4HmV60vO90D80AzeoOgBVFCwH3cm+a23s45WlACTwhoAAAjZ6N6dH+pLQDOKYkIN45eZW6goCR8drDEIMLVyJL0hoTe/jB79IsAxY3xv+3cgJTg78j9liSorSXJlKqlWMwzSfdK6HRagJhJGq9QHJZJndJmvuZ6vzJf+6Nfvu7qgzdfzvHX+2u+KudrD/sTzKnEIajU6jZnROupAgb9agDYp2wANL9ORpMM9WCgzEG225XQ+cNPQJHutJoAIjBvc/WBSHs0As718OYpPQrLWaGeIVIn1sjU7w=='
# 解析私钥
private_key = rsa.PrivateKey.load_pkcs1(private_key_data.encode())
# 解码Base64编码的密文
ciphertext = base64.b64decode(base64_ciphertext)
# 使用私钥解密密文
plaintext = rsa.decrypt(ciphertext, private_key)
print("解密后的明文:", plaintext.decode('utf-8'))
# 输出:
# 解密后的明文: {"status": "1", "state": "success", "data": [{"value": "5030"}, {"value": "9161"}, {"value": "6942"}, {"value": "6932"}, {"value": "3421"}, {"value": "3035"}, {"value": "8875"}, {"value": "5787"}, {"value": "2007"}, {"value": "3938"}]}
第57题 返回数据加密第三弹
此题仍是一个返回数据加密,根据调用堆栈可以找到加密位置:
进入 I 函数查看:
可以看到,这是一个 AES 加密,不过根据调试,这不是一个标准 AES 加密,因为传入的 key 是 8 字节的,而标准 AES 加密的密钥必须是 16,24 或 32 字节的,此时只能扣代码或者补环境了。
可以注意到,这部分代码是一个 webpack 打包后的,将 script 标签内的代码复制到本地,然后修改 0x2: [function(P, n, o) { 后面的部分内容,如下:
删除 ajax 部分的代码,并在上面添加了测试输出代码,运行后结果如下:
可以看到,成功打印了对应内容, 这样就可以通过暴露全局变量的方式,将次结果保存下来,在文件首行添加如下内容:
global.result;
global.X = '__global_X';
global.l = '__global_l';
然后修改刚刚测试的地方如下:
最后在文件末尾打印:
console.log(global.result)
此时就可以在 python 中调用了:
import subprocess
import os
webpack57js = open('57webpack.js', 'r', encoding='utf-8')
jscode = webpack57js.read().replace('__global_X', 'BGsHfKJP').replace('__global_l', '7VR537hkBRLAfu0WRFny6y7U4IKEu/mSYqntvsauubCNoS4lKZ2zf8xXU5TOG1AHJfa0qaj8Ec2dPQRq3vFpgvPl9SrmAukLJcpsWBQ52WSsk3ZgqKMGLpdGrJZRKrM4Wnyb/Ub2kerUc7dDCNQpKORE/97ajxUTxA+UvlVsHUMv32DeR8PuHYspnVMF7IPpCF6vn91yjWUl9rSpqPwRzcaR1kqmkzbD8+X1KuYC6QsqjgSCWwVxCgZyCsaBG64el0asllEqszgyOr9NX7uLEdRzt0MI1CkoYdygg//70suPa+FJHQ5eMiTZT0oS92dhV0mt/UUQvSTs37RD3Vo4EA==')
with open('57_.js', 'w', encoding='utf8') as f:
f.write(jscode)
result = subprocess.check_output(['node', '57_.js'])
print(result.decode())
os.remove('57_.js')
# 运行结果:
# {"status": "1", "state": "success", "data": [{"value": "3495\r"}, {"value": "7529\r"}, {"value": "8960\r"}, {"value": "238\r"}, {"value": "3033\r"}, {"value": "9569\r"}, {"value": "2520\r"}, {"value": "4727\r"}, {"value": "6179\r"}, {"value": "3153\r"}]}
第58题 勇敢牛牛不怕困难
查看请求可知,需获取 token,很容易找到 token 的来源:
可以看到,是一个 md5 加密,传入的参数是页码,经对比发现,这是一个标准 md5 加密,本地模拟即可:
import hashlib
page = 3
result = hashlib.md5(str(page).encode()).hexdigest()
print(result)
print(result[8:24])
# eccbc87e4b5ce2fe28308fd9f2a7baf3
# 4b5ce2fe28308fd9
第59题 脏数据
此题无任何加密,但是直接请求求和提交会显示错误,查看响应成功后相关处理的部分源码如下:
当 r===0x33 时,即页码为 51 页时,执行 i['data'][0x0]['value'] = '5734\x0d',也就是将 51 页的第一个值改为 5734,跳转至 51 页可以看到,第一个值确实是 5734:
但是我们请求的接口返回的第一值却是 5733:
所以总和是少了1
第60题 轻混url加密
该题是 url 加密,根据调用堆栈可以找到加密位置:
L 方法就是加密的方法,传入的参数是页码,可以看到,这是一个 AES 加密,但不是一个标准的 AES 加密,因为其密钥是 aiding88,只有 8 字节,而标准 AES 密钥需为 16,24或32字节,可以注意到这部分代码其实是在一个 script 标签中,而这个标签中的代码很明显是一个 webpack 打包,这里的处理方法类似 57 题,将 script 标签中的代码复制到本地,修改加密部分的代码如下做测试:
这里将 ajax 部分的代码删掉,添加图中打印,运行,结果如下:
将 page 改为2,再次运行测试,结果如下:
对比题目的接口,可以发现,结果不一样:
说明代码存在环境检测,这里我们可以先搜索 try 关键字,因为这里是改变程序流程的一个点,搜索后发现如下环境检测点:
将这几个 try catch 语句删掉,再次运行,结果就与题目接口中展示的一致了。然后将页码暴露全局使用即可。
第61题 千山鸟飞绝
本题打开开发者调试工具后会有无限 debugger,出现原因和解决方法与 54 题一样,不再赘述;
然后就是常规的找接口了,点击 Fetch/XHR 选项,点击页面的其他页,发现页面数据可以正常加载,但是选项下没有任何接口出现;
点击全部(all),刷新页面,搜索 61 看看是否有可疑接口,找到如下疑似接口:
点击查看,再点击页面其他页,可以看到,有对应数据出现了,说明这是一个 websocket 连接发送的请求:
通过此请求的调用堆栈可以找到对应的数据加密位置:
可以看到,I 就是 websocket 对象
I['\x73\x65\x6e\x64'](X(k['\x74\x6f\x53\x74' + '\x72\x69\x6e\x67']() + ('\x7c\x70\x79\x74' + '\x68\x6f\x6e\x2d' + '\x73\x70\x69\x64' + '\x65\x72\x2e\x63' + '\x6f\x6d\x7c\x79' + '\x75\x61\x6e\x72' + '\x65\x6e\x78\x75' + '\x65\x2e\x63\x6f' + '\x6d\x7c\u5927\u5a01' + '\u5929\u9f99\uff0c\u5927' + '\u7f57\u6cd5\u5492')))
这一行,经控制台打印,其实就是
I.send(X(k["toString"]() + "|python-spider.com|yuanrenxue.com|大威天龙,大罗法咒"));
k["toString"]() 就是页码,所以 websocket 对象发送的数据是字符串 页码|python-spider.com|yuanrenxue.com|大威天龙,大罗法咒
经 X 函数加密后得到的内容。
X 函数就在上方,经解混淆后如下:
const C = "aiding1234567891";
CryptoJS = J("crypto-js");
function X(k, n = C) {
const O = CryptoJS.AES["encrypt"](k, CryptoJS.enc.Utf8["parse"](n), {
"mode": CryptoJS.mode.ECB,
"padding": CryptoJS.pad["Pkcs7"]
});
return O["toString"]();
}
可以看到,这是一个 AES 加密,密钥是 aiding1234567891,模式是 ECB,填充方式为 Pkcs7, 不过遗憾的是,经调试比对,这不是一个标准 AES 加密,所以只能想办法处理了;
整个代码是在一个 script 标签中的,观察代码结构可以发现,这也是一个 webpack 打包,将这部分代码复制带本地(不要格式化),通过搜索定位位置,然后在函数 X 下方加一些打印信息:
运行文件,报错,提示 $ 不存在,把对应的代码删掉,共删两处,继续运行,得到如下结果:
对比题目中的数据:
可以看到,结果不一致,说明代码可能存在环境检测,首先搜下 try 关键字吧,找到如下检测点:
将这几个 try catch 语句删掉,再次运行,结果如下:
这样就与题目中的调试结果一致了。接下来就是 python 调用了,可以修改 js 中的打印如下:
let result=X('__page__|python-spider.com|yuanrenxue.com|大威天龙,大罗法咒');console.log(result);process.exit();
python 中用如下方式获取结果:
import subprocess
import os
with open('webpack61.js', 'r', encoding='utf-8') as f:
webpack61_js_code = f.read()
page = 1
with open('run.js', 'w', encoding='utf-8') as f:
f.write(webpack61_js_code.replace('__page__', str(page)))
send_data = subprocess.check_output(['node', 'run.js'])
print(send_data.decode())
os.remove('run.js')
# 输出 BpcFZapyYcAmt1JP12G43QzJgYizE+QhFhAtT9PGLMBSd+hkZJSVOOZZFKyAA5He3zhxZSxd3AdIrHEMj2j9oA==,与题目调试结果一致
再接着就是模拟 websocket 请求了,可以使用 curl_cffi 库:https://github.com/yifeikong/curl_cffi
其官方 websocket 示例如下:
from curl_cffi.requests import Session, WebSocket
def on_message(ws: WebSocket, message):
print(message)
with Session() as s:
ws = s.ws_connect(
"wss://api.gemini.com/v1/marketdata/BTCUSD",
on_message=on_message,
)
ws.run_forever()
官方文档相关内容如下:
据此此题可以这么写:
import json
import time
import subprocess
from curl_cffi.requests import Session, WebSocket
import os
def on_message(ws: WebSocket, message):
print(message)
def get_data(send_data):
with Session() as s:
ws = s.ws_connect("wss://www.python-spider.com/api/challenge61", on_message=on_message)
ws.send(send_data)
a = ws.recv()
return a
with open('webpack61.js', 'r', encoding='utf-8') as f:
topic61_js_code = f.read()
total_num = 0
page = 1
while page < 101:
with open('run.js', 'w', encoding='utf-8') as f:
f.write(topic61_js_code.replace('__page__', str(page)))
print('第 {} 页'.format(page))
send_data = subprocess.check_output(['node', 'run.js'])
datas = get_data(send_data.decode().replace('\n', '').encode())
result = json.loads(datas[0].decode())
print(result)
data_list = result['data']
for data in data_list:
total_num += int(data['value'])
page += 1
os.remove('run.js')
print(total_num)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】