鐵人賽 Day 24 逆向實戰 - Post 驗證參數 (中等)

本系列文章所討論的 JavaScript 資安與逆向工程技術,旨在分享知識、探討防禦之道,並促進技術交流。
所有內容僅供學術研究與學習,請勿用於任何非法或不道德的行為。
讀者應對自己的行為負完全責任。尊重法律與道德規範是所有技術人員應共同遵守的準則。
挑戰網址
aHR0cHM6Ly93d3cubWFzaGFuZ3BhLmNvbS9wcm9ibGVtLWRldGFpbC85Lw==
解題過程
DevTools 被 debugger 斷住
打開 DevTools 後會直接停在debugger需要往上追蹤 Call Stack 並且 Override 排除。

確認請求方法
選擇請求 data/ 在 Headers 標籤下可以確認請求方法為 POST。

檢查請求參數
在 Payload 標籤下觀察請求內容,看到參數 m 和 tt,為傳送的驗證參數。

追蹤請求來源
在 Initiator 分頁檢查呼叫堆疊追蹤到 window.loadPage 函式來自 pagination9.js。
點擊 pagination9.js 並進入該對應的函式。

定位 ajax 呼叫
程式碼中呼叫 $[r(1414)] 經過混淆不太容易觀察到實際內容。

驗證 ajax 映射
在 Console 輸入 r(1414) 得到 ajax,所以$[r(1414)] 等同於 $.ajax。
在 Console 輸入 $.ajax 並點擊函式可以跳到該函式位置。

分析自訂 ajax 包裝
繼續追蹤 n[r(1414)] = function(u){...},發現這裡重新包裝了 $.ajax。

函式呼叫關聯
先下斷點觀察,可以觀察到呼叫鏈中使用 t[e(613)],透過 n(r) 方式轉換並呼叫函式。
所以實際上需要觀察的是n的函式,將滑鼠放在n上會顯示函式資訊,並且點擊進入該函式。

switch-case 生成資料
這個函式也是經過混淆的。

switch-case 執行順序
透過 r(1034) 取出的字串 "1|6|7|2|0|3|4|5" 控制 switch-case 的執行順序。

所以實際上的執行邏輯如下,但這還是混淆過的程式碼,需要進一步解混淆。
var f = (new Date)[r(2244)]();
n[r(516)] = n.headers || {};
n[r(516)][r(481)] = t.fFUZS;
n[r(1939)] = n[r(1939)] || {};
n.data.m = t.MqmaW(c, t[r(1881)](t[r(489)], f));
n[r(1939)].tt = btoa(f);
n[r(1939)] = JSON[r(827)](n.data);
解混淆後的程式碼如下
var f = (new Date).getTime();
n.headers = n.headers || {};
n.headers['Content-Type'] = 'application/json';
n.data = n.data || {};
n.data.m = c('9527' + f);
n.data.tt = btoa(f);
n.data = JSON.stringify(n.data);
解完混淆可以得知兩個參數實際邏輯。
m 是透過 c('9527' + 時間戳) 產生的。
tt 則是將時間戳使用base64編碼。
觀察並進入 c 函式內部
滑鼠移到變數 c 可以顯示該函式資訊,並可直接跳轉到程式碼定義位置。

確認加密簽名生成
最後可以定位到產生簽名的函式,看到使用 HmacSHA1 並結合特定字串運算,這裡就是最終生成驗證參數的關鍵位置。

完整程式碼
const CryptoJS = require('crypto-js')
function generatorPostM(time) {
const text = `9527${time}`
return CryptoJS.HmacSHA1(text, "xxxooo").toString();
}
function generatorPostTt(time) {
return btoa(`${time}`)
}
const getPage = async(page) => {
const time = new Date().getTime();
const postM = generatorPostM(time);
const postT = generatorPostTt(time)
const response = await fetch(`https://xxxxxxxxxx/api/problem-detail/9/data/`, {
"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/9/",
"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",
},
"body": JSON.stringify({
"page": page,
"m": postM,
"tt": postT,
}),
"method": "POST",
});
let json = await response.json()
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/9.js