
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
Lago v1.53.0 - GetLago
Mutations::Invites::Accept n'a pas de AuthenticableApiUser. Create/revoke en ont un. 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 limité à 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 token à l'inviteur.
Invitez cfo@victim dans une organisation jetable, acceptez-le vous-même, et vous êtes cette personne sur toutes les organisations où cet email est déjà présent. 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 est faux par défaut). Invitez un email de victime, acceptez sans authentification avec un mot de passe factice, puis basculez l'en-tête vers Victim Corp. Factures, clés API, Stripe, wallets.
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 n'est pas ce bug. Un self-host mono-organisation sans l'email de quelqu'un d'autre 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 arbre que le Stripe one-time invoice IDOR. Bug différent.
J'ai lu Accept (pas de AuthenticableApiUser), puis find_or_initialize_by(email:), puis la branche où les appartenances sont présentes 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 regarde ça 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 est l'id de l'utilisateur victime existant qui revient.
Fausses pistes déjà consignées : accepter avec Google/Okta ; traiter 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 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 plantent Victim Corp et Attacker Corp. Des emails neufs à chaque exécution pour qu'un volume sale ne provoque pas de 422 user_already_exists. createInvite sur l'email de la victime. acceptInvite non authentifié avec un mot de passe factice. L'id utilisateur retourné correspond à la victime. currentUser.organizations liste les deux. Le basculement d'en-tête renvoie Victim Corp.
cd lab
./run.sh
Cible uniquement http://127.0.0.1:13000. Le Stripe IDOR utilisait 13001. Pas de port 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 l'utilisateur existe déjà, exiger qu'il s'authentifie en tant que lui-même (ou prouve la boîte mail / l'IdP) avant d'attacher l'appartenance. Ne pas générer de JWT pour un utilisateur existant à partir d'une acceptation non authentifiée. Cesser de renvoyer le token d'invitation à l'inviteur s'il s'agit d'un capability.
accept.rb · accept_service.rb · users_service.rb · create.rb · object.rb · google_service.rbGNU Affero GPL v3.0. Voir LICENSE. Lab en loopback uniquement. Aucune garantie.