关于python:JS-逆向百例WebSocket-协议爬虫智慧树扫码登录案例分析

4次阅读

共计 8020 个字符,预计需要花费 21 分钟才能阅读完成。

关注微信公众号:K 哥爬虫,继续分享爬虫进阶、JS/ 安卓逆向等技术干货!

申明

本文章中所有内容仅供学习交换,抓包内容、敏感网址、数据接口均已做脱敏解决,严禁用于商业用途和非法用处,否则由此产生的所有结果均与作者无关,若有侵权,请分割我立刻删除!

逆向指标

  • 指标:智慧树扫码登录,接口应用了 WebSocket 通信协议
  • 主页:aHR0cHM6Ly9wYXNzcG9ydC56aGlodWlzaHUuY29tL2xvZ2luI3FyQ29kZUxvZ2lu

WebSocket 简介

WebSocket 是一种在单个 TCP 连贯上进行全双工通信的协定,WebSocket 使得客户端和服务器之间的数据交换变得更加简略。在 WebSocket API 中,浏览器和服务器只须要实现一次握手,两者之间就间接能够创立持久性的连贯,并进行双向数据传输。

WebSocket 协定简称为 WS 或者 WSS(WebSocket Secure),其发送申请的 URL 以 ws:// 或者 wss:// 结尾,WSS 是 WS 的加密版本,相似于 HTTP 与 HTTPS。

WebSocket 协定的最大特点就是:服务器能够被动向客户端推送信息,客户端也能够被动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。与 HTTP 的对比方下图所示:

抓包剖析

来到智慧树的扫码登录页面,抓包选中 WS,用来筛选 WebSocket 申请,如下图所示:

其中有一些比拟特地的参数,是 HTTP/ HTTPS 申请中没有的:

  • Upgrade: websocket:表明这是 WebSocket 类型申请;
  • Sec-WebSocket-Version:通知服务器所应用的 Websocket Draft(协定版本),必须是 13;
  • Sec-WebSocket-Extensions:协定扩大,某类协定可能反对多个扩大,通过它能够实现协定加强;
  • Sec-WebSocket-Key:是 WebSocket 客户端发送的一个 base64 编码的密文,是浏览器随机生成的,要求服务端必须返回一个对应加密的 Sec-WebSocket-Accept 应答,否则客户端会抛出 Error during WebSocket handshake 谬误,并敞开连贯。

咱们先扫码登录一遍,再抉择 Messages 选项卡,能够看到有一些数据交互,其中绿色的箭头是客户端发送给服务器的数据,红色箭头是服务器响应返回给客户端的数据,如下图所示:

咱们察看一下整个交互过程,当咱们关上二维码页面后,也就是二维码加载进去的同时,WebSocket 连贯就建设了,每隔 8 秒左右,客户端就被动发送一串字符串,服务端也返回雷同的字符串,只不过是字典格局,当咱们扫码胜利时,服务端就返回扫码胜利的信息,当咱们点击登陆时,客户端又会返回扫码后果,如果胜利,就有一个一次性明码 oncePassword 和一个 uuid,这两个参数必定在后续的申请中会用到的。如果长时间不扫码的话,过段时间就会返回二维码已生效的信息,每隔 8 秒发送一次音讯,正是为了放弃连贯以及获取二维码状态音讯。

那么到这里就呈现了两个问题:

  1. 在来回交互发送的那串字符串,是怎么得来的?
  2. 在 Python 中应该如何实现 WebSocket 申请?
  3. 如何实现客户端每隔 8 秒发送一次数据的同时,实时接管服务端的信息?(察看申请扫码后果实时返回的,所以不能每隔 8 秒才接管一次)

参数获取

首先解决第一个问题,客户端发送的那串字符串是怎么来的,这里寻找加密字符串的形式和 HTTP/HTTPS 申请是一样的,在本例中,咱们能够间接搜寻这个字符串,发现是通过一个接口传过来的,其中 img 就是二维码图片的 base64 值,qrToken 就是客户端发送的那串字符串,如下图所示:

这里须要留神的是,并不是所有的 WebSocket 申请都是如此的简略的,有的客户端发送的数据是 Binary Message(二进制数据)、或者更简单的加密参数,间接搜寻无奈获取,针对这种状况,咱们也有解决办法:

  1. 已知创立 WebSocket 对象的语句为:var Socket = new WebSocket(url, [protocol] );,所以咱们能够搜寻 new WebSocket 定位到建设申请的地位。
  2. 已知一个 WebSocket 对象有以下相干事件,咱们能够搜寻对应事件处理程序代码来定位:
事件 事件处理程序 形容
open Socket.onopen 连贯建设时触发
message Socket.onmessage 客户端接管服务端数据时触发
error Socket.onerror 通信产生谬误时触发
close Socket.onclose 连贯敞开时触发
  1. 已知一个 WebSocket 对象有以下相干办法,咱们能够搜寻对应办法来定位:
办法 形容
Socket.send() 应用连贯发送数据
Socket.close() 敞开连贯

Python 实现 WebSocket 申请

接着后面说,第二个问题,在 Python 中应该如何实现 WebSocket 申请?Python 库中用于连贯 WebSocket 的有很多,比拟罕用、稳固的有 websocket-client(非异步)、websockets(异步)、aiowebsocket(异步)。在本案例中应用 websocket-client,这里还要留神第三个问题,对于客户端来说,要每隔 8 秒发送一次数据,对于服务端,咱们须要实时接管服务端的信息,能够察看申请,扫码的后果是实时返回的,如果咱们也每隔 8 秒才接管一次数据的话,有可能会失落数据,而且也会使得整个程序的响应也不及时,效率变低。

在 websocket-client 官网文档中给咱们提供了一个长连贯的 demo,它实现了间断发送三次数据,并实时监听服务端返回的数据,其中的 websocket.enableTrace(True) 示意是否显示连贯详细信息:

import websocket
import _thread
import time


def on_message(ws, message):
    print(message)


def on_error(ws, error):
    print(error)


def on_close(ws, close_status_code, close_msg):
    print("### closed ###")


def on_open(ws):
    def run(*args):
        for i in range(3):
            time.sleep(1)
            ws.send("Hello %d" % i)
        time.sleep(1)
        ws.close()
        print("thread terminating...")
    _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,实时接管服务端的音讯,当“扫码胜利”字样呈现在音讯里时,将失去的 oncePassworduuid 存起来,而后敞开连贯,逻辑代码如下所示,后续只有将二维码的获取逻辑接入就行了。(已脱敏解决,不能间接运行)

import json
import time
import _thread
import websocket


web_socket_url = "wss://appcomm-user. 脱敏解决.com/app-commserv-user/websocket?qrToken=%s"
qr_token = "ca6e6cfb70de4f2f915b968aefcad404"
once_password = ""uuid =""


def wss_on_message(ws, message):
    print("=============== [message] ===============")
    message = json.loads(message)
    print(message)
    if "扫码胜利" in message["msg"]:
        global once_password, uuid
        once_password = message["oncePassword"]
        uuid = message["uuid"]
        ws.close()


def wss_on_error(ws, error):
    print("=============== [error] ===============")
    print(error)
    ws.close()


def wss_on_close(ws, close_status_code, close_msg):
    print("=============== [closed] ===============")
    print(close_status_code)
    print(close_msg)


def wss_on_open(ws):
    def run(*args):
        while True:
            ws.send(qr_token)
            time.sleep(8)
    _thread.start_new_thread(run, (qr_token,))


def wss():
    # 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()

实现扫码登录

最重要的 WebSocket 申请局部曾经解决了,扫码拿到 oncePassworduuid 后,后续的解决步骤就比较简单了,当初来理一下残缺的步骤:

  1. 申请首页,第一次获取 cookie,蕴含:INGRESSCOOKIE、JSESSIONID、SERVERID、acw_tc;
  2. 申请获取二维码接口,失去二维码的 base64 值和 qrToken;
  3. 建设 WebSocket 连贯,扫描二维码,获取一次性明码 oncePassword 和 uuid(如同没什么用);
  4. 申请一个登录接口,302 重定向,须要携带一次性明码,第二次获取 cookie,蕴含:CASLOGC、CASTGC,同时更新 SERVERID;
  5. 申请第 4 步 302 重定向地址,第三次获取 cookie,蕴含:SESSION;
  6. 携带残缺 cookie,申请用户信息接口,获取实在用户名等信息。

实际上 WebSocket 连贯完结后,有很多申请,看起来都比拟能够,然而通过 K 哥测试,只有两个重定向比拟有用,抓包如下:

残缺代码

GitHub 关注 K 哥爬虫,继续分享爬虫相干代码!欢送 star!https://github.com/kgepachong/

以下只演示局部要害代码,不能间接运行! 残缺代码仓库地址:https://github.com/kgepachong…

Python 登录代码

import time
import json
import base64
import _thread
import requests
import websocket
from PIL import Image


web_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 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36"
}

qr_token = ""once_password =""
uuid = ""
cookie = {}


def get_cookies_first():
    response = requests.get(url=login_url, headers=headers)
    global cookie
    cookie = response.cookies.get_dict()


def get_login_qr_img():
    response = requests.get(url=get_login_qr_img_url, headers=headers, cookies=cookie).json()
    qr_img = response["img"]
    global qr_token
    qr_token = response["qrToken"]
    with open('code.png', 'wb') as f:
        f.write(base64.b64decode(qr_img))
    image = Image.open('code.png')
    image.show()
    print("请扫描验证码!")


def wss_on_message(ws, message):
    print("=============== [message] ===============")
    message = json.loads(message)
    print(message)
    if "扫码胜利" in message["msg"]:
        global once_password, uuid
        once_password = message["oncePassword"]
        uuid = message["uuid"]
        ws.close()


def wss_on_error(ws, error):
    print("=============== [error] ===============")
    print(error)
    ws.close()


def wss_on_close(ws, close_status_code, close_msg):
    print("=============== [closed] ===============")
    print(close_status_code)
    print(close_msg)


def wss_on_open(ws):
    def run(*args):
        while True:
            ws.send(qr_token)
            time.sleep(8)
    _thread.start_new_thread(run, (qr_token,))


def wss():
    # 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()


def get_cookie_second():
    global cookie
    params = {
        "pwd": once_password,
        "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")
    return location


def get_cookie_third(location):
    global cookie
    headers["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")
    return location


def get_login_user_info():
    headers["Host"] = "脱敏解决,残缺代码关注 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)


def main():
    # 第一次获取 cookie,蕴含 INGRESSCOOKIE、JSESSIONID、SERVERID、acw_tc
    get_cookies_first()
    # 获取二维码
    get_login_qr_img()
    # websocket 扫码登录,返回一次性明码
    wss()
    # 第二次获取 cookie,更新 SERVERID、获取 CASLOGC、CASTGC
    location1 = get_cookie_second()
    # 第三次获取 cookie,获取 SESSION
    get_cookie_third(location1)
    # 获取登录用户信息
    get_login_user_info()


if __name__ == '__main__':
    main()

正文完
 0