← All write-ups
June 15, 2025CRITICALFINDING MISSION

APK → Firestore : credentials en clair, BAC et prise de contrôle de comptes

Finding de mission (client anonymisé). Découvert et exploité par 34zY / Hamza Terir ; chaîne d’exploitation et générateur de rapport développés from scratch. Aucune donnée client réelle (identifiants, domaines, noms) n’est reproduite ici.

Contexte

Application mobile d’un client, adossée à un backend Firebase / Firestore. L’APK est distribuée publiquement sur les stores. L’authentification et la base de données utilisateurs reposent sur les règles de sécurité Firestore.

Comment je l’ai trouvée

En décompressant l’APK publique, j’ai récupéré les fichiers de configuration embarqués (.env, manifest). Ils contenaient des credentials applicatifs en clair (un couple e-mail / mot de passe de service) et la clé API Firebase.

À partir de là, la chaîne s’est déroulée :

  1. Authentification avec les credentials fuités → idToken valide.
  2. Broken Access Control : GET /users (authentifié) retourne tous les documents utilisateurs, sans aucun contrôle d’accès par UID. Même requête sans token → 403. Le différentiel 200/403 confirme le BAC.
  3. Les documents contiennent les mots de passe en clair (aucun hachage) de tous les utilisateurs.
  4. Account Takeover : réauthentification avec un mot de passe volé → idToken d’une victime (localId distinct).
  5. Écriture : PATCH / DELETE sur les documents acceptés → modification ou suppression de données arbitraires.
  6. Le token de la victime déverrouille des API backend jusque-là en 401.

Workflow technique

L’analyse initiale était hors ligne, sur une copie de l’APK :

apkanalyzer manifest print app-release.apk
unzip -l app-release.apk | rg -i 'google-services|\.env|config'
sha256sum app-release.apk

La vérification d’autorisation utilisait deux comptes synthétiques et un objet de test par compte. Une règle de base est de lier la lecture à l’UID vérifié, jamais à un identifiant fourni par le mobile :

def can_read_user(auth_uid: str | None, document_uid: str) -> bool:
    return bool(auth_uid) and auth_uid == document_uid

def require_owner(auth_uid: str | None, document_uid: str) -> None:
    if not can_read_user(auth_uid, document_uid):
        raise PermissionError("document is outside the caller scope")

Le retest contrôle aussi la rotation, l’absence de mots de passe réutilisables et l’égalité de réponse entre un objet inexistant et un objet appartenant à un autre compte.

La faille

Cumul de fautes : credentials fixés en dur dans un artefact public, stockage des mots de passe en clair, et surtout absence de règles Firestore restreignant la lecture au propriétaire du document. La collection /users était lisible en entier par n’importe quel compte authentifié.

Impact

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 10.0 (Critical).

Remédiation

CWE-284 (Improper Access Control) · CWE-798 (Hard-coded Credentials) · CWE-521 (Weak Password Requirements).