php開發的js逆向rpc框架

失色天空發表於2022-04-25

一、前言

我們在寫爬蟲的時候,經常會遇到各種加密的引數,只能在瀏覽器打斷點然後一步一步的去追程式碼,想辦法把加密部分的js給扣出來在外部呼叫。這個過程漫長且費力,還需要一定的ast能力,容易把人勸退。所以現在有一種思想,直接在外部呼叫瀏覽器內已經生成的函式獲取結果,這就是所謂的js-rpc遠端呼叫。

在白度搜尋js-rpc框架,有python和go實現的,所以我如法炮製,使用php基於websocket開發了一個,取名shermie,我個人覺得使用起來更方便一些,先上效果圖:

  • 服務端

  • 瀏覽器

  • 訪問

  • 程式碼

二、使用方法

  • 啟動服務

進入bin目錄,在命令列執行下面的命令

./cli.exe Websocket.php
  • 瀏覽器執行

1.在瀏覽器建立Websocket連線(把Websocket.js裡面的複製出來貼上到瀏覽器命令列執行),會返回一個client物件

2.在client物件上註冊需要呼叫的js函式

# 假設我們需要通過http呼叫btoa這個函式,第一個引數隨便命名,第二個引數是函式執行的內容
client.registeCall("btoa",function(params){
    return btoa(params);
});

# 會輸出一個訪問地址,比如這樣

[2022/4/24 18:16:01][info]  連線到伺服器成功
> client.registeCall("btoa",function(params){
    return window.btoa(params);
});
[2022/4/24 18:16:52][info]  註冊函式btoa成功
[2022/4/24 18:16:52][info]  訪問地址:http://127.0.0.1:9501?group=ef8d3da2-dca4-4236-ba99-82f76a5e1901&action=btoa&input=

# 引數說明
group:客戶端分組ID(不用管)

action:註冊的需要呼叫的函式(不用管)

input:呼叫這個函式傳入的引數(需要輸入)
  • 訪問地址獲取結果
    訪問上面列印的地址,並傳入自定義引數:http://127.0.0.1:9501?group=df777a58-ff44-41bb-81ce-935b6bea9c25&action=btoa&input="hh"最終返回的就是:window.btoa(“hh”)執行的結果

三、說明

github不能上傳超過100m的檔案,所以只能貼一個外部下載連結了
share.weiyun.com/xKKhm1MC

  • Websocket.js

    const host = "ws://127.0.0.1:9501";
    function WebsocketClient(host) {
      this.ws = null;
      this.uniqueId = null;
      this.registeCallMap = {};
      this.timer = -1;
      // 入口函式
      this.start = function () {
          this.logo()
          this.uniqueId = this.uuid();
          this.ws = new WebSocket(host);
          this.ws.onopen = this.open();
          this.ws.onmessage = this.message();
          this.ws.onclose = this.close();
          return this;
      }
      // 心跳函式
      this.heartbeat = function () {
          let _this = this;
          let post = {"msg": "success", "data": "ping", "code": 200, "uuid": this.uniqueId};
          if (this.ws.readyState === WebSocket.OPEN) {
              this.timer = setInterval(function () {
                  _this.ws.send(JSON.stringify(post));
              }, 60000);
          }
          return this;
      }
      // 列印logo
      this.logo = function () {
          let logo = `%c 
    ______     __  __     ______     ______     __    __     __     ______    
    /\\  ___\\   /\\ \\_\\ \\   /\\  ___\\   /\\  == \\   /\\ "-./  \\   /\\ \\   /\\  ___\\   
    \\ \\___  \\  \\ \\  __ \\  \\ \\  __\\   \\ \\  __<   \\ \\ \\-./\\ \\  \\ \\ \\  \\ \\  __\\   
    \\/\\_____\\  \\ \\_\\ \\_\\  \\ \\_____\\  \\ \\_\\ \\_\\  \\ \\_\\ \\ \\_\\  \\ \\_\\  \\ \\_____\\ 
    \\/_____/   \\/_/\\/_/   \\/_____/   \\/_/ /_/   \\/_/  \\/_/   \\/_/   \\/_____/`
          console.log(logo, "color:blue;")
      }
      // 回撥函式
      this.open = function () {
          let _this = this;
          return function (event) {
              _this.log("info", "連線到伺服器成功");
              // 心跳定時器
              _this.heartbeat();
              // 向伺服器傳送訊息
              _this.sendSuccess(null);
          }
      }
      // 回撥函式
      this.message = function () {
          let _this = this;
          return function (event) {
              let receive = event.data;
              _this.log("info", "收到伺服器傳送訊息:" + receive);
              // 解析分組和函式引數
              receive = JSON.parse(receive);
              const {group, action, input} = receive.data;
              if (_this.uniqueId !== group) {
                  _this.sendError("客戶端分組不存在");
                  return;
              }
              if (!_this.registeCallMap[group][action]) {
                  _this.sendError("呼叫函式未註冊");
                  return;
              }
              // 呼叫函式
              try {
                  let result = _this.registeCallMap[group][action](input);
                  _this.log("info", `呼叫函式${action}返回值:${result}`);
                  if (result === undefined) {
                      result = null;
                  }
                  _this.sendSuccess({result: result, input: input});
              } catch (e) {
                  _this.log("error", "呼叫函式報錯:" + e.message);
                  _this.sendError("呼叫函式報錯:" + e.message);
              }
          }
      }
      // 回撥函式
      this.close = function () {
          let _this = this;
          return function (event) {
              clearInterval(_this.timer)
              _this.log("info", "客戶端斷開連線");
          }
      }
      // 生成客戶端id
      this.uuid = function () {
          return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
              var r = Math.random() * 16 | 0,
                  v = c === 'x' ? r : (r & 0x3 | 0x8);
              return v.toString(16);
          });
      }
      // 日誌函式
      this.log = function (type, msg) {
          let datetime = (new Date()).toLocaleString()
          let color = type.toLowerCase() === "info" ? "green" : "red";
          console.log(`[${datetime}][${type}] %c ${msg}`, `color:${color};`)
      }
      // 註冊呼叫函式
      this.registeCall = function (callName, callback) {
          this.log("info", `註冊函式${callName}成功`);
          // 儲存傳入的函式
          this.registeCallMap[this.uniqueId] = {
              [callName]: callback
          };
          // 訪問地址
          this.log("info", `訪問地址:${host.replace("ws", "http")}?group=${this.uniqueId}&action=${callName}&input=`)
      }
      this.sendError = function (msg) {
          let post = {"msg": msg, "data": msg, "code": 9999, "uuid": this.uniqueId};
          this.ws.send(JSON.stringify(post));
      }
      this.sendSuccess = function (data) {
          let post = {"msg": "success", "data": data, "code": 200, "uuid": this.uniqueId};
          this.ws.send(JSON.stringify(post));
      }
    }
    let client = (new WebsocketClient(host)).start();
  • Websocket.php

    <?php
    use Swoole\Http\Request;
    use Swoole\Http\Response;
    use Swoole\Table;
    use Swoole\WebSocket\Frame;
    use Swoole\WebSocket\Server;
    /**
    * Class WebsocketServer
    */
    class WebsocketServer
    {
      /**
       * @var int
       */
      public int $port = 9501;
      /**
       * @var Server|null
       */
      public ?Server $server = null;
      /**
       * @var Table|null
       */
      public ?Table $group = null;
    
      /**
       *
       */
      const  OPEN_EVENT = "open";
      /**
       *
       */
      const  REQU_EVENT = "request";
      /**
       *
       */
      const  START_EVENT = "start";
      /**
       *
       */
      const  MSG_EVENT = "message";
      /**
       *
       */
      const  CLS_EVENT = "close";
    
      /**
       * WebsocketServer constructor.
       */
      public function __construct() {
          $this->server = new Swoole\WebSocket\Server("0.0.0.0", $this->port);
          $this->server->set([
              "worker_num"               => 2,
              "heartbeat_check_interval" => 60,
              "heartbeat_idle_time"      => 600,
          ]);
          $table = new Table(1024);
          $table->column("fd", Table::TYPE_STRING, 1024);
          $table->column("data", Table::TYPE_STRING, 1024);
          $table->create();
          $this->group = $table;
      }
    
      /**
       *
       */
      public function registeEvent() {
          $this->server->on(self::START_EVENT, [$this, 'start']);
          $this->server->on(self::REQU_EVENT, [$this, 'request']);
          $this->server->on(self::OPEN_EVENT, [$this, 'open']);
          $this->server->on(self::MSG_EVENT, [$this, 'message']);
          $this->server->on(self::CLS_EVENT, [$this, 'close']);
      }
    
      /**
       * @param Server $server
       */
      public function start(Server $server) {
          $log = <<<EOF
    ______     __  __     ______     ______     __    __     __     ______    
    /\  ___\   /\ \_\ \   /\  ___\   /\  == \   /\ "-./  \   /\ \   /\  ___\   
    \ \___  \  \ \  __ \  \ \  __\   \ \  __<   \ \ \-./\ \  \ \ \  \ \  __\   
    \/\_____\  \ \_\ \_\  \ \_____\  \ \_\ \_\  \ \_\ \ \_\  \ \_\  \ \_____\ 
    \/_____/   \/_/\/_/   \/_____/   \/_/ /_/   \/_/  \/_/   \/_/   \/_____/ 
    EOF;
          $this->log(self::START_EVENT, $log);
          $this->log(self::START_EVENT, "server listen at 0.0.0.0:{$this->port}");
      }
    
      /**
       * @param Server $server
       * @param Request $request
       */
      public function open(Server $server, Request $request) {
          $this->log(self::OPEN_EVENT, "new websocket client connect {$request->fd}");
      }
    
      /**
       * @param Server $server
       * @param Frame $frame
       */
      public function message(Server $server, Frame $frame) {
          $this->log(self::MSG_EVENT, "receive data from websocket client {$frame->fd}:" . $frame->data);
          $data = json_decode($frame->data, true);
          $this->group->set($data["uuid"], ["fd" => $frame->fd, "data" => json_encode($data, 256)]);
      }
    
      /**
       * @param Server $server
       * @param int $fd
       */
      public function close(Server $server, int $fd) {
          // TODO 刪除記憶體表中的客戶端的fd
          $this->log(self::CLS_EVENT, "server close websocket client {$fd}");
      }
    
      /**
       * @param Request $request
       * @param Response $response
       */
      public function request(Swoole\Http\Request $request, Swoole\Http\Response $response) {
          // 獲取請求引數
          $server = $request->server;
          $method = strtoupper($server["request_method"]);
          $path   = $server["request_uri"];
          $remote = $server["remote_addr"];
          $port   = $server["remote_port"];
          $params = $request->get;
          $this->log(self::REQU_EVENT, "$remote:$port $method $path " . json_encode($params, JSON_UNESCAPED_UNICODE));
          $response->header("Content-Type", "application/json;charset=utf-8");
          // 分組引數
          $group = $params["group"] ?? "";
          if (!$group) {
              $data = ["code" => 9999, "data" => "客戶端分組引數不能為空", "status" => "success"];
              $response->end(json_encode($data, 256));
              return;
          }
          // 呼叫函式式
          $action = $params["action"] ?? "";
          if (!$action) {
              $data = ["code" => 9999, "data" => "呼叫方法引數不能為空", "status" => "success"];
              $response->end(json_encode($data, 256));
              return;
          }
          // 傳入引數
          $input = $params["input"] ?? "";
          if (!$input) {
              $data = ["code" => 9999, "data" => "呼叫方法傳入引數不能為空", "status" => "success"];
              $response->end(json_encode($data, 256));
              return;
          }
          // 建立連線或者傳送資料
          if (!($this->group->get($group) ?? null)) {
              $data = ["code" => 9999, "data" => "當前分組內不存在已連線的客戶端,傳送資料失敗", "status" => "success"];
              $response->end(json_encode($data, 256));
              return;
          }
          $fd = $this->group->get($group)["fd"];
          // 判斷連線是否可用
          if (!$this->server->exist($fd) || !$this->server->isEstablished($fd)) {
              $data = ["code" => 9999, "data" => "當前分組客戶端連線已超時斷開,傳送資料失敗", "status" => "success"];
              $response->end(json_encode($data, 256));
              return;
          }
          $data = ["code" => 9999, "data" => $params, "status" => "success"];
          $this->server->push($fd, json_encode($data, 256), WEBSOCKET_OPCODE_TEXT, true);
          while (true) {
              $receiveData = json_decode($this->group->get($group)["data"], true);
              usleep(200);
              if ($receiveData["data"] ?? null) {
                  $wsData = $receiveData["data"] ?? null;
                  break;
              }
          }
          // 清空資料
          $this->group->set($group, ["fd" => $fd, "data" => null]);
          $result = ["code" => 200, "data" => $wsData, "status" => "success"];
          $response->end(json_encode($result, 256));
      }
    
      /**
       *
       */
      public function run() {
          $this->registeEvent();
          $this->server->start();
      }
    
      /**
       * @param string $event
       * @param string $msg
       */
      private function log(string $event, string $msg) {
          $msg = sprintf("[%s][$event]:%s" . PHP_EOL, (new DateTime())->format("Y-m-d Hs"), $msg);
          fwrite(STDOUT, $msg);
          fflush(STDOUT);
      }
    }
    (new WebsocketServer())->run();
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章