关注微信公众号:K爬虫大哥,继续分享高级爬虫、JS/Android逆向等技术干货!声明本文所有内容仅供学习交流。抓拍内容、敏感网址、数据接口均已脱敏处理,严禁用于商业或非法用途。否则,由此产生的一切后果与作者无关。侵权请联系我立即删除!逆向标靶:智慧树扫码登录,界面使用WebSocket通信协议主页:aHR0cHM6Ly9wYXNzcG9ydC56aGlodWlzaHUuY29tL2xvZ2luI3FyQ29kZUxvZ2luWebSocket介绍WebSocket是一种在单TCP连接上进行全双工通信的协议。WebSocket使客户端和服务器之间的数据交换变得更加简单。在WebSocketAPI中,浏览器和服务器只需要完成一次握手,两者之间就可以直接建立持久连接,进行双向数据传输。WebSocket协议缩写为WS或WSS(WebSocketSecure),发送请求的URL以ws://或wss://开头。WSS是WS的加密版本,类似于HTTP和HTTPS。WebSocket协议最大的特点就是服务端可以主动向客户端推送信息,客户端也可以主动向服务端发送信息。是真正的双向平等对话,属于服务器推送技术的一种。与HTTP的对比如下图:抓包分析,来到智慧树的扫码登录页面,抓包选择WS过滤WebSocket请求,如下图:有一些比较特殊的参数,分别是HTTP/HTTPS请求中没有:Upgrade:websocket:表示这是一个WebSocket类型的请求;Sec-WebSocket-Version:告诉服务器使用的WebsocketDraft(协议版本),必须是13;Sec-WebSocket-Extensions:协议扩展,某一类协议可能支持多种扩展,通过扩展可以对协议进行增强;Sec-WebSocket-Key:是WebSocket客户端发送的base64编码的密文,由浏览器随机生成,要求服务端返回对应的加密后的Sec-WebSocket-Accept响应,否则客户端会在WebSocket过程中抛出Error握手错误并关闭连接。让我们扫描二维码登录,然后选择“消息”选项卡。我们可以看到有一些数据交互,其中绿色箭头是客户端发送给服务端的数据,红色箭头是服务端返回给客户端的数据,如下图所示:整个交互过程。当我们打开二维码页面,也就是加载二维码的时候,就建立了WebSocket连接。每隔8秒左右,客户端主动发送一串字符串,服务端也返回同样的字符串,不过是字典格式的。当我们扫码成功后,服务器会返回扫码成功的信息。当我们点击登录时,客户端会返回扫描结果。如果成功,会有一个一次性密码oncePassword和一个uuid,这两个参数肯定会在后面的请求中用到。如果长时间不扫码,过段时间会返回二维码过期的信息,并且每8秒发送一条信息,只是为了保持连接和获取二维码的状态信息.那么这里有两个问题:你是如何得到交互式来回发送的字符串的?WebSocket请求在Python中应该如何实现?客户端如何在每8秒发送一次数据的同时,实时接收到服务器的信息呢?(观察请求扫码结果是实时返回的,所以不能每8秒接收一次)参数获取先解决第一个问题,客户端发送的字符串是怎么来的,这里是查找方法加密字符串和HTTP/HTTPS请求是一样的。在这个例子中,我们可以直接搜索这个字符串,发现它是通过接口传输的,其中img是二维码图片的base64值,qrToken是客户端发送的字符串String,如图下图:这里需要注意的是,并不是所有的WebSocket请求都这么简单,有的客户端发送的数据是BinaryMessage(二进制数据),或者更复杂的加密参数,直接搜索是获取不到的,针对这种情况,我们还有一个解决方法:已知的创建WebSocket对象的语句是:varSocket=newWebSocket(url,[protocol]);,所以我们可以搜索newWebSocket来定位请求建立的位置。已知一个WebSocket对象有如下相关事件,我们可以搜索对应的事件处理代码来定位:event事件处理描述openSocket.onopentriggersmessageSocket.onmessage当客户端收到服务器数据时触发errorSocket.onerror通信错误发生触发closeSocket.onclose连接关闭时触发已知一个WebSocket对象有以下相关方法,我们可以搜索对应的方法来定位:方法说明Socket.send()使用连接发送数据Socket.close()关闭connectionPython实现WebSocket请求然后前面说到第二个问题是如何用Python实现WebSocket请求?有许多用于连接到WebSocket的Python库。比较常用和稳定的有websocket-client(非异步)、websockets(异步)、aiowebsocket(异步)。本例中使用的是websocket-client,这里要注意第三个问题。对于客户端,需要每8秒发送一次数据。对于服务器,我们需要实时接收服务器的信息。我们可以观察请求,扫描码的结果是实时返回的。如果我们每8秒才接收一次数据,数据可能会丢失,整个程序的响应也不会及时,效率就会变低。websocket-client官方文档为我们提供了一个长连接demo,实现了连续三次发送数据,实时监听服务端返回的数据,其中websocket.enableTrace(True)表示是否显示连接详情:导入websocketimport_threadimporttimedefon_message(ws,message):print(message)defon_error(ws,error):print(error)defon_close(ws,close_status_code,close_msg):print("###closed###")defon_open(ws):defrun(*args):foriinrange(3):time.sleep(1)ws.send("Hello%d"%i)time.sleep(1)ws.close()打印(“线程终止...”)_thread.start_new_thread(run,())if__name__=="__main__":websocket.enableTrace(True)ws=websocket.WebSocketApp("ws://echo.websocket.org/",on_open=on_open,on_message=on_message,on_error=on_error,on_close=on_close)ws.run_forever()我们适当修改一下。run方法中,客户端仍然每8秒发送一次qr_token,实时接收服务端的消息,当消息中出现“扫码成功”字样时,保存获取的oncePassword和uuid,然后关闭连接。逻辑代码如下,然后接上二维码获取逻辑即可。(已脱敏处理,不能直接运行)importjsonimporttimeimport_threadimportwebsocketweb_socket_url="wss://appcomm-user.脱敏处理.com/app-commserv-user/websocket?qrToken=%s"qr_token="ca6e6cf4815de4b9f2"=""uuid=""defwss_on_message(ws,message):print("===============[消息]===============")message=json.loads(message)print(message)if"扫码成功"inmessage["msg"]:globalonce_password,uuidonce_password=message["oncePassword"]uuid=message["uuid"]ws.close()defwss_on_error(ws,error):print("===============[错误]===============")print(error)ws.close()defwss_on_close(ws,close_status_code,close_msg):print("===============[关闭]===============”)打印(关闭状态代码)打印(关闭消息)defwss_on_open(ws):defrun(*args):whileTrue:ws.send(qr_token)time.sleep(8)_thread.start_new_thread(运行,(qr_token,))defwss():#网络socket.enableTrace(True)#是否显示连接详情ws=websocket.WebSocketApp(web_socket_url%qr_token,on_open=wss_on_open,on_message=wss_on_message,on_error=wss??_on_error,on_close=wss_on_close)ws.run_forever()实现scan最重要代码登录WebSocket请求部分已经解决。扫码获取oncePassword和uuid后,后面的处理步骤就比较简单了。下面来看看完整的步骤:请求首页,第一次获取cookie,包括:INGRESSCOOKIE、JSESSIONID、SERVERID、acw_tc;请求获取二维码接口,获取二维码的base64值和qrToken;建立WebSocket连接,扫描二维码,获取一次性密码oncePassword和uuid(好像没什么用);请求登录接口,302重定向,需要携带一次性密码,第二次获取cookie,包括:CASLOGC、CASTGC,同时更新SERVERID;请求步骤4中的302重定向地址,第三次获取cookie,包括:SESSION;携带完整cookie,请求用户信息接口,获取真实用户名等信息其实WebSocket连接结束后,有很多请求,貌似都OK,但是经过K的测试,只有两个redirect比较有用,抓包如下:完整代码GitHub跟随K的爬虫,以及继续分享爬虫相关代码!欢迎加星!https://github.com/kgepachong/下面只是演示了部分关键代码,不能直接运行!完整代码仓库地址:https://github.com/kgepachong...Python登录代码importtimeimportjsonimportbase64import_threadimportrequestsimportwebsocketfromPILimportImageweb_socket_url="脱敏处理,完整代码关注GitHub:https://github.com/kgepachong/crawler"get_login_qr_img_url="脱敏处理,完整代码参考GitHub:https://github.com/kgepachong/crawler"login_url="脱敏处理,完整代码参考GitHub:https://github.com/kgepachong/crawler"user_info_url="脱敏处理,完整代码关注GitHub:https://github.com/kgepachong/crawler"headers={"Host":"脱敏处理,完整代码关注GitHub:https://github.com/kgepachong/crawler","Pragma":"no-cache","Referer":"脱敏处理,完整代码遵循GitHub:https://github.com/kgepachong/crawler","User-Agent":"Mozilla/5.0(WindowsNT10.0;Win64;x64)AppleWebKit/537.36(KHTML,如Gecko)Chrome/94.0.4606.81Safari/537.36"}qr_token=""once_password=""uuid=""cookie={}defget_cookies_first():response=requests.get(url=login_url,headers=headers)全局cookiecookie=response.cookies.get_dict()defget_login_qr_img():response=requests.get(url=get_login_qr_img_url,headers=headers,cookies=cookie).json()qr_img=response["img"]globalqr_tokenqr_token=response["qrToken"]withopen('code.png','wb')asf:f.write(base64.b64decode(qr_img))image=Image.open('code.png')image.show()print("请扫描验证码!")defwss_on_message(ws,message):print("===============[消息]===============")message=json.loads(message)print(message)if"扫码成功"inmessage["msg"]:globalonce_password,uuidonce_password=message["oncePassword"]uuid=message["uuid"]ws.close()defwss_on_error(ws,error):print("===============[错误]===============”)打印(错误)ws.close()defwss_on_close(ws,close_status_code,close_msg):打印(“================[关闭]================”)打印(关闭状态代码)打印(关闭消息)defwss_on_open(ws):defrun(*args):whileTrue:ws.send(qr_token)time.sleep(8)_thread.start_new_thread(run,(qr_token,))defwss():#websocket.enableTrace(True)#是否显示连接详情ws=websocket.WebSocketApp(web_socket_url%qr_token,on_open=wss_on_open,on_message=wss_on_message,on_error=wss??_on_error,on_close=wss_on_close)ws.run_forever()defget_cookie_second():"ppwamdglobal"cookieon_parwass{"service":"脱敏处理,完整代码参考GitHub:https://github.com/kgepachong/crawler"}headers["Host"]="脱敏处理,完整代码参考GitHub:https://github.com/kgepachong/crawler"headers["Referer"]="脱敏处理,完整代码关注github:https://github.com/kgepachong/crawler"response=requests.get(url=login_url,params=params,headers=headers,cookies=cookie,allow_redirects=False)cookie.update(response.cookies.get_dict())location=response.headers.get("Location")returnlocationdefget_cookie_third(location):globalcookieheaders["Host"]="脱敏处理,完整代码遵循GitHub:https://github.com/kgepachong/crawler"headers["Referer"]="脱敏处理,完整代码遵循GitHub:https://github.com/kgepachong/crawler"response=requests.get(url=location,headers=headers,cookies=cookie,allow_redirects=False)cookie.update(response.cookies.get_dict())location=response.headers.get("Location")返回locationdefget_login_user_info():headers["Host"]="For脱敏处理,完整代码请关注GitHub:https://github.com/kgepachong/crawler"headers["Origin"]="脱敏处理,完整代码请关注GitHub:https://github.com/kgepachong/crawler"headers["Referer"]="脱敏处理,完整代码关注github:https://github.com/kgepachong/crawler"params={"time":str(int(time.time()*1000))}response=requests.get(url=user_info_url,headers=headers,cookies=cookie,params=params)print(response.text)defmain():#第一次获取cookie,包括INGRESSCOOKIE,JSESSIONID,SERVERID,acw_tcget_cookies_first()#获取二维码get_login_qr_img()#Websocket扫码登录,返回一次性密码wss()#第二次第一次获取cookie,更新SERVERID,获??取CASLOGC,CASTGClocation1=get_cookie_second()#第三次获取cookie,获取SESSIONget_cookie_third(location1)#获取登录用户信息get_login_user_info()if__name__=='__main__':main()