BBLABSBBLABS
LaboratoriosAprender desde 0FuncionesPreciosTestimoniosDetrás de BBLABS
  1. Inicio
  2. Labs
  3. SQL injection error-based en la resolución de tenant de un portal de identidad
Media50 min

SQL injection error-based en la resolución de tenant de un portal de identidad

Por @gorka

El login y el 'olvidé mi contraseña' resuelven tu organización por username antes de validar nada; un fallo ahí filtra, trozo a trozo, un secreto interno que ningún endpoint sirve.

Este hackeo se reportó sin recompensa

689 visitas6 completadosActualizado nov 2026
Iniciar sesión para empezar

Aprende a encontrar este bug

Un hackeo real de HackerOne, reproducido para que lo practiques.

Crea tu cuenta y practica bugs reales que se pagaron. Descarga el entorno, encuéntralo y aprende la técnica exacta — tu camino a tu primer bounty.

650

hunters entrenando

50

labs de hackeos reales

380

completaciones

$12.000

pagados por estos bugs

40 flags capturadas esta semana
Crear cuenta
Ya tengo cuenta

Acceso a todos los labs · sin permanencia · cancela cuando quieras

Hackers que lo han resuelto· 6

r00thor1
@r00thorHace 28 días
sharat chandra2
@sharat chandraoct 2026
emperador3
@emperadorsept 2026
bizedtv
@bizedtvsept 2026
3nk1
@3nk1sept 2026
w4tchw0lf
@w4tchw0lfsept 2026

Objetivos

1
Detectar que el username del login/reset alcanza una consulta SQL sin parametrizar.
2
Provocar el error de sintaxis y leer el mensaje crudo del motor más la sentencia completa.
3
Reflejar en el error el resultado de una subconsulta propia dentro de una función XPath.
4
Enumerar information_schema hasta localizar una tabla interna que ningún endpoint expone.
5
Superar la truncación del error (~32 caracteres) partiendo la extracción con substring().
6
Descartar el auth-bypass y el resto de decoys con pruebas acotadas.
7
Capturar la flag almacenada en vault_secrets.federation_root_secret.

Información

Plataforma
HackerOne
Dificultad
Media
Duración
50 min
Bounty
Sin recompensa
Completados
6
Creador
gorka@gorka
Actualizado
nov 2026

Descarga el entorno

Reprodúcelo y encuentra el bug tú mismo

Crear cuenta

Herramientas

curlNavegador webBurp SuitePython (requests)

Prerequisitos

  • HTTP y formularios web
  • SQL básico (SELECT/WHERE)
  • Uso de curl o un proxy HTTP

Tags

SQLiInformation Disclosure

Logro que recibirás

Cuando resuelvas este lab desbloqueas este logro compartible

Cuando resuelvas este lab desbloqueas este logro compartible

Writeups de la comunidad

Gratis · sin cuenta

La checklist que repaso en cada objetivo nuevo

47 comprobaciones ordenadas por coste: primero lo que te puede meter en un lío, luego lo barato, y al final lo caro — que es donde están los bounties grandes. Te la mando al correo ahora mismo.

Te das de baja en un clic, desde cualquier correo.

Contenido del lab

Escenario

Ingressia es un SaaS de identidad y acceso para plantillas (workforce identity): decenas de organizaciones clientes ("tenants") delegan en Ingressia el login de sus empleados. Cada tenant tiene su propio directorio de identidades y su propia configuración SSO; antes de comprobar ninguna credencial, el portal necesita saber a qué tenant pertenece el username que el empleado teclea, para pintar el branding correcto y enrutar la petición al backend de auth de su organización.

Te contratan como pentester externo, sin ninguna credencial del sistema, para auditar el portal público de acceso: la pantalla de login y el flujo de "olvidé mi contraseña". Ambos flujos son pre-auth y aceptan el mismo dato de entrada: un username.

Tu misión, sin usar la cuenta de demostración que Ingressia te entrega para orientarte: demuestra que puedes alcanzar datos internos que ningún endpoint documentado sirve, y tráete la prueba — un secreto de federación guardado en un almacén interno — como flag.

Qué tienes

  • Cuenta demo: attacker@ingressia.local / password123 — te deja entrar y ver el dashboard y otras pantallas, pero la inyección corre pre-auth: no la necesitas para explotarla.
  • Dos formularios públicos que piden un username (login y "olvidé mi contraseña") — la superficie de ataque completa está a la vista, sin credenciales de por medio.

Guía paso a paso

  1. Reconoce las dos superficies. El login (POST /api/auth/login) y el "olvidé mi contraseña" (POST /api/auth/password-reset) piden ambos un username antes de nada — es el paso de resolución de tenant. Mete un simple ' en ese campo (username=x', cualquier password) → 500 en vez del rechazo normal.
  2. Lee el error con calma. El cuerpo del 500 trae db_message (el texto crudo de MariaDB, tipo You have an error in your SQL syntax…) y statement (la sentencia completa: SELECT id, tenant_id FROM directory_identities WHERE username = 'x'' LIMIT 1). Conclusión: username se concatena crudo en la query, y la tabla detrás es directory_identities.
  3. Descarta el atajo evidente (Decoy A). Antes de perseguir la extracción, prueba si esto es un auth-bypass: username=' OR 1=1-- en el login. Resultado: 401, sin error, sin sesión. La comprobación de la contraseña es una segunda query, parametrizada + bcrypt.compare, totalmente separada de la resolución de tenant. Concluyes: esto es exfiltración, no bypass.
  4. Confirma la extracción in-band. Inyecta ' AND updatexml(1,concat(0x7e,(select version()),0x7e),1) AND '1'='1 en username. El 500 refleja algo como ~10.11.x-MariaDB~ en db_message — el error te devuelve el resultado exacto de tu subconsulta. Repite con database() para confirmar el schema (ingressia_directory).
  5. Enumera el schema. Con el mismo primitivo, pide (select group_concat(table_name) from information_schema.tables where table_schema=database()). Entre las tablas de negocio (tenants, sso_connections, auth_audit_log, reset_tokens…) aparece vault_secrets — que no está detrás de ningún endpoint que hayas visto. Pide sus columnas del mismo modo: secret_name, secret_value.
  6. Localiza la fila correcta. Enumera secret_name de vault_secrets: aparecen smtp_relay_password, webhook_signing_key, scim_bearer_token, session_signing_key y federation_root_secret — el nombre que destaca por describir un secreto raíz de federación entre Ingressia y sus tenants.
  7. Extrae con chunking. Pide secret_value where secret_name='federation_root_secret' — el db_message te lo devuelve cortado a ~32 caracteres (~FLAG{…~, sin cierre). La flag mide 38 caracteres. Parte la lectura en dos: substring(secret_value,1,31) y substring(secret_value,32,20), y concatena los dos fragmentos reflejados.
  8. Confirma el punto ciego / verifica. Reproduce el mismo ataque contra la segunda superficie (/api/auth/password-reset) para comprobar que no es una casualidad del login, y valida tu flag con bash flag/verify.sh (md5 de la preimagen).

Canales de feedback (no vuelas a ciegas)

  • El cuerpo del 500 (db_message + statement) en cada intento — el error crudo del motor y la sentencia SQL completa que se ejecutó.
  • El string reflejado entre ~…~ tras cada updatexml — confirma exactamente qué subconsulta se evaluó y con qué resultado.
  • La longitud visible del valor reflejado (~32 caracteres, cortado) — la señal explícita de que hace falta partir la extracción.

Decoys (descártalos por razonamiento acotado)

  • Decoy A — auth-bypass (username=' OR 1=1-- ): 401, sin error, sin login — el password-check va parametrizado con bcrypt.compare, aislado de la resolución de tenant.
  • Decoy B — typeahead de organizaciones (GET /api/directory/orgs?q='): 200 con results: [], ecoa q pero busca con WHERE name LIKE ? (bind) — el ' es texto literal, no delimitador.
  • Decoy C — ORDER BY con allowlist (GET /api/directory/reset-history?sort=(select 1)): 400 invalid sort field — el valor nunca llega a interpolarse.
  • Decoy D — campos hermanos parametrizados: un ' en password o en tenant (hint opcional del login) no dispara ningún error — solo username alcanza la query cruda.

La falla del laboratorio

Clase: CWE-89 (SQL Injection) + CWE-209 (Generation of Error Message Containing Sensitive Information) + CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor). OWASP A03:2021 (Injection) + A05:2021 (Security Misconfiguration).

La causa raíz vive en lib/directory.ts: resolveTenantByUsername() (login) y findAccountForReset() (reset) construyen la query de resolución de tenant concatenando username directamente en el string SQL y la ejecutan con pool.query(sql), sin values de bind. Es una función con un propósito legítimo — decidir a qué directorio de tenant pertenece un empleado antes de validar su contraseña — que casualmente interpola el identificador crudo. El propio password-check, en cambio, va por una segunda query parametrizada (WHERE tenant_id = ? AND username = ?) más bcrypt.compare, por eso la inyección nunca salta la autenticación: solo puede hacer que la consulta de resolución se comporte mal.

El segundo defecto amplifica al primero: middleware/verboseError.ts reenvía err.sqlMessage y la sentencia reconstruida (err.statement/err.sql) tal cual al cliente "para facilitar el diagnóstico de la federación entre Ingressia y el directorio del tenant". Sin ese error verboso, updatexml() seguiría rompiendo la query, pero el cliente solo vería un 500 genérico — el texto reflejado es lo que convierte cada fallo de sintaxis en un canal de exfiltración in-band, y también lo que le enseña al atacante la tabla directory_identities sin tener que adivinarla.

El usuario de base de datos de la aplicación, ingressia_app, tiene privilegios SELECT/INSERT/UPDATE únicamente sobre su propio schema (ingressia_directory), sin FILE, PROCESS ni superusuario. No existe ruta a LOAD_FILE/INTO OUTFILE ni a stacked queries: el techo de la explotación es exfiltración pura, lo que mantiene el laboratorio en Medium en vez de escalarlo a un RCE.

Lección: parametriza siempre los identificadores dinámicos, incluso en pasos que parecen "de infraestructura" (resolver un tenant, enrutar una petición) y corren antes del chequeo de negocio real; y nunca devuelvas al cliente el mensaje crudo del motor de base de datos ni la sentencia ejecutada — un handler de errores verboso convierte cualquier fallo de sintaxis en un oráculo de datos.

Laboratorio basado en la reproducción anonimizada (empresa ficticia Ingressia) de una técnica real de bug bounty sobre inyección SQL error-based en un flujo de autenticación.

La comunidad

Esto no es una plataforma con usuarios.Es gente rompiendo lo mismo que tú.

Dentro se pregunta, se enseña lo que se ha encontrado y se resuelve en grupo lo que atascaría a cualquiera solo. Se entra gratis: sin cuenta, sin plan y sin dejar el correo.

Entrar a la comunidad de BBLABS en Discord
Entrar al Discord

Es gratis y abierto. No hace falta tener cuenta en BBLABS para entrar.

BBLABS LogoBBLABS

Laboratorios de hacking basados en hackeos reales. Aprende practicando sobre entornos vulnerables, con la resolución explicada paso a paso.

Producto

  • Funcionalidades
  • Precios
  • Labs
  • Aprender desde 0
  • Blog
  • Detrás de BBLABS

Legal

  • Términos
  • Privacidad
  • Cookies
  • Entrar
  • Crear cuenta

Para cualquier duda, bug, soporte o sugerencia puedes escribir a team@bblabs.es

© 2026 BBLABS. Todos los derechos reservados.