SQL Injection en la Practica: Explotar, Detectar y Mitigar en Lab Controlado
Demostracion tecnica de SQLi con sqlmap en entorno propio, enfocada en deteccion defensiva y fixes parametrizados que realmente sostienen trafico en produccion.

SQL Injection cumplio 27 anos en 2026 y sigue en el top tres de OWASP, no porque los atacantes se hayan vuelto mas listos, sino porque ORMs mal usados, queries dinamicas en dashboards internos y resolvers GraphQL que concatenan strings siguen llegando a produccion. Esta guia monta un lab controlado donde explotas, detectas y mitigas la vulnerabilidad de punta a punta. La meta no es presumir un dump de la tabla users: es entender el ciclo completo, desde el primer probe hasta una regla WAF que mata a sqlmap en menos de dos minutos. Todo corre en hardware propio, contra objetivos que tienes autorizacion de romper, en una red aislada sin ruta hacia nada real.
Que es realmente SQL injection
SQL injection ocurre cuando input no confiable llega al interprete SQL como codigo en vez de como dato. La base de datos no puede distinguir la intencion del desarrollador del payload del atacante una vez que ambos quedan pegados en el mismo string. El ejemplo canonico es "SELECT * FROM users WHERE id = " + req.id, donde id=1 OR 1=1 convierte una consulta de una fila en una lectura completa de la tabla. La causa raiz nunca es el motor de base de datos; es la concatenacion. Los frameworks modernos hacen de la parametrizacion el default, pero el armado crudo de strings sobrevive en endpoints de reporteria, clausulas dinamicas ORDER BY, filtros de busqueda y query builders hechos a mano.
La taxonomia de las clases de injection
No puedes probar lo que no puedes nombrar. In-band UNION-based devuelve datos directo en la respuesta HTTP mediante un UNION SELECT agregado. Boolean-based blind infiere un bit a la vez a partir de como cambia la pagina cuando una condicion es verdadera o falsa. Time-based blind usa SLEEP() o pg_sleep() para filtrar datos por latencia cuando no hay salida visible. Error-based induce a la base a echoar datos dentro de un mensaje de error. Out-of-band exfiltra por DNS o HTTP cuando el canal es totalmente ciego. Y second-order almacena un payload que dispara despues, en otra query, en otra ruta. Las aplicaciones reales suelen exponer mas de una clase.
Montando el lab aislado
Empieza con Docker en una red host-only. Corre una instancia DVWA, MySQL 8.0 con el binlog activo y un proxy Burp Suite que capture cada request. Un compose minimo te da un objetivo repetible: docker compose up -d dvwa mysql. Enruta el navegador por Burp, pon el nivel de seguridad en low para la primera pasada, luego subelo a medium y high para sentir como el filtrado de input cambia el juego. Nunca expongas este stack en una interfaz enrutable; los labs de SQLi atraen scanners oportunistas, y un MySQL vulnerable en internet publico se vuelve el cryptominer de un desconocido en cuestion de horas.
Explotacion paso a paso con sqlmap
El primer experimento golpea el endpoint GET de DVWA. Ejecuta sqlmap -u 'http://lab.local/vulnerabilities/sqli/?id=1&Submit=Submit' --cookie='PHPSESSID=...; security=low' --batch --technique=BEUST --level=3 --risk=2. En unos cuatro segundos sqlmap confirma injection boolean-based blind en el parametro id; en once enumera la base dvwa y sus tablas. Agrega --dump -T users para extraer credenciales, o --os-shell donde privilegios FILE y secure_file_priv permitan escribir una web shell. Observa los payloads en el cable: 1 AND 4523=4523, 1 AND 1=2 UNION SELECT NULL,NULL. Esos literales numericos repetitivos, el user-agent default sqlmap/1.8.x y el header Accept-Encoding ausente son exactamente la senal que armaras despues para deteccion.
Explotacion manual, porque las herramientas pierden contexto
Automatiza el descubrimiento, pero explota a mano cuando importa. Para extraccion UNION-based, primero halla el numero de columnas con ORDER BY 5-- - hasta que la query falle, luego empareja tipos con UNION SELECT 1,2,3,4,5-- - y coloca @@version, database() y group_concat(table_name) en columnas visibles via information_schema.tables. Para blind boolean, biseca cada caracter con AND ASCII(SUBSTRING((SELECT ...),1,1))>77. Para time-based, reemplaza la comparacion por AND IF(condition, SLEEP(3), 0). Hacerlo a mano una vez te ensena el modelo mental que ningun scanner da, y es la unica forma de manejar contextos mutilados por WAF donde los tamper scripts de sqlmap se quedan cortos.
Second-order injection: el bug que los scanners saltan
Registra via formulario un usuario llamado admin'-- cuyo prepared statement escapa la comilla de forma segura en la escritura. El payload queda inactivo en la base. El problema vive en un endpoint interno de busqueda o perfil que reutiliza ese valor almacenado en una query dinamica sin reparametrizar. Resultado: bypass de autenticacion en una ruta que nunca aparecio en el scan inicial, porque el payload solo detona en otro contexto. Es el mismo modo de fallo que perfora APIs GraphQL cuyos resolvers comparten un query builder. Las herramientas automaticas rara vez lo atrapan, por eso la revision manual de codigo sigue pagando renta en todo engagement serio.
Deteccion e instrumentacion
Convierte el ruido del atacante en senal. Activa SET GLOBAL general_log = 'ON' para que cada statement caiga en el query log con timestamp en microsegundos. Compara treinta segundos de trafico legitimo contra treinta de sqlmap: el coeficiente de variacion del largo de query salta de aproximadamente 0.12 a 1.8, y los errores de sintaxis se disparan. Envia el log por Filebeat a Elastic y construye tres detecciones: errores de sintaxis SQL por arriba de cinco por minuto por IP origen, secuencias con UNION SELECT NULL, y tiempo de ejecucion por arriba de tres desviaciones estandar del baseline horario. Contra sqlmap con --random-agent y --delay=2, las tres dispararon en menos de noventa segundos. Planta ademas honeytokens: una columna falsa credit_card_test con string rastreable te dice exactamente que endpoint filtro el dia que aparece en un paste site.
Mitigacion y hardening que de verdad aguanta
La mitigacion real arranca con prepared statements parametrizados y no termina ahi. Sustituye strings interpolados por parametros ligados en cada lenguaje: NamedParameterJdbcTemplate en Java, placeholders $1 en el pgx de Go, y PDO con PDO::ATTR_EMULATE_PREPARES=false en PHP para evitar el clasico bypass de comilla multibyte. Como los prepared statements no parametrizan identificadores, protege el ORDER BY dinamico y los nombres de columna con un allowlist estricto. Agrega least privilege en el usuario de la aplicacion (GRANT SELECT, INSERT, UPDATE, nunca FILE ni SUPER), validacion de input en el borde, y una WAF como ModSecurity con CRS 4.x en modo blocking delante. Reduce la superficie antes de confiar en deteccion; defensa en profundidad significa que un atacante debe vencer cada capa, no solo una.
Errores comunes
Los equipos rompen sus propias defensas de formas predecibles. Parametrizan el valor pero interpolan el nombre de la tabla. Confian en un ORM y luego bajan a una query cruda para un reporte. Escapan en la salida en vez de parametrizar en la entrada. Corren el usuario de la app como root en la base, asi una injection menor se vuelve compromiso total del servidor. Prueban solo el formulario de login e ignoran busqueda, export y paneles admin donde se esconde el SQL dinamico feo. Y toman un scan verde de sqlmap como prueba de seguridad, cuando sqlmap por diseno nunca reporta los caminos second-order y de logica de negocio que un revisor humano encuentra.
Un checklist de campo
Antes de firmar: toda query que toca input externo esta parametrizada; los identificadores pasan por un allowlist; el usuario de base tiene least privilege; los mensajes de error son genericos y nunca echoan SQL; una WAF bloquea en produccion, no solo loguea; el query log alimenta un SIEM con las tres detecciones de arriba; hay honeytokens en tablas de alto valor; y una revision trimestral cubre todo endpoint que acepte input, incluyendo rutas internas y de admin. Prueba que el ciclo funciona atacando tu propio build y viendo dispararse las alertas.
Mas alla de MySQL: otros motores cambian los payloads
La clase del bug es portable, la sintaxis no, y un tester que solo conoce MySQL se congela en el momento en que el backend es PostgreSQL o SQL Server. En PostgreSQL la concatenacion usa ||, los comentarios usan --, los retrasos vienen de pg_sleep(3), y las stacked queries suelen estar disponibles via el driver, lo que abre la puerta a ejecucion de comandos con COPY ... TO PROGRAM en instalaciones mal configuradas. En SQL Server WAITFOR DELAY '0:0:3' maneja la inferencia time-based, xp_cmdshell es la escalacion clasica cuando esta habilitado, y la extraccion error-based se apoya en desajustes de tipo con CONVERT(). Oracle obliga a cada SELECT a pasar por FROM dual y tambien concatena con ||. Los stores NoSQL tampoco son seguros: MongoDB acepta inyeccion de operadores como {"$gt": ""} cuando un body JSON pasa sin validar a una query, convirtiendo un login en un match siempre verdadero. La leccion defensiva es identica en todos, por eso generaliza: nunca construyas la query desde el input, liga siempre, y corre la cuenta de base de datos con el grant mas estrecho posible para que ni una injection exitosa alcance el sistema operativo.
FAQ
Un ORM me hace inmune a SQL injection? No. Los ORMs parametrizan el camino comun, pero los escapes de query cruda, los fragmentos SQL nativos y las clausulas WHERE u ORDER BY construidas dinamicamente reintroducen la vulnerabilidad. Audita cada lugar donde tu ORM te deja bajar a SQL crudo.
Alcanza con una WAF sola? No. Una WAF compra tiempo y bloquea tooling comun, pero trucos de encoding, tamper scripts y payloads second-order evaden las reglas de firma. Trata a la WAF como una capa sobre queries parametrizadas y least privilege, nunca como sustituto.
Takeaway practico: monta el lab, corre sqlmap una vez para sentir el ritmo de sus payloads, luego gasta el ochenta por ciento del tiempo restante en deteccion y fixes. SQL injection no es un problema de creatividad del atacante; es una query dinamica que nadie reviso. Parametriza todo lo que sea valor, usa allowlist para todo lo que sea identificador, y el dia que puedas probar que tu aplicacion mata sqlmap en menos de dos minutos con bloqueo automatico, ganaste.


