Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/mobile-pentesting/android-app-pentesting/webview-attacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,39 @@ Detection tips<sup>[[10]](#references)</sup>
- Watch for large Base64 strings returned via `evaluateJavascript` when using the app.
- Grep decompiled sources for handlers that accept `uri`/`path` and convert them to `new File(...)`.

#### Bridge file write to native-library cache poisoning

Audit **every** `addJavascriptInterface()` registration independently. A domain check in the main bridge does not protect another object registered on the same WebView: injected objects are exposed to every frame, and Android does not provide the bridge method with a trustworthy calling-frame origin. An attacker-controlled page reached through a deep link, redirect, or navigation can therefore call any second interface that remains attached.<sup>[[23]](#references)[[24]](#references)</sup>

A particularly dangerous sink accepts Base64/blob data plus a caller-controlled filename. For example, extracting a name with `#filename=(.+)$` and then calling `new File(downloadsDir, fileName)` permits traversal if the code neither rejects separators/`..` nor verifies the canonical destination remains below the intended directory. A MIME check such as `input.contains("application/vnd.ms-excel")` does not constrain the decoded bytes. Together, these bugs turn an exposed download bridge into an arbitrary-byte file-write primitive at paths writable by the app process.<sup>[[23]](#references)</sup>

One high-impact target is a deterministic native-library cache under `code_cache/`. Custom loaders sometimes extract a library from `base.apk!/lib/<abi>/...`, cache it, and treat `file.exists()` as a valid cache hit. If the bridge can replace that path, the next cold start may pass attacker bytes to `System.load()` without a hash, signature, size, or trusted-manifest check.<sup>[[23]](#references)</sup>

Do not assume a cache file ending in `.so` is an ELF. A loader may cache a ZIP container and load an inner entry with a path such as `cache.so!/lib/arm64-v8a/libTarget.so`. The replacement must reproduce the exact inner path and loader format; directly mapped native entries are normally `STORED` (not deflated) and page-aligned. The demonstrated target required 4 KiB ZIP alignment, while devices supporting 16 KiB pages may require 16 KiB alignment.<sup>[[23]](#references)[[26]](#references)</sup>

```text
cache-file.so # ZIP: PK\x03\x04
└── lib/
└── arm64-v8a/
└── libTarget.so # ELF, STORED and page-aligned
```

When the loader maps the inner ELF, its `DT_INIT`/`DT_INIT_ARRAY` constructors run before `System.load()` returns. This yields native code execution as the application UID, not root. SELinux does not necessarily stop this chain: AOSP policy permits `execute` mapping of `app_data_file` for untrusted apps, whereas the separate `execute_no_trans` permission governs executing a file as a new process without a domain transition.<sup>[[23]](#references)[[25]](#references)</sup>

Useful triage commands for this pattern are:<sup>[[23]](#references)</sup>

```bash
rg -n 'addJavascriptInterface|@JavascriptInterface|System\.load|findLibrary|code_cache|\.exists\(' jadx-src/
adb shell run-as <pkg> find code_cache -type f -ls
adb exec-out run-as <pkg> cat code_cache/<candidate> > candidate.bin
xxd -l 4 candidate.bin # 504b0304 => ZIP, 7f454c46 => ELF
unzip -l candidate.bin # recover the exact inner path
zipinfo -v candidate.bin # verify STORED/compressed state and offsets
adb logcat | grep -E 'linker|System\.load|avc:'
```

Hardening must cover both ends of the chain: remove every bridge before loading untrusted content and keep bridge-enabled WebViews limited to app-controlled documents; checking only the top-level URL cannot authenticate a calling iframe. Canonicalize the destination and require it to remain below the intended directory, then authenticate cached executable content with a digest or signature anchored in trusted app data. File ownership alone is insufficient when the vulnerable bridge writes with the application's own UID.<sup>[[23]](#references)[[24]](#references)</sup>

#### Bypassing WebView privilege gates – endsWith() host checks

Privilege decisions (selecting a JSB-enabled Activity) often rely on host allowlists. A flawed pattern is:<sup>[[10]](#references)</sup>
Expand Down Expand Up @@ -575,5 +608,9 @@ Practical notes:<sup>[[13]](#references)</sup>
- [20] [Android Developers: `OpenableColumns`](https://developer.android.com/reference/android/provider/OpenableColumns)
- [21] [Android Developers: `ParcelFileDescriptor.createPipe()`](https://developer.android.com/reference/android/os/ParcelFileDescriptor#createPipe())
- [22] [Apache Cordova: `resume` event](https://cordova.apache.org/docs/en/latest/cordova/events/events.html#resume)
- [23] [MEXC Android RCE via WebView bridge path traversal and native-library cache poisoning](https://itis911.github.io/writeups/RCE-Mexc-Andriod-App.html)
- [24] [Android Developers - WebView native bridge risks](https://developer.android.com/privacy-and-security/risks/insecure-webview-native-bridges)
- [25] [AOSP SELinux policy for untrusted applications](https://android.googlesource.com/platform/system/sepolicy/+/refs/heads/main/private/untrusted_app_all.te)
- [26] [Android Developers - Support 16 KB page sizes](https://developer.android.com/guide/practices/page-sizes)

{{#include ../../banners/hacktricks-training.md}}