SQL injection error-based en la resolución de tenant de un portal de identidad
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.
Learn to find this bug
A real HackerOne hack, reproduced for you to practice.
Create your account and practice real bugs that got paid. Download the environment, find it and learn the exact technique — your path to your first bounty.
hunters training
labs from real hacks
completions
paid out for these bugs
Access to all labs · no commitment · cancel anytime
Hackers who solved it· 1
Objectives
Achievement you'll earn
Solve this lab to unlock this shareable achievement
SQL injection error-based en la resolución de tenant de un portal de identidad
Community writeups
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
- Reconoce las dos superficies. El login (
POST /api/auth/login) y el "olvidé mi contraseña" (POST /api/auth/password-reset) piden ambos unusernameantes de nada — es el paso de resolución de tenant. Mete un simple'en ese campo (username=x', cualquierpassword) → 500 en vez del rechazo normal. - Lee el error con calma. El cuerpo del 500 trae
db_message(el texto crudo de MariaDB, tipoYou have an error in your SQL syntax…) ystatement(la sentencia completa:SELECT id, tenant_id FROM directory_identities WHERE username = 'x'' LIMIT 1). Conclusión:usernamese concatena crudo en la query, y la tabla detrás esdirectory_identities. - 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. - Confirma la extracción in-band. Inyecta
' AND updatexml(1,concat(0x7e,(select version()),0x7e),1) AND '1'='1enusername. El 500 refleja algo como~10.11.x-MariaDB~endb_message— el error te devuelve el resultado exacto de tu subconsulta. Repite condatabase()para confirmar el schema (ingressia_directory). - 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…) aparecevault_secrets— que no está detrás de ningún endpoint que hayas visto. Pide sus columnas del mismo modo:secret_name,secret_value. - Localiza la fila correcta. Enumera
secret_namedevault_secrets: aparecensmtp_relay_password,webhook_signing_key,scim_bearer_token,session_signing_keyyfederation_root_secret— el nombre que destaca por describir un secreto raíz de federación entre Ingressia y sus tenants. - Extrae con chunking. Pide
secret_value where secret_name='federation_root_secret'— eldb_messagete lo devuelve cortado a ~32 caracteres (~FLAG{…~, sin cierre). La flag mide 38 caracteres. Parte la lectura en dos:substring(secret_value,1,31)ysubstring(secret_value,32,20), y concatena los dos fragmentos reflejados. - 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 conbash 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 cadaupdatexml— 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 conbcrypt.compare, aislado de la resolución de tenant. - Decoy B — typeahead de organizaciones (
GET /api/directory/orgs?q='): 200 conresults: [], ecoaqpero busca conWHERE name LIKE ?(bind) — el'es texto literal, no delimitador. - Decoy C —
ORDER BYcon allowlist (GET /api/directory/reset-history?sort=(select 1)): 400invalid sort field— el valor nunca llega a interpolarse. - Decoy D — campos hermanos parametrizados: un
'enpasswordo entenant(hint opcional del login) no dispara ningún error — solousernamealcanza 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.