Skip to Content
Alpha — full Android pipeline + iOS toolkit + live dynamic loop. API still shifting; pin to commits in CI.
WorkflowsPlayIntel

PlayIntel — Mobile credential & Firebase reconnaissance

PlayIntel is a MedusaNexus engine that recovers backend identifiers and embedded credentials from Android APKs, then runs a small set of harmless active probes against any Firebase project it discovers. It is a Python port of the internal Go scanner go-google-login, integrated as a first-class head of the hydra alongside apktool, jadx, mobsf, ghidra, etc.

Pure Python, zero new runtime dependencies. Everything — the Google Play protocol client, the protobuf wire-format codec, the resources.arsc parser, the secret-pattern engine, the active probes, and the email + password → AAS token crypto path — runs against the existing MedusaNexus dependency set (httpx + stdlib).

What it does

Given either a local APK file or a Google Play package name, the engine:

  1. Streams the APK — issues HTTP Range requests for only the zip central directory plus the high-value entries (resources.arsc, google-services.json, JS bundles, .NET assemblies, .pem files). On a 100 MB APK this transfers ~5–10 MB end-to-end.
  2. Parses the resource table — a hand-rolled resources.arsc parser extracts every string resource (Firebase project ID, google_api_key, firebase_database_url, gcm_defaultSenderId, default_web_client_id, google_app_id, google_storage_bucket, plus thousands of unrelated strings used for entropy-filtered secret detection).
  3. Detects credentials — runs a regex+entropy detector (~25 confirmed patterns: OpenAI, Anthropic, AWS, Stripe, Slack, GitHub, FCM legacy server keys, PEM private keys, …; plus a separate “suspected” tier gated on Shannon entropy ≥ 3.0). AKIA / ASIA access-key IDs trigger a paired-secret search inside a 1024-byte window, with extra entropy and hex-only filters to drop SHA-1s. google-api-client SDK test-key fingerprints are filtered out.
  4. Probes server-side rules (optional) — for each unique Firebase project ID, hits Realtime Database, Cloud Firestore, and Cloud Storage with anonymous requests. Reports public read/write misconfiguration. RTDB region-redirects are followed once with an anti-SSRF host allow-list. The write probe targets a dedicated _scanner_probe.json child key and self-cleans on success.
  5. Persists bearing files — files containing a confirmed credential or a Firebase project ID are saved to <workspace>/secrets/<package>/ so the analyst can re-inspect them offline.
  6. Emits findings — translates everything into MedusaNexus Finding objects with appropriate CWE / OWASP-Mobile / MASVS tags, so the rest of the platform (UI, reports, hooks) renders them like any other engine output.

Design

mnexus/playintel/ ├── arsc.py # AOSP resources.arsc parser (string-type entries) ├── secret_detector.py # regex + entropy + AKIA-pair correlator ├── firebase_config.py # google-services.json + ARSC → FirebaseConfig ├── scan_targets.py # zip-entry whitelist ├── remote_zip.py # HTTP Range zip reader + LocalZip adapter ├── zip_entry_scanner.py # per-entry routing → ScanZipResult ├── scan_report.py # thread-safe aggregator ├── firebase_probes.py # RTDB / Firestore / Storage active probes ├── protobuf_codec.py # pure-Python protobuf wire-format encode/decode ├── device_props.py # Pixel 7a device fingerprint + checkin builder ├── google_auth.py # email + password → AAS token (RSA-OAEP-SHA1) ├── play_client.py # /auth + /checkin + /details + /purchase + /delivery ├── apk_source.py # pluggable: LocalAPKSource | DirectURLSource | PlayProtocolSource └── analyzer.py # high-level orchestration → AnalysisOutcome mnexus/engines/play_intel_engine.py # MedusaNexus engine wrapper

Pluggable APK source

The analyzer never branches on where bytes come from. Three sources implement the same protocol:

SourceUsed forNotes
LocalAPKSourceAny local .apk or .xapk fileNo network. Default for ingest_apk flow.
DirectURLSourcePre-resolved CDN URL + size + headersWhen another tool already did the Play handshake.
PlayProtocolSourceNative Google Play protocolPure-Python PlayClient — auth, checkin, details, purchase, delivery. Default for play-scan.

Native Play protocol stack

PlayProtocolSource wraps PlayClient, a pure-Python implementation of the protocol:

  1. Authentication — POSTs email + AAS token to https://android.clients.google.com/auth form-encoded. Parses the Auth= / Expiry= lines from the text response. The bearer token is cached and refreshed 5 minutes before expiry.
  2. Device check-in — first run only. POSTs an AndroidCheckinRequest protobuf to /checkin; the response’s androidId (fixed64 field 7) is the freshly minted GSFID, persisted back into playintel.ini for future runs.
  3. DetailsGET /fdfe/details?doc=<pkg> and walks the response ResponseWrapper → Payload → DetailsResponse → Item → DocumentDetails → AppDetails to extract versionCode.
  4. PurchasePOST /fdfe/purchase (free apps still require this) to obtain an encodedDeliveryToken.
  5. DeliveryGET /fdfe/delivery returns the signed CDN URL plus any splits and OBB additional files.

Protobuf encoding/decoding is handled by the bundled protobuf_codec module — no protobuf runtime dependency. The codec is the foundational primitive: ~250 lines covering wire types 0/1/2/5, varint encoding (including 64-bit two’s-complement for negative ints), zigzag, MGF1-friendly fixed-width reads, repeated-field iteration, and a find_path helper that walks length-delimited sub-message chains.

Account manager

PlayIntel ships a proper account manager — multiple Play identities can be stored side-by-side, one is marked default, and the /play-scan calls pick which to use via --account <name> / account_name JSON field. The store is the same SQLite that holds projects and findings (play_accounts table; the file is chmod 0600 on init since AAS tokens are sensitive at rest).

Identity precedence when no explicit name is given:

  1. Account flagged is_default in the store.
  2. Env vars PLAYINTEL_EMAIL + PLAYINTEL_AAS_TOKEN — for stateless containers / CI runs that don’t bring a sqlite file.
  3. Anything else fails fast with the documented setup hint.

mnexus play-account is the CLI surface:

mnexus play-account add --name research-1 --email me@gmail.com --password '<pw>' mnexus play-account add --name qa-pixel --email qa@gmail.com --aas 'aas_et/...' mnexus play-account list mnexus play-account use qa-pixel # promote to default mnexus play-account show research-1 # token is redacted mnexus play-account delete research-1

add accepts either --aas <token> (existing master token) or --password <pw> (mints AAS via /auth using RSA-OAEP-SHA1 against Google’s GMS public key, all in pure Python; the password itself is never persisted — only the resulting aas_et/... token goes to disk). 2FA accounts need an app password .

The first account added auto-promotes to default so the happy path ends in one command.

REST surface mirrors the CLI:

GET /v1/playintel/accounts — list (redacted) POST /v1/playintel/accounts — create from email + (aas | password) DELETE /v1/playintel/accounts/{name} — remove POST /v1/playintel/accounts/{name}/default — promote

The web UI has a dedicated PLAY ACCOUNTS page and a “scan as” dropdown on the PLAY SCAN form that populates from this same endpoint.

What the manager does not do

It does not create Google accounts. The account creation flow is gated by Google’s anti-abuse machinery (CAPTCHA, phone verification, behavioral fingerprinting); automating it via Selenium / web-form scraping puts the tool squarely on the abuse side regardless of intent, and the resulting accounts are typically suspended within 24-72 hours alongside the AAS tokens they minted. Create the account manually (one-off, in a browser, with a phone number you control), then register it here.

Findings

Severities are tuned conservatively — they describe the credential’s blast radius, not whether the specific token is currently active:

ClassSeverity
GCP Service Account JSONCRITICAL
PEM private keyCRITICAL
RTDB / Firestore / Storage open to worldCRITICAL / HIGH
FCM Server Key, AWS Key Pair, Stripe live key, Slack/GitHub/SendGrid/Twilio tokenHIGH
OneSignal, Vercel, HuggingFace tokensMEDIUM
Generic api_key= / JWT (suspected tier)MEDIUM
Firebase project identifiers (informational)INFO

Every CRITICAL / HIGH finding ships a code-level remediation block — that’s enforced at construction time by Finding.model_validator.

What the engine does not do

  • Mint Firebase auth tokens. The Go reference scanner can signInAnonymously / signInWithIdp to test “auth required” rules. That path requires a working anonymous-auth provider on the target project (or a leaked OAuth client secret) and is left to a future iteration.
  • Touch user data. The RTDB write probe is the only mutating call, targets a dedicated child path, and self-cleans on success. Firestore and Storage probes are read-only.

Usage

One-time setup (Play streaming)

mnexus play-account add --name primary --email me@gmail.com --password '<pw>' # or, if you already have an AAS token from elsewhere: mnexus play-account add --name primary --email me@gmail.com --aas 'aas_et/...'

The token (or minted token) and the email are stored in the same SQLite the rest of the platform uses. The first account auto-promotes to default; subsequent additions don’t clobber the existing default unless you pass --default.

CLI — interactive REPL (slash command)

🔱 nexus ❯ /play-account add --name primary --email me@gmail.com # interactive prompt for the secret 🔱 nexus ❯ /play-account list 🔱 nexus ❯ /play-account use research-1 🔱 nexus ❯ /play-scan com.example.app 🔱 nexus ❯ /play-scan com.example.app --account research-1

Optional flags inside the REPL:

  • /play-scan <pkg> --apk <local-file> — bypass Play and use a local APK.
  • /play-scan <pkg> --account <name> — scan as a specific stored identity.
  • /play-scan <pkg> --no-probes — static-only scan (no outbound traffic).

CLI — flat subcommands

mnexus play-scan com.example.app mnexus play-scan com.example.app --apk ~/Downloads/target.apk mnexus play-scan com.example.app --account research-1 mnexus play-scan com.example.app --no-probes

Web UI

Sidebar:

  • PLAY SCAN (#/play-scan) — package input, source-mode switcher (PLAY STREAM / LOCAL PATH / UPLOAD .APK), “scan as” account dropdown, active-probes toggle. Results panel renders Firebase project IDs, confirmed secrets, active-probe vulnerabilities, and the engine’s emitted findings.
  • PLAY ACCOUNTS (#/play-accounts) — register / list / promote / delete stored Play identities. Tokens never leave the server in responses.

REST API

POST /v1/playintel/scan Content-Type: application/json { "package": "com.example.app", "apk_path": "/optional/local.apk", # bypass Play streaming "account_name": "research-1", # pick stored identity (default if unset) "run_active_probes": true } POST /v1/playintel/scan-upload # multipart APK upload + scan fields: file (.apk), package, run_active_probes GET /v1/playintel/accounts # list (redacted) POST /v1/playintel/accounts # create from email + (aas_token | password) DELETE /v1/playintel/accounts/{name} # remove POST /v1/playintel/accounts/{name}/default

Response:

{ "package": "com.example.app", "source": "play-bridge", "firebase_projects": ["…"], "confirmed_secrets": [{"type": "OpenAI API Key", "location": "…"}], "suspected_secrets_count": 3, "vulnerabilities": ["Realtime Database public access: …"], "findings": [{"id": "FND-…", "title": "…", "severity": "high", "category": "…", "location": "…"}], "saved_files_dir": "/workspace/secrets/com.example.app" }

Programmatic

from pathlib import Path from mnexus.config import NexusConfig from mnexus.engines.play_intel_engine import PlayIntelEngine from mnexus.playintel.apk_source import LocalAPKSource, PlayBinarySource config = NexusConfig.from_env() engine = PlayIntelEngine(config) # Local file: source = LocalAPKSource(Path("./target.apk")) outcome, findings = await engine.analyze_package( "com.example.app", source=source, workspace=config.workspace, run_active_probes=False, ) # Or stream from Play (requires the Go bridge binary on PATH): source = PlayBinarySource() outcome, findings = await engine.analyze_package( "com.example.app", source=source, workspace=config.workspace, run_active_probes=True, ) print(outcome.report.confirmed_secrets()) print(outcome.report.vulnerabilities)

Operating notes

Severity framing — AIza* keys are not secrets

AIzaSy* API keys are project identifiers — Firebase / Maps SDKs are designed to ship them inside client APKs. Their disclosure is unavoidable; what determines actual risk is server-side configuration:

  1. API-key application restrictions — Android package + SHA-1 signing certificate, plus a whitelist of allowed Google APIs.
  2. Firebase Security Rules — for Realtime Database, Firestore, Cloud Storage.
  3. Firebase App Check — Play Integrity attestation, enforced on Firestore / RTDB / Storage / Cloud Functions.

The engine reflects this: a recovered FirebaseConfig produces an INFO finding (informational, no remediation required). The CRITICAL / HIGH findings come from the active probes — they demonstrate that the rules don’t actually keep an anonymous attacker out.

Test data policy

All tests use synthetic inputs. The ARSC test suite includes a small encoder helper (tests/playintel/test_arsc.py::_build_minimal_arsc) that constructs a valid resource-table blob in memory. The analyzer integration test builds a tiny synthetic APK with a fabricated google-services.json and a fake credential. Do not commit real-world target data — recovered credentials, customer-data fixtures, or named-app case studies belong in one-off deliverables, not in the repo.

Adding a new credential pattern

Edit mnexus/playintel/secret_detector.py:

  • Add to SECRET_PATTERNS if the pattern is vendor-specific and rarely produces false positives. The pattern must match the issuer’s published format prefix (sk_live_, ghp_, etc.).
  • Add to SUSPECTED_SECRET_PATTERNS if it’s a generic key=value shape. It will only fire when no confirmed pattern matched the same value AND Shannon entropy ≥ MIN_SECRET_ENTROPY (3.0).
  • Set the severity in mnexus/engines/play_intel_engine.py::_SECRET_SEVERITY. Default is MEDIUM; reach for HIGH or CRITICAL only when the credential class unlocks real customer data.

Add a test under tests/playintel/test_secret_detector.py that exercises both the positive case and a near-miss that should not trigger.

Performance characteristics

  • Range-fetched APK scan: dominated by HTTP round-trip latency, not bandwidth. RemoteZip.prefetch_entries issues one ranged GET per whitelisted entry to avoid the cold-cache pattern of zipfile-internal small reads.
  • resources.arsc parsing: linear in the size of the global string pool. McDonald’s-class APKs (~15 MB arsc, 6k+ string resources) parse in tens of milliseconds.
  • Active probes: 3 HTTP round-trips per Firebase project (RTDB read, RTDB write+cleanup, Firestore listCollectionIds, Storage listObjects), bounded by httpx.Client(timeout=10s).

Active probe orchestration (standalone)

Two endpoints fire probes outside the full ingest pipeline — useful when you already have a Firebase config and just want the RTDB/Firestore/Storage readout:

POST /v1/firebase/probe Content-Type: application/json {"project_id": "myapp-prod", "api_key": "AIza…", "storage_bucket": "myapp-prod.appspot.com", "database_url": "https://myapp-prod-default-rtdb.firebaseio.com"}

All four fields are optional individually; probes skip a service when its inputs are missing. Returns {rtdb, firestore, storage, vulnerable} with one block per service that ran.

POST /v1/projects/{id}/firebase/probe

Walks the project’s most recent PlayScanRecord, finds every recovered Firebase config, runs all three probes per unique project_id. 404 when no prior play-scan exists for the project (run /v1/projects/{id}/play-scan first to recover configs from the APK).

Both endpoints make read-only requests against the target services — they never write. The vulnerable flag in the per-service block mirrors the same semantics the in-flow analyser uses (public read on RTDB / public read on Firestore documents / public listing on Storage).

Files of interest

  • mnexus/playintel/arsc.py — resources.arsc parser; references AOSP frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h.
  • mnexus/playintel/secret_detector.py — pattern catalog + entropy filter + AKIA-pair correlator.
  • mnexus/playintel/firebase_probes.py — active-probe implementations, including the RTDB region-redirect handling and SSRF allow-list.
  • mnexus/engines/play_intel_engine.py — engine wrapper; severity table; finding emission.
  • tests/playintel/test_arsc.py — synthetic ARSC encoder used as a test fixture.