
Lab de preuve de concept démontrant un contournement d'authentification critique acceptInvite dans Lago GraphQL qui permet une prise de contrôle de compte inter-organisations via l'acceptation d'invitation non authentifiée.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · lago-invite-ato
Write-up : https://abraxaslabs.tech/research/lago-invite-ato
Lago v1.53.0 - GetLago
Mutations::Invites::Accept n'a pas de AuthenticableApiUser. Create/revoke en ont. Accept n'en a pas. register_from_invite charge l'utilisateur par l'email de l'invitation. Si ce compte possède déjà des appartenances, le mot de passe soumis est ignoré. AuthToken.encode(user:) s'exécute quand même. Le JWT est lié à l'utilisateur. Aucune revendication d'organisation. Le client envoie x-lago-organization pour l'organisation à laquelle cet utilisateur appartient déjà. Y compris celle de la victime. createInvite renvoie déjà le jeton à l'inviteur.
Invitez cfo@victim dans une organisation jetable, acceptez l'invitation vous-même, et vous êtes cette personne sur toutes les organisations où cet email figure déjà. La boîte mail n'est jamais impliquée. Le mot de passe de la victime reste inchangé.
| ID | pas encore de CVE |
| CWE | CWE-287, CWE-306 |
| CVSS | Critique : 9.6 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N |
| Produit | Lago / lago-api |
| Affecté | jusqu'à v1.53.0 acceptInvite email/mot de passe |
| Auth | admin d'organisation (organization:members:create) ; l'inscription Cloud suffit |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 uniquement |
Vous avez besoin d'une organisation que vous contrôlez. L'inscription Cloud suffit (LAGO_DISABLE_SIGNUP vaut false par défaut). Invitez l'email d'une victime, acceptez sans authentification avec un mot de passe factice, puis basculez l'en-tête vers Victim Corp. Factures, clés API, Stripe, portefeuilles.
Pas un visiteur aléatoire sans compte. Pas une réinitialisation de mot de passe. La connexion factice échoue. Le vrai mot de passe de la victime fonctionne toujours. L'acceptation Google/Okta lie l'email de l'IdP et ne relève pas de ce bug. Un self-host mono-organisation sans l'email d'un tiers dans la base ne vous donne pas accès à un autre marchand. Même base de données, deux organisations, même email : oui.
Même arborescence que l'IDOR sur facture à usage unique Stripe. Bug différent.
J'ai lu Accept (pas de AuthenticableApiUser), puis find_or_initialize_by(email:), puis la branche où les appartenances existent qui saute le mot de passe, puis AuthToken.encode(user:). Ensuite l'acceptation Google, qui vérifie bien google_oidc["email"] == invite.email. L'acceptation email/mot de passe ne lie pas le mot de passe d'un utilisateur existant.
Le premier client qui se penche là-dessus invitera une adresse inutilisée, l'acceptera, et obtiendra un tout nouvel utilisateur Lago avec le mot de passe qu'il a soumis. C'est la fonctionnalité. Le témoin, c'est l'id de l'utilisateur existant de la victime qui revient.
Fausses pistes déjà consignées : accepter avec Google/Okta ; considérer le 401 de createInvite sans JWT comme toute l'histoire (create est authentifié, accept ne l'est pas) ; ne regarder que l'organisation de l'attaquant sur currentUser (l'indice, c'est Victim Corp dans la liste) ; la connexion avec le mot de passe factice (cela signifierait que le hash a été écrasé - le lab affirme que non) ; un reverse shell. Du théâtre. Pas de boîte mail.
Ensuite : deux appels registerUser créent Victim Corp et Attacker Corp. Des e-mails neufs à chaque exécution afin qu'un volume sale ne provoque pas de 422 user_already_exists. createInvite sur l'e-mail de la victime. acceptInvite non authentifié avec un mot de passe factice. L'id utilisateur retourné correspond à celui de la victime. currentUser.organizations liste les deux. Le changement d'en-tête renvoie Victim Corp.
cd lab
./run.sh
Cibler uniquement http://127.0.0.1:13000. L'IDOR Stripe utilisait 13001. Aucun port d'interface.
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 l'utilisateur existe déjà, exigez qu'il s'authentifie en tant que lui-même (ou prouve la boîte aux lettres / l'IdP) avant d'attacher l'adhésion. Ne générez pas de JWT pour un utilisateur existant à partir d'une acceptation non authentifiée. Cessez de renvoyer le token d'invitation à l'inviteur s'il s'agit d'un jeton de capacité.
accept.rb · accept_service.rb · users_service.rb · create.rb · object.rb · google_service.rbGNU Affero GPL v3.0. Voir LICENSE. Laboratoire en boucle locale uniquement. Aucune garantie.