La cadena de 7 fallas que tomó el admin de un SaaS sin login
Pentest a un SaaS B2B brasileño consolidado: 7 fallas pequeñas encadenadas se volvieron una toma total de admin desde cero, sin usuario ni contraseña.
Investigador de seguridad y fundador de No Vuln

Imagina abrir internet, ir a la dirección de tu SaaS, y — sin tener login, sin tener contraseña, sin hacer clic en “olvidé mi contraseña”, sin tener ninguna cuenta en tu plataforma — en 5 minutos convertirte en el administrador master de tu aplicación. Leyendo todos los datos de todos tus clientes. Editando contratos. Cambiando configuraciones. Cambiando contraseñas.
Fue exactamente eso lo que encontramos en un pentest reciente a un SaaS B2B brasileño consolidado, con una base activa de cientos de clientes y millones en ARR. La cadena tiene 7 eslabones. Cada eslabón, por sí solo, parece pequeño. En secuencia, se vuelven una catástrofe.
Este post es el análisis técnico anonimizado de lo que pasó — porque los 7 eslabones se repiten en el SaaS B2B brasileño con una frecuencia preocupante. Si operas una plataforma web, vale la pena leerlo.
El escenario (anonimizado)
- Aplicación: SaaS B2B en producción, base activa, contratos enterprise
- Stack: PHP + Adianti Framework (framework brasileño popular en ERPs y plataformas administrativas)
- Base de datos: MySQL multi-tenant
- Infra: servidor compartido, PHP 8.x, Apache
- Severidad consolidada del engagement: 53 hallazgos — 12 críticos, 17 altos, 8 medios, 9 bajos, 7 informativos
El cliente ya había pasado por otras auditorías antes. “Estaba limpio”, según ellos. En una investigación adversarial enfocada, encontramos 53 vulnerabilidades distintas. Este post se centra en uno de los 12 críticos: la cadena de 7 eslabones que se vuelve un ATO de admin master pre-auth. Los demás hallazgos críticos serán tema de posts propios.
La cadena en 1 minuto
| # | Eslabón | Severidad aislada | Función en la cadena |
|---|---|---|---|
| 1 | Servicio de upload de archivos accesible sin autenticación | Media | Puerta de entrada |
| 2 | Validación de extensión débil (acepta .phar) | Media | Engaña al filtro |
| 3 | Directorio de upload dentro del webroot + ejecución PHP permitida | Media | Permite ejecutar lo que se subió |
| 4 | Archivo application.ini legible por el proceso web | Alta | Expone secretos |
| 5 | Hash de contraseñas en MD5 sin salt | Alta | Permite forjar credenciales |
| 6 | API REST que valida el token por secreto compartido (no JWT firmado) | Media | Posibilita crear una sesión de admin |
| 7 | Sesión PHP basada en filesystem, sin rotación de ID en el login | Media | Secuestro de la sesión de admin |
Cada fila es “media” por separado. Combinadas → Crítica catastrófica.
La historia, paso a paso
Eslabón 1 — La puerta de atrás sin llave
El primer descubrimiento fue un endpoint:
POST /engine.php?class=AdiantiUploaderServiceAdianti Framework expone ciertas clases como “públicas” vía un archivo de configuración. El equipo de desarrollo marcó el servicio de upload como público — probablemente para permitir subir el avatar en el signup. Resultado: cualquier persona en internet podía subir archivos al servidor. Sin login. Sin token. Sin nada.
El equivalente físico: una puerta en la parte de atrás de la oficina, sin cerradura, y nadie mirando.
Eslabón 2 — El filtro que filtra mal
El servicio validaba la extensión del archivo enviado. Bloqueaba .php. Pero aceptaba .phar — Phar es un formato de archivo PHP que sirve como ejecutable empaquetado. El servidor no lo diferenciaba.
El equivalente físico: la puerta de atrás tenía un cartel de “prohibido entrar con tenis”, pero aceptaba sandalias. El mismo problema, con otra ropa.
Eslabón 3 — La carpeta donde todo se ejecuta
El upload caía en /var/www/html/tmp/. Por defecto en Apache, todo en esa carpeta podía ejecutarse como PHP — incluido el .phar que acabábamos de subir.
Resultado: el atacante sube un archivo, después accede a ese archivo por la URL, y el servidor corre el código que él escribió.
1. POST /engine.php?class=AdiantiUploaderService ← sube payload.phar
2. GET /tmp/payload.phar ← el servidor lo ejecuta
3. El atacante ahora corre comandos en el servidor ← RCE confirmadoTraducción para no técnicos: el atacante ahora corre código en tu servidor como si fuera su propio servidor.
Eslabón 4 — La caja fuerte de secretos sin puerta
Con el RCE en la mano, el atacante lee el archivo de configuración de la aplicación:
[general]
seed = "abc123def456..." ← usada para firmar tokens
rest_key = "789xyz..." ← clave para la API REST
db_password = "..."Esos secretos deberían estar protegidos. Pero el proceso PHP que sirve el sitio tiene permiso de lectura sobre ese archivo (lo necesita, para funcionar). Como el atacante ahora corre como el proceso PHP, lo lee todo.
Traducción para no técnicos: el intruso encontró el cuadernito del dueño de la empresa, con todas las contraseñas anotadas.
Eslabón 5 — Contraseñas que se volvieron azúcar mojado
El hash de las contraseñas en la base de datos usaba MD5 sin salt. MD5 es un algoritmo de hash de 1992 — roto para su uso en contraseñas desde ~2010. Sin salt, cualquier contraseña común aparece en rainbow tables (tablas precomputadas) en segundos.
Peor: con la “seed” del application.ini en la mano, el atacante logra forjar la contraseña del admin directamente sin necesidad de romper nada. Conoce el algoritmo + tiene la clave = puede inventar el hash que quiera.
Traducción para no técnicos: las contraseñas de tus usuarios estaban guardadas con un candado de bicicleta de los años 90. Cualquiera lo abre.
Eslabón 6 — La API que confiaba en el secreto compartido
La aplicación tenía una API REST autenticada por secreto compartido (no por JWT firmado, no por OAuth — solo “manda el secreto correcto, yo confío”). Con el rest_key en la mano (filtrado en el eslabón 4), el atacante llama a la API como si fuera el servidor hablando consigo mismo.
La API permite crear una sesión de admin — porque es la forma en que entra el admin master. El atacante ahora tiene un session_id válido de admin master.
Traducción para no técnicos: el intruso encontró la contraseña del interfono interno de la empresa, llamó a recepción y dijo “manda la llave maestra a mi departamento”. Recepción la mandó.
Eslabón 7 — La sesión que no rota
El atacante toma el session_id recién creado y lo pega en un navegador anónimo. Como la aplicación no rota el ID de sesión entre el login y el estado autenticado, el ID creado por la API sirve directo en el navegador.
El atacante recarga la página → está adentro como admin master. Lee todos los datos. Crea contratos falsos. Borra registros. Todo lo que puede hacer el admin master, puede hacerlo él.
Traducción para no técnicos: la credencial que forjó por teléfono funcionó directo en el molinete. Sin preguntas.
El impacto si lo explota un atacante real
Estimación que le presentamos al cliente:
- Filtración de datos personales sensibles (LGPD Art. 5º + Art. 11 — la ley brasileña de protección de datos) de toda la base de clientes
- Multa LGPD potencial: hasta 2% de la facturación o R$ 50M por infracción (tope)
- Ruptura de contratos enterprise: SLA, DPA, cláusulas de seguridad
- Comunicación obligatoria a la ANPD (la autoridad brasileña de protección de datos) en 3 días hábiles (Resolución CD/ANPD n.º 15/2024)
- Comunicación a los titulares afectados (toda la base)
- Churn post-incidente: 4 de cada 10 consumidores brasileños no vuelven tras un incidente (Akamai, 2024)
- Costo de respuesta: equipo de respuesta a incidentes, asesoría legal, comunicación, forense, retención de clientes
- Reputación: años de recuperación
La cadena se reportó, se validó con una PoC reproducible en 10 pasos (colección de Postman), y el cliente la corrigió en ~72h. Nunca fue explotada por un atacante real, hasta donde se sabe.
Por qué esto pasó desapercibido en auditorías anteriores
3 motivos prácticos:
1. El scanner no arma cadenas
Cada eslabón aislado es medio — o sea, un scanner automatizado generaría 5 alertas “medias” desconectadas, que el equipo probablemente despriorizaría (“tenemos cosas peores que resolver”). Ningún scanner conecta “upload sin auth” + “extensión .phar aceptada” + “ejecución en el webroot” + “config.ini legible” + “MD5 sin salt” + “API REST con secreto compartido” + “la sesión no rota” y dice “esto se vuelve un ATO de admin”.
2. La auditoría de checklist mira por categoría, no por cadena
Las auditorías estilo “ISO 27001 controls check” miran si la empresa tiene controles en cada categoría, no si los controles encadenados sobreviven a un atacante creativo. Resultado: la empresa marcaría el 90% de los checks en verde y aun así tendría la cadena activa.
3. Adianti Framework expone mucho por default
Adianti tiene defaults legados (de 2014–2018) que tenían sentido para ERPs en red interna. En un SaaS multi-tenant expuesto en la internet pública en 2026, esos defaults se vuelven superficie de ataque. Quien no sabe que “AdiantiUploaderService público sin auth” es una vulnerabilidad, no lo busca. Adianti tiene miles de instalaciones brasileñas con esos mismos defaults.
Cómo auditarlo tú mismo (10 minutos)
Si tu plataforma usa Adianti (o cualquier framework PHP), valida los 7 eslabones:
- Lista todas las clases “públicas” de tu framework. ¿Cuáles son accesibles sin autenticación? Cada una es una puerta de entrada potencial.
- Para los servicios de upload: valida las extensiones con una allowlist (no blocklist). Incluye
.phar,.phtml,.pht,.svg,.htmlen la blocklist explícita. - Directorio de upload: ¿está fuera del webroot? Si está dentro, ¿la ejecución PHP está deshabilitada (
.htaccesso config de nginx)? - Archivos de configuración:
application.ini/.env/config.php— ¿qué permisos tienen? ¿Quién puede leerlos? Si el PHP web puede leerlos, un atacante con RCE puede leerlos. - Hash de contraseñas: ¿estás usando
password_hash()(nativo de PHP, bcrypt/argon2)? ¿O MD5/SHA1? - APIs internas: ¿la autenticación es por JWT firmado o por secreto compartido? Si es compartido, se filtra junto con la config.
- Sesiones: ¿tu framework rota el
session_iden cada cambio de estado de auth? PHP nativo NO lo rota por defecto — hace faltasession_regenerate_id(true).
Cada ítem lleva ~1-2 minutos verificarlo. Si 3+ están vulnerables, estás expuesto a esta categoría de cadena.
La lección más importante
La diferencia entre el equipo vulnerable y el equipo seguro no es el talento. Es el proceso de validación adversarial:
- Code review enfocado en cadenas, no en bugs aislados
- Pentest periódico que valida el “cómo” encadena el atacante
- Actualización de los defaults legados del framework
- Pruebas automatizadas que validan toda la cadena de autorización
Si tu plataforma es PHP + un framework (Adianti, Laravel, Symfony, CodeIgniter), o cualquier stack legado, vale la pena auditar específicamente este patrón antes de descubrirlo por las malas.
Qué hacer hoy
- Mapea las clases/endpoints públicos de tu framework. ¿Qué hace cada uno?
- Audita los endpoints de upload con las 7 pruebas de arriba.
- Si no tienes un pentest documentado en los últimos 12 meses, no sabes si tu plataforma está expuesta. Solo sabes que todavía no fue atacada.
- Si descubres 1+ eslabón vulnerable, contrata un pentest enfocado en cadenas.
Para conversar sobre un pentest enfocado en cadenas de ATO:
Si quieres saltar a la acción: solicitar un pentest. NDA bilateral en 24h.
Siguiente paso
¿Quieres aplicar esto a tu sistema?
No Vuln hace pentests en profundidad con la misma metodología descrita en este artículo. Solicita una propuesta: NDA bilateral en 24h y alcance definido en una llamada técnica.