【js逆向专题】12.RPC技术

目录

      • 一. websocket
        • 1. 什么是websocket
        • 2. websocket的原理
        • 3. websocket实现方式
          • 1. 客户端
          • 2.服务端
          • 3. 实际案例
            • 1. 案例目标
            • 2. 解析思路
      • 二. RPC
        • 1. RPC 简介
        • 2.Sekiro-RPC
          • 1. 使用方法
            • 1. 执行方式
            • 2.客户端环境
            • 3.使用参数说明
          • 2. 测试使用
            • 1. 前端代码
            • 2. SK API
            • 3.`python`调用代码
      • 三.项目实战
        • 1. 替换文件注入案例1
          • 1. 逆向目标
          • 2.定位cookie加密位置
        • 2. 替换文件注入案例2
          • 1. 逆向目标
          • 2. 逆向分析
          • 3. 实现代码
        • 3. 油猴注入形式
          • 1. 油猴工具介绍
          • 2.逆向目标
          • 3. 逆向分析
      • 结语

上一篇直通车:【js逆向专题】11.AST节点树

js逆向专题传送门

学习目标:

  1. 了解 websocket协议
  2. 熟悉 websocket实现原理
  3. 掌握 RPC启用和注入方式

RPC,英文 RangPaCong,中文让爬虫,旨在为爬虫开路,秒杀一切,让爬虫畅通无阻!图片

WebSocket的出现,使得浏览器具备了实时双向通信的能力。

参考:https://blog.csdn.net/zyym9009/article/details/104203995

参考:https://www.cnblogs.com/chyingp/p/websocket-deep-in.html

一. websocket

1. 什么是websocket
  • WebSocket是HTML5下一种新的协议(websocket协议本质上是一个基于tcp的协议)
  • 它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的
  • Websocket是一个持久化的协议
2. websocket的原理
  • websocket约定了一个通信的规范,通过一个握手的机制,客户端和服务器之间能建立一个类似tcp的连接,从而方便它们之间的通信
  • 在websocket出现之前,web交互一般是基于http协议的短连接或者长连接
  • websocket是一种全新的协议,不属于http无状态协议,协议名为"ws"

总结(总体过程):

  • 首先,客户端发起http请求,经过3次握手后,建立起TCP连接;http请求里存放WebSocket支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version等;
  • 然后,服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据;
  • 最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。
3. websocket实现方式
1. 客户端
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<input id="box" type="text">
<button onclick="ps()">发送</button>

<script>

    // 与服务器约定的连接 以及端口  本机的   hosts文件  localhost      www.ps.com
    const websocket = new WebSocket('ws://127.0.0.1:8080/')

    //连接发生错误的回调方法
    websocket.onerror = () => {
        console.log("WebSocket连接发生错误");
    };

    //连接成功建立的回调方法
    websocket.onopen = function () {
        console.log("WebSocket连接成功");
    }

    //接收到消息的回调方法  接收服务器的数据
    websocket.onmessage = function (event) {
        console.log(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function () {
        console.log("WebSocket连接关闭");
    }

    function ps() {
        // jquery -> val   JS -> value
        var text = document.getElementById('box').value
        // 客户端发信息发服务器
        websocket.send(text)
    }

</script>
</body>
</html>
2.服务端
# encoding: utf-8
import asyncio
import websockets


async def echo(websocket):
    # 使用WebSocket在客户端和服务器之间建立全双工双向连接后,就可以在连接打开时调用send()方法。
    message = 'hello world'
    # 发送数据
    await websocket.send(message)
    return True


async def recv_msg(websocket):
    while 1:
        # 接收数据
        recv_text = await websocket.recv()
        print(recv_text)


async def main_logic(websocket, path):
    await echo(websocket)
    await recv_msg(websocket)


start_server = websockets.serve(main_logic, '127.0.0.1', 8080)
loop = asyncio.get_event_loop()
loop.run_until_complete(start_server)
# 创建了一个连接对象之后,需要不断监听返回的数据,则调用 run_forever 方法,要保持长连接即可
loop.run_forever()

3. 实际案例
1. 案例目标
  • 网址:https://jzsc.mohurd.gov.cn/data/company

  • 需求:通过websocket解析加密数据

  • 实际注入网站代码

!(function () {
    if (window.flag) {
    } else {
        const websocket = new WebSocket('ws://127.0.0.1:8080')
// 创建一个标记用来判断是否创建套接字
        window.flag = true;
//  接收服务端发送的信息
        websocket.onmessage = function (event) {
            var data = event.data
// 调用js解密
            var res = b(data)
            console.log(res)
// 发送解密数据给服务端
            websocket.send(res)
        }
    }
}())
2. 解析思路
  • 定位到加密位置
  • 将我们写的websocket命令注入到代码当中(通过替换的方式实现)

在这里插入图片描述

  • 要注意数据是否正确,不带v请求头数据是有问题的

在这里插入图片描述

  • 注入之后需要刷新页面才能把js 执行起来

  • python执行代码

# encoding: utf-8
import asyncio
import websockets
import requests
import time
import json

def get_data(page):
    headers = {
        "v": "231012",
        "Referer": "https://jzsc.mohurd.gov.cn/data/company",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
    }

    url = "https://jzsc.mohurd.gov.cn/APi/webApi/dataservice/query/comp/list"
    params = {
        "pg": page,
        "pgsz": "15",
        "total": "450"
    }
    response = requests.get(url, headers=headers, params=params)
    print(response.text)
    return response.text

async def echo(websocket):
    for i in range(1, 4):

        data = get_data(i)
        await websocket.send(data)
        # time.sleep(2)
        # return True


async def recv_msg(websocket):
    while 1:
        # 接收数据
        recv_text = await websocket.recv()
        print(json.loads(recv_text))


async def main_logic(websocket, path):
    await echo(websocket)
    await recv_msg(websocket)


start_server = websockets.serve(main_logic, '127.0.0.1', 8080)
loop = asyncio.get_event_loop()
loop.run_until_complete(start_server)
# 创建了一个连接对象之后,需要不断监听返回的数据,则调用 run_forever 方法,要保持长连接即可
loop.run_forever()

二. RPC

1. RPC 简介

为什么要使用RPC技术呢?我们在使用websocket时候可以发现,python在操作的时候,需要创建连接,还需要不断去接受传递数据,非常的麻烦, 那这个时候rpc技术可以帮助到我们,简单来说就是网页直接和rpc服务器进行交互,我们python可以直接调用,rpc暴露的接口,不需要关心,创建连接这一块的问题.

RPC 技术是非常复杂的,对于我们搞爬虫、逆向的来说,不需要完全了解,只需要知道这项技术如何在逆向中应用就行了。

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

在这里插入图片描述

2.Sekiro-RPC
  • 官网地址:https://sekiro.iinti.cn/sekiro-doc/
1. 使用方法
1. 执行方式
  • 在本地开启服务端
  • 需要有 Java 环境,配置参考:https://baijiahao.baidu.com/s?id=1762153534119669123&wfr=spider&for=pc
    • 下载地址:https://repo.huaweicloud.com/java/jdk/8u201-b09/
  • Linux & Mac:bin/sekiro.sh 双击打开服务端
  • Windows:bin/sekiro.bat 双击打开服务端
2.客户端环境
  • 地址:file.virjar.com/sekiro_web_client.js?_=123 这个地址是在前端创建客户端的时候需要用到的代码,Sekiro-RPC 把他封装在一个地址里面了
3.使用参数说明
  • 使用原理:客户端注入到浏览器环境,然后通过SekiroClientSekiro 服务器通信,即可直接 RPC 调用浏览器内部方法,官方提供的 SekiroClient 代码样例如下:
// 生成唯一标记uuid编号
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=ws-group&clientId="+guid());
// 业务接口 
client.registerAction("登陆",function(request, resolve, reject){
    resolve(""+new Date());
})

  • group:业务类型(接口组),每个业务一个 groupgroup 下面可以注册多个终端(SekiroClient),同时group 可以挂载多个 Action

  • clientId:指代设备,多个设备使用多个机器提供 API 服务,提供群控能力和负载均衡能力;

  • SekiroClient:服务提供者客户端,主要场景为手机/浏览器等。最终的 Sekiro 调用会转发到 SekiroClient。每个 client 需要有一个惟一的 clientId

  • registerAction:接口,同一个 group 下面可以有多个接口,分别做不同的功能;

  • resolve:将内容传回给服务端的方法;

  • request:服务端传过来的请求,如果请求里有多个参数,可以以键值对的方式从里面提取参数然后再做处理。

2. 测试使用
1. 前端代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<script src="http://file.virjar.com/sekiro_web_client.js?_=123"></script>
<script>
    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-test&clientId=" + guid());

    client.registerAction("clientTime", function (request, resolve, reject) {
        resolve("" + new Date());
    })

</script>
</body>
</html>
2. SK API

Sekiro 为我们提供了一些 API

  • 查看分组列表:http://127.0.0.1:5620/business-demo/groupList

  • 查看队列状态:http://127.0.0.1:5620/business-demo/clientQueue?group=rpc-test

  • 调用转发:http://127.0.0.1:5620/business-demo/invoke?group=rpc-test&action=clientTime

3.python调用代码
import requests

params = {
    "group": "rpc-test",
    "action": "clientTime",
}
res = requests.get("http://127.0.0.1:5620/business-demo/invoke", params=params)
print(res.text)

三.项目实战

1. 替换文件注入案例1
1. 逆向目标
  • 地址:http://q.10jqka.com.cn/
  • 接口:http://q.10jqka.com.cn/index/index/board/all/field/zdf/order/desc/page/2/ajax/1/
  • 对抗:cookie反爬虫处理,关键字v
2.定位cookie加密位置
(function () {
  Object.defineProperty(document, 'cookie', {
    set: function (val) {
      if (val.indexOf('v') != -1) {
        debugger;
      }
      console.log('Hook捕获到cookie设置->', val);
      return val;
    }
  });
})();

在这里插入图片描述

  • 确定好位置之后我们就可以替换当前文件,把我们需要的rpc的代码给注入到网页中

  • JavaScript代码

(function () {

    function SekiroClient(wsURL) {
        this.wsURL = wsURL;
        this.handlers = {};
        this.socket = {};
        this.base64 = false;
        // check
        if (!wsURL) {
            throw new Error('wsURL can not be empty!!')
        }
        this.webSocketFactory = this.resolveWebSocketFactory();
        this.connect()
    }

    SekiroClient.prototype.resolveWebSocketFactory = function () {
        if (typeof window === 'object') {
            var theWebSocket = window.WebSocket ? window.WebSocket : window.MozWebSocket;
            return function (wsURL) {

                function WindowWebSocketWrapper(wsURL) {
                    this.mSocket = new theWebSocket(wsURL);
                }

                WindowWebSocketWrapper.prototype.close = function () {
                    this.mSocket.close();
                };

                WindowWebSocketWrapper.prototype.onmessage = function (onMessageFunction) {
                    this.mSocket.onmessage = onMessageFunction;
                };

                WindowWebSocketWrapper.prototype.onopen = function (onOpenFunction) {
                    this.mSocket.onopen = onOpenFunction;
                };
                WindowWebSocketWrapper.prototype.onclose = function (onCloseFunction) {
                    this.mSocket.onclose = onCloseFunction;
                };

                WindowWebSocketWrapper.prototype.send = function (message) {
                    this.mSocket.send(message);
                };

                return new WindowWebSocketWrapper(wsURL);
            }
        }
        if (typeof weex === 'object') {
            // this is weex env : https://weex.apache.org/zh/docs/modules/websockets.html
            try {
                console.log("test webSocket for weex");
                var ws = weex.requireModule('webSocket');
                console.log("find webSocket for weex:" + ws);
                return function (wsURL) {
                    try {
                        ws.close();
                    } catch (e) {
                    }
                    ws.WebSocket(wsURL, '');
                    return ws;
                }
            } catch (e) {
                console.log(e);
                //ignore
            }
        }
        //TODO support ReactNative
        if (typeof WebSocket === 'object') {
            return function (wsURL) {
                return new theWebSocket(wsURL);
            }
        }

        throw new Error("the js environment do not support websocket");
    };

    SekiroClient.prototype.connect = function () {
        console.log('sekiro: begin of connect to wsURL: ' + this.wsURL);
        var _this = this;
        // 涓峜heck close锛岃
        // if (this.socket && this.socket.readyState === 1) {
        //     this.socket.close();
        // }
        try {
            this.socket = this.webSocketFactory(this.wsURL);
        } catch (e) {
            console.log("sekiro: create connection failed,reconnect after 2s");
            setTimeout(function () {
                _this.connect()
            }, 2000)
        }

        this.socket.onmessage(function (event) {
            _this.handleSekiroRequest(event.data)
        });

        this.socket.onopen(function (event) {
            console.log('sekiro: open a sekiro client connection')
        });

        this.socket.onclose(function (event) {
            console.log('sekiro: disconnected ,reconnection after 2s');
            setTimeout(function () {
                _this.connect()
            }, 2000)
        });
    };

    SekiroClient.prototype.handleSekiroRequest = function (requestJson) {
        console.log("receive sekiro request: " + requestJson);
        var request = JSON.parse(requestJson);
        var seq = request['__sekiro_seq__'];

        if (!request['action']) {
            this.sendFailed(seq, 'need request param {action}');
            return
        }
        var action = request['action'];
        if (!this.handlers[action]) {
            this.sendFailed(seq, 'no action handler: ' + action + ' defined');
            return
        }

        var theHandler = this.handlers[action];
        var _this = this;
        try {
            theHandler(request, function (response) {
                try {
                    _this.sendSuccess(seq, response)
                } catch (e) {
                    _this.sendFailed(seq, "e:" + e);
                }
            }, function (errorMessage) {
                _this.sendFailed(seq, errorMessage)
            })
        } catch (e) {
            console.log("error: " + e);
            _this.sendFailed(seq, ":" + e);
        }
    };

    SekiroClient.prototype.sendSuccess = function (seq, response) {
        var responseJson;
        if (typeof response == 'string') {
            try {
                responseJson = JSON.parse(response);
            } catch (e) {
                responseJson = {};
                responseJson['data'] = response;
            }
        } else if (typeof response == 'object') {
            responseJson = response;
        } else {
            responseJson = {};
            responseJson['data'] = response;
        }

        if (typeof response == 'string') {
            responseJson = {};
            responseJson['data'] = response;
        }

        if (Array.isArray(responseJson)) {
            responseJson = {
                data: responseJson,
                code: 0
            }
        }

        if (responseJson['code']) {
            responseJson['code'] = 0;
        } else if (responseJson['status']) {
            responseJson['status'] = 0;
        } else {
            responseJson['status'] = 0;
        }
        responseJson['__sekiro_seq__'] = seq;
        var responseText = JSON.stringify(responseJson);
        console.log("response :" + responseText);


        if (responseText.length < 1024 * 6) {
            this.socket.send(responseText);
            return;
        }

        if (this.base64) {
            responseText = this.base64Encode(responseText)
        }

        //澶ф姤鏂囪鍒嗘浼犺緭
        var segmentSize = 1024 * 5;
        var i = 0, totalFrameIndex = Math.floor(responseText.length / segmentSize) + 1;

        for (; i < totalFrameIndex; i++) {
            var frameData = JSON.stringify({
                    __sekiro_frame_total: totalFrameIndex,
                    __sekiro_index: i,
                    __sekiro_seq__: seq,
                    __sekiro_base64: this.base64,
                    __sekiro_is_frame: true,
                    __sekiro_content: responseText.substring(i * segmentSize, (i + 1) * segmentSize)
                }
            );
            console.log("frame: " + frameData);
            this.socket.send(frameData);
        }
    };

    SekiroClient.prototype.sendFailed = function (seq, errorMessage) {
        if (typeof errorMessage != 'string') {
            errorMessage = JSON.stringify(errorMessage);
        }
        var responseJson = {};
        responseJson['message'] = errorMessage;
        responseJson['status'] = -1;
        responseJson['__sekiro_seq__'] = seq;
        var responseText = JSON.stringify(responseJson);
        console.log("sekiro: response :" + responseText);
        this.socket.send(responseText)
    };

    SekiroClient.prototype.registerAction = function (action, handler) {
        if (typeof action !== 'string') {
            throw new Error("an action must be string");
        }
        if (typeof handler !== 'function') {
            throw new Error("a handler must be function");
        }
        console.log("sekiro: register action: " + action);
        this.handlers[action] = handler;
        return this;
    };

    SekiroClient.prototype.encodeWithBase64 = function () {
        this.base64 = arguments && arguments.length > 0 && arguments[0];
    };

    SekiroClient.prototype.base64Encode = function (s) {
        if (arguments.length !== 1) {
            throw "SyntaxError: exactly one argument required";
        }

        s = String(s);
        if (s.length === 0) {
            return s;
        }

        function _get_chars(ch, y) {
            if (ch < 0x80) y.push(ch);
            else if (ch < 0x800) {
                y.push(0xc0 + ((ch >> 6) & 0x1f));
                y.push(0x80 + (ch & 0x3f));
            } else {
                y.push(0xe0 + ((ch >> 12) & 0xf));
                y.push(0x80 + ((ch >> 6) & 0x3f));
                y.push(0x80 + (ch & 0x3f));
            }
        }

        var _PADCHAR = "=",
            _ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
            _VERSION = "1.1";//Mr. Ruan fix to 1.1 to support asian char(utf8)

        //s = _encode_utf8(s);
        var i,
            b10,
            y = [],
            x = [],
            len = s.length;
        i = 0;
        while (i < len) {
            _get_chars(s.charCodeAt(i), y);
            while (y.length >= 3) {
                var ch1 = y.shift();
                var ch2 = y.shift();
                var ch3 = y.shift();
                b10 = (ch1 << 16) | (ch2 << 8) | ch3;
                x.push(_ALPHA.charAt(b10 >> 18));
                x.push(_ALPHA.charAt((b10 >> 12) & 0x3F));
                x.push(_ALPHA.charAt((b10 >> 6) & 0x3f));
                x.push(_ALPHA.charAt(b10 & 0x3f));
            }
            i++;
        }


        switch (y.length) {
            case 1:
                var ch = y.shift();
                b10 = ch << 16;
                x.push(_ALPHA.charAt(b10 >> 18) + _ALPHA.charAt((b10 >> 12) & 0x3F) + _PADCHAR + _PADCHAR);
                break;

            case 2:
                var ch1 = y.shift();
                var ch2 = y.shift();
                b10 = (ch1 << 16) | (ch2 << 8);
                x.push(_ALPHA.charAt(b10 >> 18) + _ALPHA.charAt((b10 >> 12) & 0x3F) + _ALPHA.charAt((b10 >> 6) & 0x3f) + _PADCHAR);
                break;
        }

        return x.join("");
    };

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

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

            // 创建一个标记用来判断是否创建套接字
            window.flag = true;
            var client = new SekiroClient("ws://127.0.0.1:5620/business-demo/register?group=rpc-test&clientId=" + guid());
            client.registerAction("ths", function (request, resolve, reject) {
                resolve(rt.update());
            })
        }
    }

    setTimeout(startRpc, 1000)
})()
  • 控制台出现一下页面说明注入是成功的
    在这里插入图片描述

  • python代码

import requests

data = {
    "group": "rpc-test",
    "action": "ths",
}
res = requests.get("http://127.0.0.1:5620/business-demo/invoke", params=data)
print(res.text)

2. 替换文件注入案例2
1. 逆向目标
  • 地址:https://www.zhipin.com/web/geek/job?query=python&city=101250100&page=3
  • 接口:详情页面__zp_stoken__cookie值
2. 逆向分析
  • 根据关键字定位到cookie生成的位置

在这里插入图片描述

  • 可以看出来n的赋值就是__zp_stoken__生成的位置
  • 那我们的rpc就能在这一块来进行编写,直接调用(new a).z(e, parseInt(t) + 60 * (480 + (new Date).getTimezoneOffset()) * 1e3)
  • 需要用到两个入参,一个是e一个是t,两个参数是来自这个详情页面第一次请求成功之后返回响应头里的数据信息,是服务器返回的

在这里插入图片描述

  • 那我们的就需要先把js文件进行替换,把我们rpc的代码替换到网址当中
  • python代码就需要先请求一次这个网址,获取到响应数据,在发送给接口进行加密
3. 实现代码
  • JavaScript注入代码
(function () {
    /*
      Copyright (C) 2020 virjar <virjar@virjar.com> for https://github.com/virjar/sekiro

      Redistribution and use in source and binary forms, with or without
      modification, are permitted provided that the following conditions are met:

        * Redistributions of source code must retain the above copyright
          notice, this list of conditions and the following disclaimer.
        * Redistributions in binary form must reproduce the above copyright
          notice, this list of conditions and the following disclaimer in the
          documentation and/or other materials provided with the distribution.

      THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
      AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
      IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
      ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
      DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
      (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
      LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
      ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
      (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
      THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    */


    function SekiroClient(wsURL) {
        this.wsURL = wsURL;
        this.handlers = {};
        this.socket = {};
        this.base64 = false;
        // check
        if (!wsURL) {
            throw new Error('wsURL can not be empty!!')
        }
        this.webSocketFactory = this.resolveWebSocketFactory();
        this.connect()
    }

    SekiroClient.prototype.resolveWebSocketFactory = function () {
        if (typeof window === 'object') {
            var theWebSocket = window.WebSocket ? window.WebSocket : window.MozWebSocket;
            return function (wsURL) {

                function WindowWebSocketWrapper(wsURL) {
                    this.mSocket = new theWebSocket(wsURL);
                }

                WindowWebSocketWrapper.prototype.close = function () {
                    this.mSocket.close();
                };

                WindowWebSocketWrapper.prototype.onmessage = function (onMessageFunction) {
                    this.mSocket.onmessage = onMessageFunction;
                };

                WindowWebSocketWrapper.prototype.onopen = function (onOpenFunction) {
                    this.mSocket.onopen = onOpenFunction;
                };
                WindowWebSocketWrapper.prototype.onclose = function (onCloseFunction) {
                    this.mSocket.onclose = onCloseFunction;
                };

                WindowWebSocketWrapper.prototype.send = function (message) {
                    this.mSocket.send(message);
                };

                return new WindowWebSocketWrapper(wsURL);
            }
        }
        if (typeof weex === 'object') {
            // this is weex env : https://weex.apache.org/zh/docs/modules/websockets.html
            try {
                console.log("test webSocket for weex");
                var ws = weex.requireModule('webSocket');
                console.log("find webSocket for weex:" + ws);
                return function (wsURL) {
                    try {
                        ws.close();
                    } catch (e) {
                    }
                    ws.WebSocket(wsURL, '');
                    return ws;
                }
            } catch (e) {
                console.log(e);
                //ignore
            }
        }
        //TODO support ReactNative
        if (typeof WebSocket === 'object') {
            return function (wsURL) {
                return new theWebSocket(wsURL);
            }
        }
        // weex 鍜� PC鐜鐨剋ebsocket API涓嶅畬鍏ㄤ竴鑷达紝鎵€浠ュ仛浜嗘娊璞″吋瀹�
        throw new Error("the js environment do not support websocket");
    };

    SekiroClient.prototype.connect = function () {
        console.log('sekiro: begin of connect to wsURL: ' + this.wsURL);
        var _this = this;
        // 涓峜heck close锛岃
        // if (this.socket && this.socket.readyState === 1) {
        //     this.socket.close();
        // }
        try {
            this.socket = this.webSocketFactory(this.wsURL);
        } catch (e) {
            console.log("sekiro: create connection failed,reconnect after 2s");
            setTimeout(function () {
                _this.connect()
            }, 2000)
        }

        this.socket.onmessage(function (event) {
            _this.handleSekiroRequest(event.data)
        });

        this.socket.onopen(function (event) {
            console.log('sekiro: open a sekiro client connection')
        });

        this.socket.onclose(function (event) {
            console.log('sekiro: disconnected ,reconnection after 2s');
            setTimeout(function () {
                _this.connect()
            }, 2000)
        });
    };

    SekiroClient.prototype.handleSekiroRequest = function (requestJson) {
        console.log("receive sekiro request: " + requestJson);
        var request = JSON.parse(requestJson);
        var seq = request['__sekiro_seq__'];

        if (!request['action']) {
            this.sendFailed(seq, 'need request param {action}');
            return
        }
        var action = request['action'];
        if (!this.handlers[action]) {
            this.sendFailed(seq, 'no action handler: ' + action + ' defined');
            return
        }

        var theHandler = this.handlers[action];
        var _this = this;
        try {
            theHandler(request, function (response) {
                try {
                    _this.sendSuccess(seq, response)
                } catch (e) {
                    _this.sendFailed(seq, "e:" + e);
                }
            }, function (errorMessage) {
                _this.sendFailed(seq, errorMessage)
            })
        } catch (e) {
            console.log("error: " + e);
            _this.sendFailed(seq, ":" + e);
        }
    };

    SekiroClient.prototype.sendSuccess = function (seq, response) {
        var responseJson;
        if (typeof response == 'string') {
            try {
                responseJson = JSON.parse(response);
            } catch (e) {
                responseJson = {};
                responseJson['data'] = response;
            }
        } else if (typeof response == 'object') {
            responseJson = response;
        } else {
            responseJson = {};
            responseJson['data'] = response;
        }

        if (typeof response == 'string') {
            responseJson = {};
            responseJson['data'] = response;
        }

        if (Array.isArray(responseJson)) {
            responseJson = {
                data: responseJson,
                code: 0
            }
        }

        if (responseJson['code']) {
            responseJson['code'] = 0;
        } else if (responseJson['status']) {
            responseJson['status'] = 0;
        } else {
            responseJson['status'] = 0;
        }
        responseJson['__sekiro_seq__'] = seq;
        var responseText = JSON.stringify(responseJson);
        console.log("response :" + responseText);


        if (responseText.length < 1024 * 6) {
            this.socket.send(responseText);
            return;
        }

        if (this.base64) {
            responseText = this.base64Encode(responseText)
        }

        //澶ф姤鏂囪鍒嗘浼犺緭
        var segmentSize = 1024 * 5;
        var i = 0, totalFrameIndex = Math.floor(responseText.length / segmentSize) + 1;

        for (; i < totalFrameIndex; i++) {
            var frameData = JSON.stringify({
                    __sekiro_frame_total: totalFrameIndex,
                    __sekiro_index: i,
                    __sekiro_seq__: seq,
                    __sekiro_base64: this.base64,
                    __sekiro_is_frame: true,
                    __sekiro_content: responseText.substring(i * segmentSize, (i + 1) * segmentSize)
                }
            );
            console.log("frame: " + frameData);
            this.socket.send(frameData);
        }
    };

    SekiroClient.prototype.sendFailed = function (seq, errorMessage) {
        if (typeof errorMessage != 'string') {
            errorMessage = JSON.stringify(errorMessage);
        }
        var responseJson = {};
        responseJson['message'] = errorMessage;
        responseJson['status'] = -1;
        responseJson['__sekiro_seq__'] = seq;
        var responseText = JSON.stringify(responseJson);
        console.log("sekiro: response :" + responseText);
        this.socket.send(responseText)
    };

    SekiroClient.prototype.registerAction = function (action, handler) {
        if (typeof action !== 'string') {
            throw new Error("an action must be string");
        }
        if (typeof handler !== 'function') {
            throw new Error("a handler must be function");
        }
        console.log("sekiro: register action: " + action);
        this.handlers[action] = handler;
        return this;
    };

    SekiroClient.prototype.encodeWithBase64 = function () {
        this.base64 = arguments && arguments.length > 0 && arguments[0];
    };

    SekiroClient.prototype.base64Encode = function (s) {
        if (arguments.length !== 1) {
            throw "SyntaxError: exactly one argument required";
        }

        s = String(s);
        if (s.length === 0) {
            return s;
        }

        function _get_chars(ch, y) {
            if (ch < 0x80) y.push(ch);
            else if (ch < 0x800) {
                y.push(0xc0 + ((ch >> 6) & 0x1f));
                y.push(0x80 + (ch & 0x3f));
            } else {
                y.push(0xe0 + ((ch >> 12) & 0xf));
                y.push(0x80 + ((ch >> 6) & 0x3f));
                y.push(0x80 + (ch & 0x3f));
            }
        }

        var _PADCHAR = "=",
            _ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
            _VERSION = "1.1";//Mr. Ruan fix to 1.1 to support asian char(utf8)

        //s = _encode_utf8(s);
        var i,
            b10,
            y = [],
            x = [],
            len = s.length;
        i = 0;
        while (i < len) {
            _get_chars(s.charCodeAt(i), y);
            while (y.length >= 3) {
                var ch1 = y.shift();
                var ch2 = y.shift();
                var ch3 = y.shift();
                b10 = (ch1 << 16) | (ch2 << 8) | ch3;
                x.push(_ALPHA.charAt(b10 >> 18));
                x.push(_ALPHA.charAt((b10 >> 12) & 0x3F));
                x.push(_ALPHA.charAt((b10 >> 6) & 0x3f));
                x.push(_ALPHA.charAt(b10 & 0x3f));
            }
            i++;
        }


        switch (y.length) {
            case 1:
                var ch = y.shift();
                b10 = ch << 16;
                x.push(_ALPHA.charAt(b10 >> 18) + _ALPHA.charAt((b10 >> 12) & 0x3F) + _PADCHAR + _PADCHAR);
                break;

            case 2:
                var ch1 = y.shift();
                var ch2 = y.shift();
                b10 = (ch1 << 16) | (ch2 << 8);
                x.push(_ALPHA.charAt(b10 >> 18) + _ALPHA.charAt((b10 >> 12) & 0x3F) + _ALPHA.charAt((b10 >> 6) & 0x3f) + _PADCHAR);
                break;
        }

        return x.join("");
    };
	// 上面的代码是SekiroClient生成的客户端代码代码
    function startRpc() {
        if (window.flag) {
        } else {
            function guid() {
                function S4() {
                    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
                }

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

            // 创建一个标记用来判断是否创建套接字
            window.flag = true;
            var client = new SekiroClient("ws://127.0.0.1:5620/business-demo/register?group=rpc-test&clientId=" + guid());
            client.registerAction("boss", function (request, resolve, reject) {
                e = request['seed'];
                t = request['ts']

                n = (new a).z(e, parseInt(t) + 60 * (480 + (new Date).getTimezoneOffset()) * 1e3)
                resolve(n);
            })
        }
    }

    setTimeout(startRpc, 1000)

})()
  • python连接代码
import requests
import re
from urllib import parse


def get_seed_ts():
    url = f"https://www.zhipin.com/job_detail/fa99eaf310ebb9681nZ-2tS-FldR.html?lid=20Zu96fzYW2.search.97&securityId=6ThsGuZNTR1PL-D137itvqn6an8wLyzyX2N5_41Gpap1iPd8YnCOgy3WdbjF10uUixQzOT2FuOlljwvbz7qx5-ASxQvUCq2EDr6GWMttfNLsSMsVXA~~&sessionId="
    headers = {
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
    }
    response = requests.get(url, headers=headers, allow_redirects=False)
    seed = re.findall('\?seed=(.*?)&', response.headers['location'])[0]
    seed = parse.unquote(seed)
    ts = re.findall('&ts=(.*?)&c', response.headers['location'])[0]
    return seed, ts


def get_sig():
    seed, ts = get_seed_ts()
    print(seed, ts)
    data = {
        "group": "rpc-test",
        "action": "boss",
        'seed': seed,
        'ts': ts
    }
    res = requests.post(url="http://127.0.0.1:5620/business-demo/invoke", data=data, verify=False)
    if res.status_code == 200:
        return res.json().get('data')


def get_index():
    url = 'https://www.zhipin.com/job_detail/fa99eaf310ebb9681nZ-2tS-FldR.html?lid=20Zu96fzYW2.search.97&securityId=6ThsGuZNTR1PL-D137itvqn6an8wLyzyX2N5_41Gpap1iPd8YnCOgy3WdbjF10uUixQzOT2FuOlljwvbz7qx5-ASxQvUCq2EDr6GWMttfNLsSMsVXA~~&sessionId='
    token = get_sig()
    print(token)
    headers = {
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
        "cookie": f"__zp_stoken__={token}"
    }
    res = requests.get(url, headers=headers)
    res.encoding = 'utf-8'
    print(res.text)


get_index()

3. 油猴注入形式
1. 油猴工具介绍

Tampermonkey是一款免费的浏览器扩展和最为流行的用户脚本管理器,它适用于 Chrome, Microsoft Edge, Safari, Opera Next, 和 Firefox。
以上是油猴官网给出的介绍。它可以让用户自行在添加脚本,并在开启对应页面时应用。如果你了解"脚本注入",你可以把它认为是一个给自己注入脚本的一个工具。

用户油猴脚本:https://greasyfork.org/zh-CN/scripts

大家可以自行娱乐娱乐, 比如说啊VIP电影免费破解

用油猴可以帮助我们一直去动态的监听网址,有些参数可能是刷新网站的时候生成的比较快,控制台没有那么快去执行代码

选项含义
@name脚本的名称
@namespace命名空间,用来区分相同名称的脚本,一般写作者名字或者网址就可以
@version脚本版本,油猴脚本的更新会读取这个版本号
@description描述这个脚本是干什么用的
@author编写这个脚本的作者的名字
@match从字符串的起始位置匹配正则表达式,只有匹配的网址才会执行对应的脚本,例如 * 匹配所有,https://www.baidu.com/* 匹配百度等,可以参考 Python re 模块里面的 re.match() 方法,允许多个实例
@include和 @match 类似,只有匹配的网址才会执行对应的脚本,但是 @include 不会从字符串起始位置匹配,例如 *://*baidu.com/* 匹配百度,具体区别可以参考 TamperMonkey 官方文档[8]
@icon脚本的 icon 图标
@grant指定脚本运行所需权限,如果脚本拥有相应的权限,就可以调用油猴扩展提供的 API 与浏览器进行交互。如果设置为 none 的话,则不使用沙箱环境,脚本会直接运行在网页的环境中,这时候无法使用大部分油猴扩展的 API。如果不指定的话,油猴会默认添加几个最常用的 API
@require如果脚本依赖其他 JS 库的话,可以使用 require 指令导入,在运行脚本之前先加载其它库
@run-at脚本注入时机,该选项是能不能 hook 到的关键,有五个值可选:document-start:网页开始时;document-body:body出现时;document-end:载入时或者之后执行;document-idle:载入完成后执行,默认选项;context-menu:在浏览器上下文菜单中单击该脚本时,一般将其设置为 document-start
2.逆向目标
  • 目标网址:https://www.toutiao.com/
  • 解析:_signature
3. 逆向分析
  • 通过关键字的方式定位到加密位置

在这里插入图片描述

  • 跟I方法看看执行的内容

在这里插入图片描述

  • 可以直接把当前方法导出
  • 油猴代码
// ==UserScript==
// @name        头条-rpc
// @match        https://www.toutiao.com/*
// @grant        none
// @require      http://file.virjar.com/sekiro_web_client.js?_=123
// ==/UserScript==

(function() {
    'use strict';

      var client = new SekiroClient("ws://127.0.0.1:5620/business-demo/register?group=test&clientId=" + Math.random());
  client.registerAction("tt", function (request, resolve, reject) {
        var url = request['url'];  // 接收python传的地址
        if (!url){
            reject("url 不能为空")
        }
        resolve({"signature": window.byted_acrawler.sign({url}), "cookie": document.cookie})
  });

})();
  • python代码
# encoding: utf-8
"""
@file: 头条测试.py
"""
import urllib3,requests
urllib3.disable_warnings()

def get_sig(url):
    data = {
        "group": "test",
        "action": "tt",
        "url": url
    }
    res = requests.post(url="http://127.0.0.1:5620/business-demo/invoke", data=data, verify=False)
    resp = res.json()
    print(res.json())
    if "?" in url:
        url += "&_signature={}".format(resp['signature'])
    else:
        url += "?_signature={}".format(resp['signature'])
    return url

url = get_sig("https://www.toutiao.com/api/pc/list/feed?channel_id=0&max_behot_time=1698925370&offset=0&category=pc_profile_recommend&aid=24&app_name=toutiao_web")
print(url)
headers = {
    "authority": "www.toutiao.com",
    "accept": "application/json, text/plain, */*",
    "accept-language": "zh-CN,zh;q=0.9",
    "cache-control": "no-cache",
    "pragma": "no-cache",
    "referer": "https://www.toutiao.com/",
    "sec-ch-ua": "^\\^Google",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "^\\^Windows^^",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-origin",
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
}
res = requests.get(url, headers=headers)
print(res.text)
# https://www.toutiao.com/api/pc/list/feed?channel_id=0&min_behot_time=1636703275&refresh_count=2&category=pc_profile_recommend&_signature=_02B4Z6wo00d01KWcaZwAAIDAJZ6T3JmB4wiluG0AAEwpfdsN1DmbuNsUZxKy6hQ9zmq5aoV6APEJmbKSJmmYKcV7Mr4VnVYu3tJ11y1TYvRcyhTGsiq5RdbNdsSdf1msDFZUvL.AAJ-zz4GM34
  • 油猴rpc,适用于全局的方法,要是函数内部的方法导出不了

    结语

以上就是关于js逆向技术中的RPC技术全部内容了,欢迎同学们在评论区讨论交流,有任何js逆向、数据采集相关需求也可以V后台regentwan与我联系哟~

上一篇直通车:【js逆向专题】11.AST节点树

js逆向专题传送门

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/900469.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AR模型时序预测——预测未来(含完整代码)

一、前言 随着数据科学的快速发展&#xff0c;利用自回归&#xff08;AR&#xff09;模型进行时序预测已成为一个热门话题。AR模型因其简洁有效&#xff0c;广泛应用于各类预测任务。本文将介绍一套基于Matlab的AR模型时序预测代码&#xff0c;重点在于如何通过历史数据预测未…

工业相机详解及选型

工业相机相对于传统的民用相机而言&#xff0c;具有搞图像稳定性,传输能力和高抗干扰能力等&#xff0c;目前市面上的工业相机大多数是基于CCD&#xff08;Charge Coupled Device)或CMOS(Complementary Metal Oxide Semiconductor)芯片的相机。 一&#xff0c;工业相机的分类 …

爬虫+数据保存

爬虫以及数据保存 这篇文章, 分享如何将爬虫爬到的数据, 保存到excel表格当中。 文章目录 1.安装保存数据的第三方库openpyxl并使用 2.爬虫加单表数据保存 3.爬虫加多表数据保存 4.实战 一、安装保存数据的第三方库openpyxl并使用 我们需要安装openpyxl的第三方库 安装…

01 springboot-整合日志(logback-config.xml)

logback-config.xml 是一个用于配置 Logback 日志框架的 XML 文件&#xff0c;通常位于项目的 classpath 下的根目录或者 src/main/resources 目录下。 Logback 提供了丰富的配置选项&#xff0c;可以满足各种不同的日志需求。需要根据具体情况进行配置。 项目创建&#xff0…

打造充电场站:场地选择与合规运营详解

建设一座充电站需要六步流程&#xff1a;准备工作 → 备案 → 土地审核 → 规划审核 → 电力申请 → 验收确认 一、准备工作 在确定建设前&#xff0c;要考察待选的场地&#xff0c;例如空地、停车场等&#xff0c;与场地所有方签订充电站建设合作协议。根据场地和车流量等实际…

用docker Desktop 下载使用thingsboard/tb-gateway

1、因为正常的docker pull thingsboard/tb-gateway 国内不行了&#xff0c;所以需要其它工具来下载 2、在win下用powershell管理员下运行 docker search thingsboard/tb-gateway 可以访问到了 docker pull thingsboard/tb-gateway就可以下载了 3、docker Desktop就可以看到…

铲屎官进!双十一宠物空气净化器买哪款,有什么推荐的吗?

害&#xff0c;一到换毛季&#xff0c;真的顶不顺&#xff01;家里两只布偶疯狂掉毛&#xff0c;地板、衣服上这些常规的地方就不用说了&#xff0c;竟然连水杯旁也有浮毛的存在&#xff0c;被我不小心喝进去好几次&#xff0c;最严重的时候已经猫毛拌饭了。 我寻求了很多解决方…

jQuery:动画 节点

jQuery&#xff1a;动画 & 节点 定位获取位置滚动距离 动画显示隐藏淡入淡出展开收起动画队列自定义动画动画回调函数动画延迟 节点插入节点删除节点 定位 获取位置 jquery提供了两个方法&#xff0c;来获取元素所处的位置&#xff1a; // 取值 jQuery对象.offset() // …

【JVM】—深入理解ZGC回收器—背景概念回收流程

深入理解ZGC回收器—背景概念&回收流程 ⭐⭐⭐⭐⭐⭐ Github主页&#x1f449;https://github.com/A-BigTree 笔记链接&#x1f449;https://github.com/A-BigTree/Code_Learning ⭐⭐⭐⭐⭐⭐ 如果可以&#xff0c;麻烦各位看官顺手点个star~&#x1f60a; 文章目录 深入…

采集QQ群成员的过程中遇到的问题

错误思路一&#xff1a;通过抓取windows的QQ软件来获取QQ成员 难点&#xff1a;通过spy获取不到节点和句柄 正确思路&#xff1a;通过抓取手机版本的QQ来获取QQ成员 用到的开发工具 开维控制精灵 按键精灵助手 查找节点 有自带的函数,比如cs控件类cs.id 能提取所有节点js…

基于KV260的基础视频链路通路(MIPI+Demosaic+VDMA)

目录 1. 简介 1.1 要点 1.2 背景 1.2.1 Got stuck 1.2.2 Cant be Initialized 2. Overlay 2.1 参考 Overlay 2.1.1 KV260 Base 2.1.2 Pynq-CV-OV5640 2.2 自建 Overlay 2.2.1 IIC IP 2.2.2 MIPI CSI-2 Rx 2.2.3 AXI4-S Subset 2.2.4 Demosaic 2.2.5 Pixel Pack …

非个人小程序注册材料及认证流程

一、注册材料 1、 电子邮箱A、 未被微信公众平台注册B、 未被微信开放平台注册C、 未被个人微信号绑定过&#xff0c;如果被绑定了需要解绑 或 使用其他邮箱&#xff08;如已被占用建议找回账号登录或换邮箱注册&#xff09;2、 管理员手机号码3、 管理员个人身份证&#xff08…

小程序云开发CMS新版数据模型讲解,可视化网页管理后台,内容管理对数据库进行增删改查操作,新闻小程序实战学习

一直跟着石头哥学习小程序开发的同学比较清楚cms是什么&#xff0c;cms就是可以进行可视化的管理云开发数据库的网页后台。有了cms我们可以很方便的管理云开发数据库。 但是云开发官方一直改版&#xff0c;所以现在cms功能被整合到了云开发的数据模型里&#xff0c;也就是现在想…

opencv 图像翻转- python 实现

在做图像数据增强时会经常用到图像翻转操作 flip。 具体代码实现如下&#xff1a; #-*-coding:utf-8-*- # date:2021-03 # Author: DataBall - XIAN # Function: 图像翻转import cv2 # 导入OpenCV库path test.jpgimg cv2.imread(path)# 读取图片 cv2.namedWindow(image,1) …

第十一章 TypeScript模块和命名空间的介绍和使用

文章目录 一、模块1. 导出基础导出重新导出导出重命名 2. 导入基础导入导入重命名 3. 默认导出4. 模块化兼容exports import require()编译结果 二、命名空间1. 例子2. 命名空间3. 引入命名空间 三、模块和命名空间 一、模块 JavaScript 在 ES2015 中引入了模块的概念&#x…

【331】基于Springboot的“有光”摄影分享网站系统

“有光”摄影分享网站设计与实现 摘 要 自互联网的发展至今&#xff0c;其基础理论与技术都已完善&#xff0c;并积极参与了整个社会各个领域。它容许信息根据媒体传播&#xff0c;并和信息可视化工具一起为大家提供优质的服务。对于信息多头管理、差错率高、信息安全系数差、…

【GAMES101笔记速查——Lecture 18 Advanced Topics in Rendering】

目录 1 渲染前沿 1.1 有偏vs无偏 1.2 无偏光线传播方法&#xff08;Unbiased light transport methods&#xff09; 1.2.1 双向路径追踪&#xff08;Bidirectional path tracing&#xff0c;BDPT&#xff09; &#xff08;1&#xff09;双向路径追踪(BDPT)举例 1.2.2 Metr…

《等保测评新视角:安全与发展的双赢之道》

在数字化转型的浪潮中&#xff0c;企业面临的不仅是技术革新的挑战&#xff0c;更有信息安全的严峻考验。等保测评&#xff0c;作为国家网络安全等级保护的一项重要措施&#xff0c;不仅为企业的安全护航&#xff0c;更成为推动企业高质量发展的新引擎。本文将从全新的视角&…

中航资本:光伏股,集体涨停!千亿龙头,罕见封板!

今日早盘&#xff0c;A股放量走强&#xff0c;半日成交超越万亿元。北证50指数持续放量上攻&#xff0c;飙升逾8%&#xff0c;再创前史新高&#xff0c;创业板指大涨逾3%&#xff0c;克复2200点&#xff0c;上证指数站上3300点。 盘面上&#xff0c;BC电池、固态电池、房地产、…