RPC过某乎zse96参数-某乎热搜

目标

地址https://www.zhihu.com/hot

需求:知乎热榜数据抓取

字段:字段:标题、链接地址、热度, 用mongodb保存

思路

前期工作

定位接口

关键词搜索定位接口

精简请求

精简请求后只剩下这4个参数需要逆向

逆向参数

x-zse-93

多次重放测试后可以确定这是一个定值:101_3_3.0

d_c0

cookie是后端返回的

d_c0请求并无特别加密

z_c0

z_c0也是后端返回的

这个参数和登陆接口有关,经过测试可以复用,如果选择逆向的话,需要直面点选验证码,后面会出一篇机器学习过点选的文章,到时候拿这个试手,这里笔者选择复用。

x-zse-96

定位加密位置

搜索x-zse-96

一共有2处,都打上断点。

成功断住,取消勾选另外一个断点,不难看出,ef函数调用是关键突破点

分析加密逻辑

进入ef函数,一眼看见signature关键词及其生成代码

关键代码:tq(ti).encrypt.(tg()(td))

参数:tg()(td)

看上去像一个md5加密
用'1'测试确定了是标准md5
td的生成逻辑一目了然

总结一下:”101_3_3.0”+url的path和search部分+dc0,用+号作为连接符形成的字符串再用标准的md5加密,虽然上图显示还需要x-zst-81,但经过测试header里不传这个参数也可以不需要这个。

方法:(0,tq(ti).encrypt)


可以看到是_g._encrypt对参数进行了加密,一般到了这一步有两种选择,1是使用rpc,2是继续逆向,这里选择1。

RPC

RPC是远程过程调用(Remote Procedure Call)的缩写形式,简单来说就是将本地和浏览器,看做是服务端和客户端,二者之间通过 WebSocket 协议进行通信,在浏览器中将加密函数暴露出来,在本地直接调用浏览器中对应的加密函数,从而得到加密结果,不必去在意函数具体的执行逻辑,也省去了扣代码、补环境等操作,可以省去大量的逆向调试时间。

这里使用Sekiro-RPC搭建服务器和客户端。
Sekiro-RPC的github传送门

油猴脚本注入客户端

注入客户端之前先打开本地Sekiro-RPC服务器,然后在油猴脚本新建内容如下的脚本。

// ==UserScript==
// @name         zhihu
// @namespace    zhihu
// @version      0.1
// @description  zhihu
// @author       yueqian
// @match        https://www.zhihu.com/*
// @grant        none
// @require      http://file.virjar.com/sekiro_web_client.js?_=123
// ==/UserScript==

!(function () {
    'use strict';

    function guid() {
        function S4() {
            return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
        }

        return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
    }

    var client = new SekiroClient("ws://127.0.0.1:5620/business-demo/register?group=rpc&clientId=" + guid());
    client.registerAction("zse96", function (request, resolve, reject) {
        return resolve(window.zse96(request["param"]))
    });
})();

将加密函数的引用挂到全局作用域中

使用override修改源码

python调用rpc

def get5rpc(param):
    data = {
        "group": "rpc",
        "action": "zse96",
        "param": param
    }
    res = requests.post(url="http://127.0.0.1:5620/business-demo/invoke", data=data, verify=False)
    resp = res.json()
    zse96 = '2.0_' + resp['data']
    logger.debug('zse96: ' + zse96)
    return zse96

传入一个’1’测试,可以正常拿到结果。

代码及运行结果

import json

import requests
from jmespath import search
from loguru import logger
import pymongo

import hashlib


class Spider():
    def __init__(self):
        self.mongodb = pymongo.MongoClient()
        self.db = self.mongodb['spider']
        self.coll = self.db['zhihuHot']

    def get5rpc(self, param):
        data = {
            "group": "rpc",
            "action": "zse96",
            "param": param
        }
        res = requests.post(url="http://127.0.0.1:5620/business-demo/invoke", data=data, verify=False)
        resp = res.json()
        zse96 = '2.0_' + resp['data']
        logger.debug('zse96: ' + zse96)
        return zse96

    def md5(self, str):
        md5 = hashlib.md5()
        md5.update(str.encode('utf-8'))
        return md5.hexdigest()

    def get_dc0(self):
        headers = {
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
            'Referer': 'https://www.zhihu.com/search?type=content&q=%E7%88%AC%E8%99%AB',
        }

        response = requests.post('https://www.zhihu.com/udid', headers=headers)
        dc0 = response.cookies.get_dict()['d_c0']
        logger.debug(f'dc0: {dc0}')
        return dc0

    def spider(self, d_c0):
        zse96 = self.get5rpc(self.md5("101_3_3.0+/api/v3/feed/topstory/hot-lists/total?limit=50&desktop=true+" + d_c0))
        headers = {
            "Referer": "https://www.zhihu.com/hot",
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36",
            "x-zse-93": "101_3_3.0",
            "x-zse-96": zse96,
        }
        cookies = {
            "d_c0": d_c0,
            "z_c0": "2|1:0|10:1686573425|4:z_c0|92:Mi4xSzlXNEJnQUFBQUFBRUpkdmhfcnJGaVlBQUFCZ0FsVk5jRjkwWlFCSmdseHJOaXBhT3kyWTl5MFNDUGtIVmdET3N3|86159a1b5ed1dd64ad5f260c920e1e50350aa54f9a9d20f96dcb46ac379458dc",
        }
        url = "https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total"
        logger.debug(f'请求: {url}')
        params = {
            "limit": "50",
            "desktop": "true"
        }
        response = requests.get(url, headers=headers, cookies=cookies, params=params)
        logger.debug(f'下载: {response.status_code}')
        return response

    def parse(self, res):
        data2save = []
        items = search('data', res.json())
        for item in items:
            title = search('target.title', item)
            href = search('target.url', item)
            hotDegree = search('detail_text', item)
            data2save.append({
                'title': title,
                'href': href,
                'hotDegree': hotDegree,
            })
        logger.debug('解析: ' + json.dumps(data2save, indent=2, ensure_ascii=False))
        return data2save

    def save(self, data):
        for i in data:
            self.coll.insert_one(i)
        logger.debug('保存成功!')

    def main(self):
        dc0 = self.get_dc0()
        resp = self.spider(dc0)
        data = self.parse(resp)
        self.save(data)


if __name__ == '__main__':
    spider = Spider()
    spider.main()
  

运行结果

mongodb存储

这道题的解法不仅对热榜api是有效的,经过测试对知乎搜索api也同样适用,把url和param改一下即可。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,如有问题请邮件至2454612285@qq.com。
跃迁主页