
Laboratorio de prueba de concepto que demuestra un bypass de autenticación crítico en el acceptInvite de GraphQL de Lago, el cual permite la toma de control de cuentas entre organizaciones mediante la aceptación de invitaciones sin autenticación.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · lago-invite-ato
Análisis: https://abraxaslabs.tech/research/lago-invite-ato
Lago v1.53.0 - GetLago
Mutations::Invites::Accept no tiene AuthenticableApiUser. Create/revoke sí lo tienen. Accept no. register_from_invite carga el usuario por el email de la invitación. Si esa cuenta ya tiene membresías, la contraseña enviada se descarta. AuthToken.encode(user:) se ejecuta igualmente. El JWT está acotado al usuario. Sin claim de organización. El cliente envía x-lago-organization para la organización a la que ese usuario ya pertenece. Incluida la de la víctima. createInvite ya devuelve el token al invitador.
Invita a cfo@victim a una organización desechable, acepta tú mismo la invitación, y eres esa persona en todas las organizaciones en las que ese email ya está presente. El buzón nunca interviene. La contraseña de la víctima no cambia.
| ID | aún sin CVE |
| CWE | CWE-287, CWE-306 |
| CVSS | Crítica: 9.6 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N |
| Producto | Lago / lago-api |
| Afectado | hasta v1.53.0 acceptInvite con email/contraseña |
| Autenticación | admin de organización (organization:members:create); basta con el registro en Cloud |
| Licencia | GNU Affero GPL v3.0 |
| Laboratorio | solo 127.0.0.1 |
Necesitas una organización que controles. Basta con el registro en Cloud (LAGO_DISABLE_SIGNUP es false por defecto). Invita un email de la víctima, acepta sin autenticación con una contraseña ficticia, y luego cambia la cabecera a Victim Corp. Facturas, claves de API, Stripe, wallets.
No es un visitante aleatorio sin cuenta. No es un restablecimiento de contraseña. El inicio de sesión con la contraseña ficticia falla. La contraseña real de la víctima sigue funcionando. La aceptación con Google/Okta vincula el email del IdP y no es este fallo. Un self-host de una sola organización sin el email de nadie más en la base de datos no te da acceso a otro comerciante. Misma base de datos, dos organizaciones, mismo email: sí lo hace.
Mismo árbol que el IDOR de factura de un solo uso de Stripe. Fallo distinto.
Leí Accept (sin AuthenticableApiUser), luego find_or_initialize_by(email:), luego la rama de membresías presentes que omite la contraseña, luego AuthToken.encode(user:). Después la aceptación con Google, que sí comprueba google_oidc["email"] == invite.email. La aceptación con email/contraseña no vincula la contraseña de un usuario existente.
El primer cliente que examine esto invitará una dirección sin usar, la aceptará y obtendrá un usuario de Lago completamente nuevo con la contraseña que envió. Esa es la funcionalidad. La prueba es que vuelve el id del usuario existente de la víctima.
Pistas falsas ya registradas: aceptar con Google/Okta; tratar el 401 de createInvite sin un JWT como la historia completa (create está autenticado, accept no); mirar solo la organización del atacante en currentUser (la señal es Victim Corp en la lista); que la contraseña ficticia inicie sesión (eso significaría que el hash fue sobrescrito - el laboratorio confirma que no lo fue); una reverse shell. Teatro. Sin bandeja de entrada.
Luego: dos llamadas a registerUser crean Victim Corp y Attacker Corp. Correos electrónicos nuevos en cada ejecución para que un volumen sucio no provoque un 422 user_already_exists. createInvite sobre el correo de la víctima. acceptInvite sin autenticación con una contraseña ficticia. El id de usuario devuelto coincide con el de la víctima. currentUser.organizations lista ambas. El cambio de cabecera devuelve Victim Corp.
cd lab
./run.sh
Dirígete solo a http://127.0.0.1:13000. El IDOR de Stripe utilizó 13001. Sin puerto de UI.
IOC victim_password_unchanged
IOC victim_real_password_still_works
SUCCESS LAGO-INVITE-ATO acceptInvite JWT is existing victim; lists Victim Corp; no inbox
Si el usuario ya existe, exija que se autentique como él mismo (o que demuestre la titularidad del buzón / IdP) antes de vincular la membresía. No emita un JWT para un usuario existente a partir de una aceptación no autenticada. Deje de devolver el token de invitación al invitador si este constituye una capacidad.
accept.rb · accept_service.rb · users_service.rb · create.rb · object.rb · google_service.rbGNU Affero GPL v3.0. Consulte LICENSE. Solo para laboratorio en loopback. Sin garantía.