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:
- Streams the APK — issues HTTP
Rangerequests for only the zip central directory plus the high-value entries (resources.arsc,google-services.json, JS bundles, .NET assemblies,.pemfiles). On a 100 MB APK this transfers ~5–10 MB end-to-end. - Parses the resource table — a hand-rolled
resources.arscparser 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). - 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.
- 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.jsonchild key and self-cleans on success. - 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. - Emits findings — translates everything into MedusaNexus
Findingobjects 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 wrapperPluggable APK source
The analyzer never branches on where bytes come from. Three sources implement the same protocol:
| Source | Used for | Notes |
|---|---|---|
LocalAPKSource | Any local .apk or .xapk file | No network. Default for ingest_apk flow. |
DirectURLSource | Pre-resolved CDN URL + size + headers | When another tool already did the Play handshake. |
PlayProtocolSource | Native Google Play protocol | Pure-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:
- Authentication — POSTs
email + AAS tokentohttps://android.clients.google.com/authform-encoded. Parses theAuth=/Expiry=lines from the text response. The bearer token is cached and refreshed 5 minutes before expiry. - Device check-in — first run only. POSTs an
AndroidCheckinRequestprotobuf to/checkin; the response’sandroidId(fixed64 field 7) is the freshly minted GSFID, persisted back intoplayintel.inifor future runs. - Details —
GET /fdfe/details?doc=<pkg>and walks the response ResponseWrapper → Payload → DetailsResponse → Item → DocumentDetails → AppDetails to extractversionCode. - Purchase —
POST /fdfe/purchase(free apps still require this) to obtain anencodedDeliveryToken. - Delivery —
GET /fdfe/deliveryreturns 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:
- Account flagged
is_defaultin the store. - Env vars
PLAYINTEL_EMAIL+PLAYINTEL_AAS_TOKEN— for stateless containers / CI runs that don’t bring a sqlite file. - 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-1add 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 — promoteThe 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:
| Class | Severity |
|---|---|
| GCP Service Account JSON | CRITICAL |
| PEM private key | CRITICAL |
| RTDB / Firestore / Storage open to world | CRITICAL / HIGH |
| FCM Server Key, AWS Key Pair, Stripe live key, Slack/GitHub/SendGrid/Twilio token | HIGH |
| OneSignal, Vercel, HuggingFace tokens | MEDIUM |
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/signInWithIdpto 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-1Optional 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-probesWeb 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}/defaultResponse:
{
"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:
- API-key application restrictions — Android package + SHA-1 signing certificate, plus a whitelist of allowed Google APIs.
- Firebase Security Rules — for Realtime Database, Firestore, Cloud Storage.
- 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_PATTERNSif 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_PATTERNSif it’s a generickey=valueshape. 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_entriesissues one ranged GET per whitelisted entry to avoid the cold-cache pattern ofzipfile-internal small reads. resources.arscparsing: 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/probeWalks 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 AOSPframeworks/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.