提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!
一、websocket逆向
- 相关的知识可以看看k哥爬虫 上的文章,我就是从这里看的,【JS 逆向百例】WebSocket 协议爬虫,智慧树扫码登录案例分析
- 因为websocket通过实时交互获取信息,所以需要关注的地方是寻找发送 和接收函数,可以通过hook来定位页面中发送和接收的位置,然后观察交互规律即可
- hook代码,主要是 send 和 onmessage , 但是 onmessage 是一个接收返回数据时触发的事件,页面会绑定它指定一个回调函数来获取事件数据,所以只能 hook send ,hook 到后可以根据 websocket对象 找到 onmessage
javascript
// 保存原始的WebSocket.prototype.send方法
let sendCache = WebSocket.prototype.send;
// 重写WebSocket.prototype.send方法
WebSocket.prototype.send = function (data) {
console.info("Hook WebSocket send => ", data);
debugger;
return sendCache.apply(this, arguments);
};
二、案例地址
aHR0cHM6Ly9kYi5zeGF1LmVkdS5jbi90emdnLmh0bQ==
三、分析流程
-
查找标题,打开f12 控制台,但是进入了debugger ,追栈发现是通过构造函数执行debugger,但是去除debugger后查找不到标题。
javascript0?1:function(){return !0}["constructor"]("debugger")["call"]["action"]
javascript// hook构造函数去掉debugger (function () { let constructorCache = Function.prototype.constructor; Function.prototype.constructor = function (string) { if (string === "debugger") { // console.log("Hook constructor debugger!"); return function () {}; } return constructorCache(string); }; })();
-
查看浏览器发送的请求,有一个响应内容加密的session链接,但是不太像,通过翻页发现有一个 wss 链接,保持会话进行传输数据,但是传输数据的内容是二进制,翻页的话,这里也会有变化,所以定位这里应该就是加密的位置
-
分析wss请求,有三个参数会一直发生变化
-
这三个参数来自 session 请求中的 set-cookie ,所以需要先解决 session 请求,session请求中需要扣 1. headers 参数中的 etag 2. 请求参数中的 data
四、逆向参数
-
追栈,在 e.sessionData 那点进去,打上断点,然后调试就到了data生成的位置
-
可以发现是调用了 E 函数, 观察 E 函数,它是包含了许多key, value,; key是函数名, value是函数体,s.default里进行的定义;
-
追栈发现调用 E 的上一栈来自 n(755) , 通过加载器的方式抠出来,发现会卡住,不知道什么原因,所以E 拿出来单独扣
-
E是一个大的加密函数, 抠出来后 补充下 s.default , 缺啥补啥, 补完后调用发现执行是空,页面上完成这一步去调用也是空,所以应该是还需要经历一些步骤
-
全部抠出来后进行调试(生成data那里的一堆加密函数返回的值就是 getKey),发现报错了
-
因为 p() 函数里的数组进行了移位,所以把移位的函数找到即可,并且这个操作要放在 E 函数执行的上方
-
然后又补充了几个变量的值
-
报错 u 找不到, 找到位置,发现是个加载器进来的函数,这个通过扣webpack的方式扣就可以
-
从加载器开始的那个函数那打断点,因为好多东西是一开始加载进去的
-
然后进到加载器后,在hook加载器, 然后再 u 结束的位置打上断点
-
拿出来后 替换下 ' == ' , \ = \ , 放在加载器中就可以调用了
var i = n(548); var u = i(n(235));
-
再根据报错补充几个变量值就可以成功调用了
javascriptE['getKey']() window.etag = E['genrandomString']()
-
etag 参数扣取, etag就是上面的 genrandomString 生成的, 就是 E 函数中的 iv值
-
tabId 生成,加密的参数中还有个 tabId, 这个直接搜索就可以搜索到, 是每次随机生成的, 并且这个 tabId 也是 wss链接中的参数,也是 wss 向服务器发送信息中需要携带的参数
五、webSocket 交互位置
-
wss链接组成参数
-
服务器客户端信息交互的位置,通过文章开头的 hook函数可以定位到 send 的位置,定位到两个位置,一个是定期发送信息保持交互的 send , 一个就是要获取相关信息时发送时的 send , 需要关注第二个
-
然后打开网页,查看日志,发现接收的数据中能找到标题,但是找不到链接
-
经过研究发现,需要点击 标题后,客户端会发送 一系列的信息,然后才能获取链接 ,然后会跳转到对应的链接 页面
-
tabid 前面已经可以拿到了, 标题id,在加密生成wss的时候已经将 当前页面的 链接传入了, 在绑定wss链接与服务器交互的时候,会接收到服务器发送的信息,这个时候标题以及对应的标题id都会拿到
-
然后将上面的发送信息里面的 tabid 和 标题id 替换,发送就可以拿到链接了,最后发现只需要发送 mousedown 和 click 两个事件就可以。
-
代码
python
import time
import _thread
import requests
import websocket
import random
import subprocess
from functools import partial
subprocess.Popen = partial(subprocess.Popen, encoding="utf-8")
import execjs
import json
class WebSocketClinet():
def __init__(self):
self.count = 0
self.tabId = ""
self.web_socket_url = ""
# 读取解密js代码
with open("test4.js", encoding="utf8") as f:
self.jsCode = f.read()
def get_wss_first(self):
url = 'https://**'
js_resul = execjs.compile(self.jsCode).call('js_sdk', "encrypt", url)
headers = {
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "keep-alive",
"Content-Type": "application/json; charset=UTF-8",
"etag": js_resul['etag'],
"Fetch-Mode": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
"Host": "",
"Origin": "",
"Referer": "",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36"
}
url = "https://**/api/v1/sessions"
data = {
"data": js_resul['data']
}
data = json.dumps(data, separators=(',', ':'))
response = requests.post(url, headers=headers, data=data, verify=False)
cookie_dict = response.cookies.get_dict()
p1 = cookie_dict['FW9****']
p2 = cookie_dict['dGg2****']
self.web_socket_url = "wss://***/***/pr/{}/b/ws/{}/{}".format(p2, js_resul['tabId'], p1)
self.tabId = js_resul['tabId']
def wss_on_message(self, ws, message):
print("=============== [message] ===============")
js_resul = execjs.compile(self.jsCode).call('js_sdk', "decode", list(message))['data']
print("recv message: ", js_resul)
print("recv count: ", js_resul[0][0])
if js_resul[0][0] != -1:
self.count = js_resul[0][0]
def wss_on_error(self, ws, error):
print("=============== [error] ===============")
print(error)
ws.close()
def wss_on_close(self, ws, close_status_code, close_msg):
print("=============== [closed] ===============")
print(close_status_code)
print(close_msg)
def wss_on_open(self, ws):
msg_list= [
[
426,
[
self.tabId,
"1",
4,
"1",
"498",
"mousedown",
29,
29,
1,
0,
1,
0,
"",
701,
177,
0,
0,
29,
29,
704,
1265,
704,
65,
True
]
],
[
431,
[
self.tabId,
"1",
4,
"1",
"594",
"click",
29,
29,
1,
0,
0,
0,
"",
701,
177,
0,
0,
29,
29,
704,
1265,
704,
65,
True
]
]
]
def run(*args):
count_client = 1
# for arg in msg_list:
time.sleep(3)
for arg in msg_list:
if arg[0] == -1:
print('count', self.count)
arg[1][3] = self.count
else:
arg[0] = count_client
count_client += 1
data_resu = execjs.compile(self.jsCode).call('js_sdk', 'data', arg)['data']
bytes_object = bytes(data_resu['data'])
print('发送', arg)
ws.send(bytes_object, opcode=websocket.ABNF.OPCODE_BINARY)
if arg[0] == -1:
time.sleep(3)
else:
time.sleep(random.random())
_thread.start_new_thread(run, (msg_list,))
def wss(self):
# websocket.enableTrace(True) # 是否显示连接详细信息
ws = websocket.WebSocketApp(
self.web_socket_url, on_open=self.wss_on_open,
on_message=self.wss_on_message, on_error=self.wss_on_error,
on_close=self.wss_on_close
)
print('start')
ws.run_forever()
if __name__ == '__main__':
W = WebSocketClinet()
# 第一次获取 cookie,包含 INGRESSCOOKIE、JSESSIONID、SERVERID、acw_tc
W.get_wss_first()
W.wss()