
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
Lago v1.53.0 - GetLago
Mutations::Invites::Accept no tiene AuthenticableApiUser. Create/revoke sí. 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:) igual se ejecuta. 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, acéptala tú mismo, y eres esa persona en cada organización en la que ese email ya esté. 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ítico: 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 |
| Auth | admin de organización (organization:members:create); basta con registrarse en Cloud |
| Licencia | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
Necesitas una organización que controles. Basta con registrarse 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 login ficticio falla. La contraseña real de la víctima sigue funcionando. El accept de Google/Okta vincula el email del IdP y no es este bug. 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. La misma base de datos, dos organizaciones, el mismo email: sí lo hace.
Mismo árbol que el IDOR de factura única de Stripe. Bug 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:). Luego el accept de Google, que sí comprueba google_oidc["email"] == invite.email. El accept con email/contraseña no vincula la contraseña de un usuario existente.
El primer cliente que mire 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. El testigo es el id del usuario víctima existente que regresa.
Desvíos ya registrados: aceptar con Google/Okta; tratar el 401 de createInvite sin un JWT como toda la historia (create está autenticado, accept no); mirar solo la organización del atacante en currentUser (la pista es Victim Corp en la lista); que la contraseña ficticia inicie sesión (eso significaría que el hash fue sobrescrito - el lab afirma que no lo fue); una reverse shell. Teatro. Sin bandeja de entrada.
Luego: dos llamadas a registerUser plantan Victim Corp y Attacker Corp. Emails nuevos en cada ejecución para que un volumen sucio no devuelva 422 user_already_exists. createInvite sobre el email 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
Objetivo solo http://127.0.0.1:13000. El IDOR de Stripe usó 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, exige que se autentique como él mismo (o que demuestre el buzón / IdP) antes de adjuntar la membresía. No emitas un JWT para un usuario existente desde un accept sin autenticación. Deja de devolver el token de la invitación al invitador si es una capacidad.
accept.rb · accept_service.rb · users_service.rb · create.rb · object.rb · google_service.rbGNU Affero GPL v3.0. Ver LICENSE. Solo lab en loopback. Sin garantía.