JS 加密秒殺方案——遠程方法調用 JSRPC-超詳細-

在滲透測試或安全評估中,我們對前端 js 代碼的關注度是很高的,因爲 JS 代碼中會經常包含一些接口、前端路由、敏感信息和請求參數的加解密邏輯。爲防止攻擊者篡改數據包,開發者常在前端對請求數據進行加密 (比如登錄密碼、接口的參數 sign、_signature、一些 cookie 等) 或者 對返回包信息解密,這使得我們的請求或者返回包的修改和查看變得困難,JS 逆向本就是一件費時間的事情,這時候小天向大家介紹一種技術:JSRPC , 它可以使得這一情況迎刃而解。

遠程方法調用概述

RPC(Remote Procedure Call,遠程過程調用)是一種通過網絡讓程序調用另一臺計算機上的服務或程序的技術。它允許程序在不同的地址空間(通常是不同的計算機)之間調用函數或方法 ,而 JSRPC 則屬於在 JavaScript 環境中的 RPC 技術的實現。

爲什麼要推薦這個技術?

在前端當中經常出現 JS 加密的,這時候我們使用 JSRPC 技術則不需要關心他加密函數的具體實現方法就可以做到直接調用加密函數得到結果。

簡單舉個例子說明:比如請求中的加密參數 sign 由加密函數a通過一系列信息(如搜索內容、時間戳等)生成。

一般情況下,我們會通過扣代碼、補環境、還原加解密算法等方式解決,這樣實現如果在加密邏輯很複雜同時又有很多的環境檢測或者調用的情況下就比較費時間或者難以完成。這時候如果我們使用 JSRPC 技術可以使我們直接調用加密函數 a 來實現請求的加密,就像是本地函數調用一樣,而 a 是我們寫好的函數只需我們傳個參數就可以有返回值。

協議介紹

因爲 JSRPC 一般都是基於 WebSocket 或者 WebSocket Secure 協議實現的,在這裏介紹一下這倆協議:

WebSocket 是基於 TCP 的應用層協議,WSS(WebSocket Secure)是 WebSocket 的加密版本 ,WS 和 WSS 的關係類似與 http 和 https,他們採用的是雙向通信模式,類似以根兩端開口的管道,當客戶端和服務器之間簡歷連接後,不論是客戶端還是服務端都可以隨時發送數據給對方,WebSocket 協議請求 url 爲 ws:// 開頭,WebSocket Secure 請求協議則爲 wss:// 開頭,並且通常每隔一段時間需要發送心跳包維持長連接 (心跳包可以是一個簡單的 Ping 消息)。

一般再社交聊天室、股票實時報價、直播間信息流等場景中會存在。

大致思路

思路簡明如下:

  1. 服務端
  1. 客戶端
  1. 代碼注入
  1. 數據交互

案例實戰

這次我們拿某團登錄來舉例,大家練練手

地址 :aHR0cHM6Ly9wYXNzcG9ydC5tZWl0dWFuLmNvbS9hY2NvdW50L3VuaXRpdmVsb2dpbg==

初步分析

輸入手機號,密碼,抓包對比每次的請求

其中 uuid、token_id、csrf 在頁面源碼中都是存在的,直接 xpath、正則、css 選擇器啥的獲取到就 OK。

多次嘗試之後發現需要處理的值有兩個 password 和 h5Fingerprint,而這裏的 password 的加密實現很簡單,而 h5Fingerprint 這個很長很長的值就是一個 h5 頁面的指紋,我們這個值用 JSRPC 實現加密,對於 password 這個好欺負的值我們還是老規矩,先猜一下。

首先我們計算一下他的比特位,因爲 password 最終是 base64 編碼形式,這裏先給大家說一下 base64 的一些相關知識 (在上一篇文章也提到了):

1.base64 每 4 個字符代表 3 個編碼前數據的原始字節

2. 如果原始數據的字節數不是 3 的倍數,那麼最後一批數據將被填充,以確保 Base64 編碼的輸出是 4 字符的倍數。

3. 一個等號表示原始數據最後多了 1 個字節(即原始數據總字節數除以 3 餘 1‍)兩個等號表示原始數據最後多了 2 個字節(即原始數據總字節數除以 3 餘 2)。

計算如下,所以我們可以得出這一串加密密碼的長度是 1024 個比特位

結合我每次輸入的密碼都是不變的情況下,password 的值每次都在變且都固定爲 1024 位,說明很可能是密鑰長度爲 1024 位的 RSA 加密,又因爲最後結果是 base64 編碼形式所以大概率是 PKCS#1 的填充模式,同時單純的 RSA 算法無法加密大量文本,當我們在密碼框輸入很多的數據抓包如下:

可以看到在輸入大量的數據情況下,請求正常發送但是 password 直接變爲了 false,說明幾乎確定了加密方式爲 RSA 加密且未魔改。

驗證猜測

我們直接搜索 setPublicKey(rsa 加密的關鍵字),可以看到以下結果

然後我們點進去倒數第二個很容易發現 h5Fingerprint 和 password 的加密函數,如下所示。

或者我們不猜,直接搜索password =也可以找到 password 和 h5Fingerprint 加密的位置,這樣還更快 (JS 常見的加密關鍵字 encrypt),如下:

直接在 encrypt.setPublicKey 方法運行處打個斷點就能找到 PublicKey,同時也看到了 encrypt 是 JSEncrypt 的對象, 而 JSEncrypt 又是一個用於在 JavaScript 中實現 RSA 加密的庫 ,公鑰如下。

MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCRD8YahHualjGxPMzeIWnAqVGMI rWrrkr5L7gw+5XT55iIuYXZYLaUFMTOD9iSyfKlL9mvD3ReUX6Lieph3ajJAPPGEu SHwoj5PN1UiQXK3wzAPKcpwrrA2V4Agu1/RZsyIuzboXgcPexyUYxYUTJH48DeYBG Je2GrYtsmzuIu6QIDAQAB

簡單單步跟進就能發現填充方法 PKCS#1

有了公鑰和填充方法要實現一個單純的 RSA 加密很簡單,這裏就不再演示了,無論是用 python 還是直接使用 JS 都可以,大家直接 GPT 即可,這裏重點說一下通過 JSRPC 生成 h5Fingerprint 的結果。

手動實現

客戶端

首先我們通過本地替換的方法來實現,我們在對應存在 h5Fingerprint 加密方法的 JS 代碼的頁面當中點擊鼠標右鍵選擇替換內容。

上方會彈出來一個這個提示,我們點擊選擇文件夾。

選擇好之後上方會詢問是否允許本地替換,我們點擊允許,這樣我們就可以在本地修改他的 JS 代碼了,使得我們可以自定義他的運行邏輯 (也可以用以下工具實現FiddlerMitmproxy谷歌的GRPC協議瀏覽器插件 ReRes)

然後我們在 h5Fingerprint 的加密下方插入客戶端的代碼,爲了防止污染環境這裏改爲了自執行函數

注意點:這裏因爲在當前作用域所以我們直接調用,否則需要將方法提升到全局例如賦給 window。

簡單的客戶端的模板寫法:

(function() {
  if (window.flagSkyx) return;
  try {
    var ws = new WebSocket("ws://127.0.0.1:1234");
    window.flagSkyx = true;
    ws.onmessage = function(param) {
      console.log("接受到參數: " + param.data);
      if (param.data === 'exit') {
        ws.close(); // 關閉 WebSocket
      } else {
        // 自定義代碼邏輯
        ws.send(aa(param.data)); // 使用 aa 函數處理參數併發送
      }
    };
  } catch {
    console.log("連接出錯");
    window.flagSkyx = false; // 連接錯誤時設置爲 false
  }
})();

服務端

因爲加密函數的參數爲window.location.origin + url 所以我們需要先構造一下這個值,這個 url 很簡單,其實就是一個登錄的 url 後跟了一些當前頁面的固定參數,我們這裏就先通過 copy 方法來直接複製這個值用來後續測試 (這個值在當前頁面中是固定的,除非是刷新之後會導致變化):

https://passport.meituan.com/account/unitivelogin?risk_partner=-1&risk_platform=1&risk_app=-1&joinkey=&uuid=4aa6a78d699f4973ba3b.1728630140.1.0.0&token_id=DNCmLoBpSbBD6leXFdqIxA&service=www&continue=https%3A%2F%2Fwww.meituan.com%2Faccount%2Fsettoken%3Fcontinue%3Dhttps%253A%252F%252Fwww.meituan.com

我們將以下的代碼粘貼到 python 當中運行,這裏用作示範,我們實際當中可以直接使用框架、工具即可快速構造服務端和客戶端,不用自己寫這些代碼

import asyncio
import websockets
async def receive_message(websocket, path):
    try:
        while True:
            send_text = input("請輸入要加密的字符串: ")
            if send_text.lower() == "exit":
                await websocket.send(send_text)
                break
            await websocket.send(send_text)
            response_text = await websocket.recv()
            print("\n加密結果:", response_text)
    except websockets.exceptions.ConnectionClosed:
        print("連接已關閉。")
    except Exception as e:
        print(f"發生錯誤: {e}")
async def main():
    async with websockets.serve(receive_message, '127.0.0.1', 1234):
        print("WebSocket 服務器已啓動,等待連接...")
        await asyncio.Future()  # 保持服務器運行
if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n服務器已停止。")

**我們先運行服務端!然後手動觸發一次登錄操作,觸發一次登錄操作!(保證註冊連接觸發)**即可連接成功,在 python 中運行如下,可以看到成功調用了前端加密函數並且得到了結果。

自己寫太費事了,而且我們如果想將服務端變爲 api 的形式部署到服務器的話還需自己優化接口,同時考慮很多情況:比如有的網站只支持 wss 怎麼辦?如果有 CSP 策略怎麼辦?如果有多個加密函數需要提供 rpc 實現怎麼辦?...,這時候小天向大家推薦兩款來解決這些問題,如下所示。

JsRpc 工具的使用

部署方法

JsRpc 是一款十分優秀的 go 語言工具,如今已經 1.2k 的 star,它爲 JS 逆向和滲透測試提供了 JSRPC 調用的很大的便捷。

項目地址:https://github.com/jxhczhl/JsRpc

我們下載編譯的版本,同時將配置文件 config.yaml 放到同一目錄下面

下方的目錄是默認設置,如果需要 https/wss 服務則需要從阿里雲、騰訊雲等申請免費的 https 證書。

客戶端代碼初始化代碼(JsEnv_Dev.js)插入到瀏覽器環境,可以在頁面打開可以在頁面打開的時候就先注入環境(初始化代碼是固定的),也可以和**接口註冊(紅框框起來的)**一起插入到瀏覽器。

接口註冊代碼如下, 客戶端初始化代碼就是 JsEnv_Dev.js 文件裏面的,是固定的

// 注入環境後連接通信
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=test");
// 可選  
//var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=zzz&clientId=hliang/"+new Date().getTime())
demo.regAction("getH5fingerprint", function (resolve,param) {
    //這樣添加了一個param參數,http接口帶上它,這裏就能獲得 aa是加密參數
    resolve(aa(param));
})

詳細解釋

  1. zzz 是分組,註冊連接的時候隨便寫,調用接口的時候要對應,這個就是起到區分的作用

  2. ws/wss 代表是註冊連接的接口,調用的則將其改爲 go

  3. hello2  是調用接口時候的 action 參數值

  4. param 就是調用接口所傳遞的參數 多個參數:param['aa'],param['bb'] 多參數的時候可以用 json 格式傳遞 ,項目文檔中的示例如下:

實戰演示

我們演示分開插入的方法,這兩個工具的原理是類似的我們在下一個工具結合本地替換演示註冊和初始化一起插入的方法

首先運行服務端程序

然後打開登錄頁面,直接將客戶端初始化代碼輸入到控制檯然後回車(下面的輸出不用管)

然後在加密處下個斷點。

點擊登錄觸發斷點, 在控制檯輸入window.H5Fingerprint=utility.getH5fingerprint將方法提升到全局

然後取消斷點,繼續運行,然後在瀏覽器控制檯輸入以下代碼:

var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=meituan");
demo.regAction("H5fingerprint", function (resolve,param) {
    resolve(window.H5Fingerprint(param));
})

顯示 rpc 連接成功

服務端也看到了連接成功的客戶端 id

此時訪問接口,已經有了加密後的 data 數據

http://127.0.0.1:12080/go?group=meituan&action=H5fingerprint¶m=123

所以現在我們可以直接通過接口調用即可獲得加密後的數據,我們後續可以通過 python 自動獲取同時結合 Mitmproxy +burpsuite 進行滲透測試,當然如何在 burpsuite 利用 js 逆向的結果在後續的文章中小天會爲大家總結,這裏就先不贅述了。

常見問題

  1. websocket 連接失敗,內容安全策略

  這個網站不讓連接 websocket,可以用油猴注入使用,或者更改網頁響應頭

  1. 異步操作獲取值

  參考

  1. 只允許 wss 連接需要下載 ca 證書,從騰訊雲或者阿里雲 申請免費的 https 證書,要. pem 和. key 文件的,然後放到目錄裏,再在配置文件裏配置後啓動即可

3.  記得將方法提升到全局之後放開斷點

Sekiro 框架的使用

框架介紹

JsRpc 是用 go 語言寫的專門爲 JS 逆向做的項目,而 sekiro 是由鄧維佳(渣總)寫的一個基於長鏈接和代碼注入的 API 服務暴露框架,通用性更強,**它可以在 APP 逆向、JS 逆向、**Android 羣控、app 爬蟲等場景使用,同時提供了市面上主流編程語言的客戶端 demo 以及十分完善的中文使用文檔

官方文檔:https://sekiro.iinti.cn/sekiro-doc/

部署方法

首先先在以下的 url 下載服務端程序

https://oss.iinti.cn/sekiro/sekiro-demo

需要有 java 環境,需要 JDK 版本在 1.8 以上。

下載好之後解壓,打開 bin 文件夾如下所示,以下就是啓動我們 PRC 服務端的腳本。

Linux 就運行. sh,windows 系統則運行. bat,本機是 windows 系統,我們需要先啓動服務端運行效果如下:

連接端口的更改在 conf 文件夾下的 config.properties 文件當中。

需要將以下代碼插入到瀏覽器 (官網也有),然後觸發頁面的功能點即可連接,藍框的是客戶端初始化代碼可以在頁面打開的時候直接插入瀏覽器運行也可以和後面紅框代碼一起運行,重點是紅框的事件註冊和建立 ws/wss 連接的代碼。

詳細代碼如下:

function SekiroClient(e){if(this.wsURL=e,this.handlers={},this.socket={},!e)throw new Error("wsURL can not be empty!!");this.webSocketFactory=this.resolveWebSocketFactory(),this.connect()}SekiroClient.prototype.resolveWebSocketFactory=function(){if("object"==typeof window){var e=window.WebSocket?window.WebSocket:window.MozWebSocket;return function(o){function t(o){this.mSocket=new e(o)}return t.prototype.close=function(){this.mSocket.close()},t.prototype.onmessage=function(e){this.mSocket.onmessage=e},t.prototype.onopen=function(e){this.mSocket.onopen=e},t.prototype.onclose=function(e){this.mSocket.onclose=e},t.prototype.send=function(e){this.mSocket.send(e)},new t(o)}}if("object"==typeof weex)try{console.log("test webSocket for weex");var o=weex.requireModule("webSocket");return console.log("find webSocket for weex:"+o),function(e){try{o.close()}catch(e){}return o.WebSocket(e,""),o}}catch(e){console.log(e)}if("object"==typeof WebSocket)return function(o){return new e(o)};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 e=this;try{this.socket=this.webSocketFactory(this.wsURL)}catch(o){return console.log("sekiro: create connection failed,reconnect after 2s:"+o),void setTimeout(function(){e.connect()},2e3)}this.socket.onmessage(function(o){e.handleSekiroRequest(o.data)}),this.socket.onopen(function(e){console.log("sekiro: open a sekiro client connection")}),this.socket.onclose(function(o){console.log("sekiro: disconnected ,reconnection after 2s"),setTimeout(function(){e.connect()},2e3)})},SekiroClient.prototype.handleSekiroRequest=function(e){console.log("receive sekiro request: "+e);var o=JSON.parse(e),t=o.__sekiro_seq__;if(o.action){var n=o.action;if(this.handlers[n]){var s=this.handlers[n],i=this;try{s(o,function(e){try{i.sendSuccess(t,e)}catch(e){i.sendFailed(t,"e:"+e)}},function(e){i.sendFailed(t,e)})}catch(e){console.log("error: "+e),i.sendFailed(t,":"+e)}}else this.sendFailed(t,"no action handler: "+n+" defined")}else this.sendFailed(t,"need request param {action}")},SekiroClient.prototype.sendSuccess=function(e,o){var t;if("string"==typeof o)try{t=JSON.parse(o)}catch(e){(t={}).data=o}else"object"==typeof o?t=o:(t={}).data=o;(Array.isArray(t)||"string"==typeof t)&&(t={data:t,code:0}),t.code?t.code=0:(t.status,t.status=0),t.__sekiro_seq__=e;var n=JSON.stringify(t);console.log("response :"+n),this.socket.send(n)},SekiroClient.prototype.sendFailed=function(e,o){"string"!=typeof o&&(o=JSON.stringify(o));var t={};t.message=o,t.status=-1,t.__sekiro_seq__=e;var n=JSON.stringify(t);console.log("sekiro: response :"+n),this.socket.send(n)},SekiroClient.prototype.registerAction=function(e,o){if("string"!=typeof e)throw new Error("an action must be string");if("function"!=typeof o)throw new Error("a handler must be function");return console.log("sekiro: register action: "+e),this.handlers[e]=o,this};
var client = new SekiroClient("ws://127.0.0.1:5612/business-demo/register?group=test_web&clientId=" + Math.random());
client.registerAction("testAction", function (request, resolve, reject) {
    resolve("ok");
});

請注意,如果目標網站是 https,且 demo 無法正確連接,請下載證書並安裝到你的系統中

我們重點關注後面這一段 (前面的代碼相當於初始化可以頁面打開的時候優先插入到瀏覽器,也可以和接口註冊一起插入

var client = new SekiroClient("ws://127.0.0.1:5612/business-demo/register?group=test_web&clientId=" + Math.random());
client.registerAction("testAction", function (request, resolve, reject) {
    resolve("ok");
});

詳細說明

var client = new SekiroClient("ws://127.0.0.1:5612/business-demo/register?group=test_web&clientId=" + Math.random());

  1. new SekiroClient(): 就是新建一個 ws 或者 wss 連接

  2. ws: 是代表了 WebSocket 協議,wss 則爲 WebSocket Secure 協議;127.0.0.1:5612 連接地址加端口默認端口是 5612 ;business-demo 是版本;register 代表是連接註冊 調用接口的時候要將 register 改爲 invoke ;test_web 是分組 ;clientId 就是隨機的一個 ID 訪問的時候加不加都可以

client.registerAction("testAction", function (request, resolve, reject) { resolve("ok");});

  1. testAction 調用接口時候的 action 參數

  2. request 是訪問時候的請求,可以通過 request['param'] 來獲取訪問接口的 param 參數,參數名可自定義 (上個工具 JsRpc 的參數名爲 param),多個參數:request['param1'],request['param1']... 也可以使用 json 的方法傳遞 (具體大家看官網)

如下是一個訪問調用接口例子

http://127.0.0.1:5612/business/invoke?group=test_frida&action=testAction¶m1=testparm1¶m2=testparm2

因爲我們不是商業版的,所以將 business 改爲了 business-demo

使用實戰

還是拿上面講某團登錄舉例子,我們先啓動本地的服務端

我們在上面本地替換後的 JS 文件當中加密函數附近輸入:window.getH5fingerprint=utility.getH5fingerprint,將加密方法賦給全局變量 window,然後觸發一下登錄操作(也可以先個打斷點,再在控制檯將加密函數暴露到 window 全局之後放開斷點,不要一直打着斷點,不然是沒法成功通信的)。

瀏覽器控制檯直接輸入下方代碼,上面那一長串不用管,帶着就行,重要的是框起來的部分,我這裏因爲將方法提升到全局了大家也可以在替換後的 JS 文件當中直接寫入客戶端代碼從而直接調用加密函數(作用域要對)。

在瀏覽器控制檯執行效果如下:

這樣說明了連接成功了,然後我們直接訪問下方 url,即可得到加密的結果(123 只爲了演示)。

http://127.0.0.1:5612/business-demo/invoke?group=meituan&action=getH5fingerprint&input=123

對於 input 參數的傳遞用 get 或者是 post 都可以,同時也支持 json 格式傳遞

常見問題

  1. 我們一般在本地迴環地址 127.0.0.1 結合 ws 協議使用即可,如果 https 頁面出現錯誤請到 Sekiro 框架官網安裝證書,官網詳解如下:

https://sekiro.iinti.cn/sekiro-doc/02_advance/03_sslForWebsocket.html

  1. 如果出現以下報錯,說明是 CSP 策略禁止了連接。

refused to connect to 'wss://sekiro.iinti.cn/business/register?group=ws-group&clientId=1221' because of it violates the following Content Security Policy directive: xxxxxx

可以按照官網的解決辦法:使用瀏覽器代理、使用子域名繞過 csp 等,同時需要安裝 Sekiro 的 CA 證書,篇幅有限這裏就不多說了,況且官網已經十分詳細了。地址如下:

https://sekiro.iinti.cn/sekiro-doc/02_advance/03_sslForWebsocket.html

大家如果還是有疑問建議去仔細閱讀 Sekiro 框架官網

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xiK0i80lvfLittsPPPlOkg