Skip to content
Open
Show file tree
Hide file tree
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
27 changes: 27 additions & 0 deletions src/mobile-pentesting/android-app-pentesting/content-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,37 @@ On modern Android versions, direct `/sdcard` paths matter less than **granted**

From an offensive perspective, these URIs are attractive because a vulnerable app may call `takePersistableUriPermission()` after receiving them. That turns a one-shot `content://` access into a **long-lived grant** that can survive app restarts or even device reboots. When chaining an exported proxy Activity or an intent-redirection bug, prefer testing **document/tree URIs** in addition to plain MediaStore paths.

#### Scoped URI grants as SQL-injection carriers

A narrowly scoped read grant can pass the provider's URI authorization while a caller-controlled `selection` expression reads outside the authorized row. Consequently, querying another URI may correctly throw `SecurityException`, yet the granted URI can still act as an SQL-injection carrier. CVE-2026-28576 demonstrated this boundary mismatch in Android 17's Contacts Provider: a picker-issued grant for one contact was enough to infer data from the underlying contacts database without `READ_CONTACTS`.<sup>[[4]](#references)[[7]](#references)</sup>

In that case, `ContactsProvider2.queryLocal()` enabled `setStrictColumns()` and `setStrictGrammar()` only when compat change `484953293` applied to the calling UID. The change was annotated with `@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)`, so an application targeting SDK 36 or lower stayed on the permissive path. The existing `setStrict(true)` wrapping defeated unbalanced clause breakouts, but a syntactically balanced scalar subquery remained valid.<sup>[[5]](#references)[[7]](#references)</sup>

The following probe requests only the granted row's `_id`; the sensitive value influences whether that outer row is returned. A true guess produces a non-empty cursor and a false guess produces an empty cursor.<sup>[[7]](#references)</sup>

```java
String probe = "1 AND (SELECT substr(data1,3,1) FROM data " +
"WHERE mimetype_id=(SELECT _id FROM mimetypes WHERE mimetype=" +
"'vnd.android.cursor.item/phone_v2') " +
"ORDER BY _id LIMIT 1 OFFSET 0)='5'";
try (Cursor c = getContentResolver().query(
grantedUri, new String[]{"_id"}, probe, null, null)) {
boolean match = c != null && c.getCount() > 0;
}
```

To turn this into a Boolean extraction oracle, first establish the normal row count for the granted URI, then vary the `substr()` position and guessed character. Advance `LIMIT 1 OFFSET k` to enumerate rows and change the table/filter to select other fields. This technique is especially useful when error text is hidden and projection injection is blocked, because only cursor cardinality is observed.<sup>[[7]](#references)</sup>

During review, test both current and legacy `targetSdkVersion` values and inspect every compat-gated validation branch. On a vulnerable test build, the fixed behavior can be simulated for the probe package with `adb shell am compat enable 484953293 <package>`; strict grammar rejects the nested `SELECT` with `IllegalArgumentException` before SQLite executes it. `SQLiteQueryBuilder.setStrictGrammar(true)` rejects subqueries in `WHERE`/`HAVING`, but it is disabled by default on a new builder, so providers must enable it (and strict column validation) for every untrusted caller rather than only for new-target applications.<sup>[[5]](#references)[[6]](#references)[[7]](#references)</sup>

## References

- [1] [CENSUS - Remote exploitation of a man-in-the-disk vulnerability in WhatsApp (CVE-2021-24027)](https://www.census-labs.com/resources/remote-exploitation-of-a-man-in-the-disk-vulnerability-in-whatsapp-cve-2021-24027)
- [2] [Microsoft - "Dirty stream" attack: Discovering and mitigating a common vulnerability pattern in Android apps](https://www.microsoft.com/en-us/security/blog/2024/05/01/dirty-stream-attack-discovering-and-mitigating-a-common-vulnerability-pattern-in-android-apps/)
- [3] [census-labs.com - Whatsapp Mitd Remote Exploitation CVE 2021 24027](https://census-labs.com/news/2021/04/14/whatsapp-mitd-remote-exploitation-CVE-2021-24027)
- [4] [Android 17 Security Release Notes](https://source.android.com/docs/security/bulletin/android-17)
- [5] [GrapheneOS - Always enforce strict SQL checks regardless of app targetSdk](https://github.com/GrapheneOS/platform_packages_providers_ContactsProvider/commit/c4129a1c210f)
- [6] [Android SDK - SQLiteQueryBuilder.setStrictGrammar()](https://developer.android.com/reference/android/database/sqlite/SQLiteQueryBuilder#setStrictGrammar(boolean))
- [7] [Mobile Hacking Lab - CVE-2026-28576 analysis and PoC](https://github.com/mobilehackinglab/CVE-2026-28576-poc)

{{#include ../../banners/hacktricks-training.md}}
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ Accessible tables for uri content://jakhar.aseem.diva.provider.notesprovider/not
sqlite_sequence
```

For the distinct case where the caller holds only a row-scoped URI grant, continue with [**Scoped URI grants as SQL-injection carriers**](../content-protocol.md#scoped-uri-grants-as-sql-injection-carriers).<sup>[[12]](#references)</sup>

### writePermission omission + blind SQLi via update()

A common OEM mistake is to export a ContentProvider with a readPermission but omit writePermission. When writePermission is null, any app can call insert/update/delete if those methods are implemented. If update() concatenates the caller-controlled WHERE (selection) directly into an SQL statement, you can build a blind inference oracle and exfiltrate data from other tables in the same SQLite DB (even those normally protected by privileged read permissions like READ_SMS).<sup>[[1]](#references)[[2]](#references)</sup>
Expand Down Expand Up @@ -417,5 +419,6 @@ These changes in recent Android versions mean many legacy exploitation primitive
- [9] [drozer 3.1.0 release notes](https://github.com/WithSecureLabs/drozer/releases/tag/3.1.0)
- [10] [Android Security Bulletin—July 2024](https://source.android.com/security/bulletin/2024-07-01)
- [11] [Reading Contact Photos Without READ_CONTACTS: A Google Messages Confused Deputy Bug](https://blog.devploit.dev/posts/google-messages-avatarcontentprovider-contacts-bypass/)
- [12] [Mobile Hacking Lab - CVE-2026-28576 analysis and PoC](https://github.com/mobilehackinglab/CVE-2026-28576-poc)

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