鐵人賽 Day 22 逆向實戰 - Header 驗證參數 + Response加密 (簡單)

本系列文章所討論的 JavaScript 資安與逆向工程技術,旨在分享知識、探討防禦之道,並促進技術交流。
所有內容僅供學術研究與學習,請勿用於任何非法或不道德的行為。
讀者應對自己的行為負完全責任。尊重法律與道德規範是所有技術人員應共同遵守的準則。
挑戰網址
aHR0cHM6Ly93d3cubWFzaGFuZ3BhLmNvbS9wcm9ibGVtLWRldGFpbC82Lw==
解題過程
觀察 Network 請求
在 Chrome DevTools 打開 「 Network 」 分頁,並篩選 「 XHR / Fetch 」 請求。
可以看到 Method 為 GET 且 URL 沒有任何驗證餐數。

查看 Request Headers
查看 Request Headers 可以看到兩個關鍵欄位。
s:驗證參數。
tt:時間戳。

查看 Request Response
伺服器回傳一段加密字串 t 需要解密後才能得到真實資料。

搜尋參數位置
按下 ESC 打開底部工具欄透過 Search 搜尋 s:。
搜尋結果顯示 pagination6.js 檔案裡有 s: window.token 的程式碼。
這代表 s 的值,實際上是 window.token。

找到加密位置
發現 token 是透過 xxoo 函式運算後產生並與時間戳組合存入 window.hhh。

設置斷點
在第 92 行 (window.token = window.xxoo(...)) 打斷點。
切換分頁時程式會停在這邊,滑鼠移到 xxoo 上方會顯示該函式資訊。
點擊後即可跳轉到 xxoo 函式所在的行數。

進入 xxoo 函式
在第 76 行打斷點後按下「Resume script execution」後進入函式並且馬上斷著。
可在 Scope → Local 看到傳入參數。
n:sssssbbbbb1756951474291
r:undefined
t:undefined

逐步執行觀察輸出
使用「Step into next function call」逐步追蹤可以發現實際會執行 h(l(n)) 函式。
按下 ESC 打開底部工具欄在 Console 中輸入 n 可以查看參數初始值再次輸入 h(l(n)) 可以得到加密過後的值為 "9e93f4ebddf3803e2ec4f248d34e1378"。

此數值長度為32位非常類似MD5 我們可以實際測試將 "sssssbbbbb1756951474291" 使用NodeJS進行MD5,實際運算後發現結果與 h(l(n)) 的結果相同證實 h(l(n)) 確實為MD5。
const CryptoJS = require('crypto-js');
const message = "sssssbbbbb1756951474291";
const md5Hash = CryptoJS.MD5(message).toString();
console.log(`MD5 雜湊值: ${md5Hash}`);
Response 解密流程
在第 124 行 (JSON.parse(xxxoooo(data.t))) 打斷點。
切換分頁時程式會停在這邊,點擊後即可跳轉到 xxxoooo 函式所在的行數。

分析 AES 解密函式
在 xxxoooo 函式內看到使用 AES 解密。
kkkk 與 iiii 作為 key 與 iv 透過 AES.decrypt 解密伺服器回傳的 t 即可得到真實資料。

完整程式碼
const CryptoJS = require('crypto-js')
function generatorHeaderS(time) {
const data = "sssssbbbbb" + time
return CryptoJS.MD5(data).toString()
}
const decrypt = (encryptedHex) => {
let key = CryptoJS.enc.Utf8.parse("xxxxxxxxoooooooo");
let iv = CryptoJS.enc.Utf8.parse("0123456789ABCDEF");
let parseEncryptedHex = CryptoJS.enc.Hex.parse(encryptedHex);
let decryptBuffer = CryptoJS.AES.decrypt({
ciphertext: parseEncryptedHex
},
key,
{
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
iv: iv,
});
return decryptBuffer.toString(CryptoJS.enc.Utf8);
}
const getPage = async(page) => {
const time = new Date().getTime();
const response = await fetch("https://xxxxxxxxxx/api/problem-detail/6/data/?page=" + page, {
"headers": {
"accept": "*/*",
"accept-language": "zh-TW,zh;q=0.9,en;q=0.8,en-US;q=0.7",
"cache-control": "no-cache",
"pragma": "no-cache",
"priority": "u=1, i",
"sec-ch-ua": "\"Not;A=Brand\";v=\"99\", \"Google Chrome\";v=\"139\", \"Chromium\";v=\"139\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"cookie": "sessionid=xxxxxxxxxx",
"Referer": "https://xxxxxxxxxx/problem-detail/6/",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"s": generatorHeaderS(time),
"tt": time
},
"body": null,
"method": "GET"
});
let json = await response.json()
json = JSON.parse(decrypt(json.t))
return json.current_array.reduce((a, b) => a + b, 0);
}
const run = async() => {
let total = 0;
for(let i = 1; i <= 20; i++){
total += (await getPage(i))
}
console.log(`total: ${total}`)
}
run()
Github 原始碼
https://github.com/mrnick6886/ScrapingChallenges/blob/main/mashangpa/6.js