猿人学内部练习平台第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)
posted @ 2024-06-07 00:19  脱下长日的假面  阅读(75)  评论(0编辑  收藏  举报