# Tearing the APK apart: the static pass that finds a critical before you set a proxy

> The APK on your disk is your target's source code, config, and secrets in one zip. One curl against a Firebase URL you grep out of strings.xml can be a world-readable database — the whole report, before you intercept a single request. Here's the static pass that finds it.

**Key takeaways:**
- The manifest is the map: read AndroidManifest.xml first for exported components, deeplink schemes, and the debuggable / allowBackup / cleartext / network_security_config flags that tell you the TLS posture before you try to intercept.
- Grep out any firebaseio.com URL and curl its /.json — if it returns data instead of Permission denied, the Realtime Database is world-readable, and often world-writable. One of the most common criticals in mobile, straight from the zip.
- The high-impact chains are structural: an exported ContentProvider with path traversal reads arbitrary app files, and an exported activity that loads an intent URL into a WebView with addJavascriptInterface hands an attacker a bridge to native methods.
- Static feeds dynamic: extracted endpoints become proxy targets, extracted tokens become identities, and the pinning code you located tells you exactly what to unpin at runtime.

**Series:** Mobile Lab  
**Published:** July 5, 2026 · 7 min read  
**Canonical:** https://crusaderproxy.com/research/en/posts/reading-the-apk-static-analysis/

---


Most mobile write-ups treat static analysis as the boring prelude — decompile, skim, "note any hardcoded secrets," then get to the real work of intercepting traffic. That framing is why testers miss criticals that were sitting in a zip file the whole time. The dynamic pass only shows you what the app does *while you drive it*. The static pass shows you the endpoints behind feature flags, the components every other app on the device can reach, and the one Firebase URL that turns into a report before you've opened a proxy.

> **An APK is your target's source code, configuration, and secrets shipped to your disk in a zip. The secrets aren't the finding — the finding is that the server still trusts them.**

Client-side secrets are not secret. The entire app is on your machine; nothing in it is hidden, only obfuscated. So the whole static pass reduces to one question asked over and over: *the app trusts this — does the server?*

## Getting the app open

Pull the APK off a rooted or emulated device (`adb pull`), a mirror, or the store. Modern apps ship split — a base plus per-density and per-ABI splits, or a `.aab` bundle — so **merge the splits first** or you'll decompile the base and silently miss the module that holds the networking code. Then two views, two roles:

- **apktool** — decodes `AndroidManifest.xml`, the resources (`res/values/strings.xml`), and disassembles the bytecode to smali. This is your configuration and attack-surface view.
- **jadx** — reconstructs readable Java. This is where you *read the logic* — where a secret is used, how a deeplink URL is parsed, what a `@JavascriptInterface` method actually does.

```
apktool d target.apk -o target_apktool
jadx -d target_jadx target.apk
```

## The manifest is the map — read it first

`AndroidManifest.xml` is the single highest-value file in the archive, because it tells you the entire external attack surface and the TLS posture before you touch the network.

- **Exported components.** Every `activity`, `service`, `receiver`, or `provider` with `android:exported="true"` (or an intent-filter that implies it) is reachable by *any other app on the device*. This is the list of doors that don't require you to be inside the app.
- **Deeplinks.** The `<intent-filter>` schemes and hosts (`myapp://`, `https://app.example/...`) are untrusted input surfaces — the exact places the app turns an attacker-supplied URL into an action.
- **The flags that decide your day.** `android:debuggable="true"` means you can `run-as <pkg>` and read the app's private data directory on any device. `android:allowBackup="true"` means `adb backup` pulls the app's private files off the device with no root. `usesCleartextTraffic` and the referenced **`network_security_config`** tell you the pinning and CA-trust posture *before* you try to intercept — read it and you know whether you're facing a system-CA install, a config patch, or nothing at all. Those are the exact walls from the [interception guide]({{< relref "posts/intercepting-mobile-apps-that-fight-back.md" >}}), and the manifest tells you which one you're up against before you burn an hour.

## The ten-second critical: the Firebase open-DB test

This is the highest return-on-effort check in mobile, and it lives entirely in static. Grep the decompiled tree — `strings.xml`, resources, smali — for `firebaseio.com` and pull out any project URL. Then ask the database directly:

```
curl https://<project>.firebaseio.com/.json
```

If Firebase security rules are set correctly you get `{"error":"Permission denied"}`. If instead you get **JSON data** — user records, tokens, whatever the app stores — the Realtime Database is world-readable with no authentication. That's a critical, extracted from a zip, no session required. Then test whether it's also world-*writable*:

```
curl -X PUT -d '{"pwn":1}' https://<project>.firebaseio.com/test.json
```

A `200` with your payload echoed back means anyone on the internet can write to the app's database. This misconfiguration is one of the most common criticals in mobile precisely because the URL ships in the client and developers assume nobody reads the client.

## The chains that pay: structure, not string literals

Grepping keys is the tutorial. The high-severity mobile bugs are structural — an exported component wired to a dangerous sink.

**Exported ContentProvider → path traversal → arbitrary file read.** Find a `provider` with `android:exported="true"` (watch for `grantUriPermissions`) that serves files by name without sanitizing the path. Another app — yours — asks for a file that isn't the one intended:

```
content://<authority>/../../../../data/data/<pkg>/databases/app.db
```

If the provider naively joins your path onto its base directory, the `../` sequences climb out of the intended folder and hand you the app's private database, shared-prefs, or token store. A malicious app with zero permissions reads another app's secrets.

**Deeplink → WebView → JS bridge → local file theft or RCE.** This is the gadget worth learning to spot in jadx. Look for an exported activity that takes an **intent-supplied URL** and loads it into a `WebView` configured with all three of:

```java
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setAllowFileAccess(true);
webView.addJavascriptInterface(bridgeObject, "bridge");
```

`addJavascriptInterface` exposes `bridgeObject`'s `@JavascriptInterface`-annotated methods to *any JavaScript running in that WebView*. Those annotated methods are the sink. Because the activity is exported and takes the URL from the intent, an attacker deeplink can point the WebView at attacker-controlled JS (or a `file:///` page the app itself dropped on disk), and that JS calls `bridge.<method>()` to reach into native code — reading local files, or worse depending on what the bridge exposes. Chase the `@JavascriptInterface` annotations; each one is a native method reachable from a hostile web origin.

## Secrets: grep the shapes, then test the trust

Chase string literals — ProGuard/R8 renames classes to `a.b.c`, but URLs, keys, and error strings usually survive obfuscation intact. `apkleaks` and `MobSF` automate this, but plain grep over the decompiled tree finds most of it:

```
AIza            # Google/Maps API key → test for an unrestricted key = billing abuse
AKIA            # AWS access key id → test what it can do
firebaseio.com  # Realtime DB → curl /.json (see above)
s3.amazonaws.com # bucket → test listing and ACLs
-----BEGIN      # a private key shipped in the client
eyJ             # a hardcoded JWT
client_secret   # OAuth client secret in the app = not a secret
```

Every match is the same question: does the backend scope this? An `AIza` key with no API/referrer restriction is billable at your leisure. A JWT (`eyJ`) baked into the binary is a static identity you can decode and replay. `client_secret` in a mobile app is a contradiction the server may or may not have noticed.

**Native and encrypted secrets.** Run `strings libnative.so` over bundled `.so` files — endpoints and keys survive in native code, and JNI symbol names often leak endpoint hints even when the strings are stripped. When a secret is *encrypted at rest* and static comes up empty, that emptiness is itself the signal: the app decrypts it at runtime, so hook the decryption routine with **Frida** and dump the plaintext as it's produced. That's the static-to-dynamic bridge in miniature — static tells you *where* to hook, dynamic gives you the cleartext.

## From static to dynamic — what you carry over

The static pass isn't the deliverable; it's the map that aims every request you send next:

- Extracted **endpoints** (every `http(s)://`, `/api/`, hostname — including hosts the running app only calls behind a flag) → **targets you point Crusader at**.
- Extracted **tokens and keys** (`eyJ` JWTs, `AIza` keys, bearer tokens) → **identities and auth material to replay**.
- **Exported components and deeplinks** → intents and URLs to fire at the device.
- **The pinning code you located** in the manifest's `network_security_config` and in smali → exactly what to unpin when you move to interception.

## The point

Testers skip the static pass because interception feels like the real work — but the APK is the target handing you its endpoint list, its secrets, and its intent surface for free, with nothing sent and nothing to trip a defense. Read the manifest, grep the shapes, curl the Firebase URL, and you've often found the report before setting a proxy. Everything that survives — the endpoints, the tokens, the pinning code — is the input to the dynamic half.

That's where Crusader takes over: the endpoints you extracted become the requests you replay and fuzz, and the tokens you pulled out of `strings.xml` become the identities you send them as. Static analysis found the doors; the proxy is where you walk through them. [Download Crusader free](https://crusaderproxy.com/#install) and turn the map into traffic.


---

*Original research by Crusader Research. Try Crusader free: https://crusaderproxy.com/#install*
