Doors Left Open tutorial banner showing exported Android components under forensic inspection
    N/A
    26 min mhfh research 2026-07-20

    Doors Left Open: Exploiting Android Exported Activities, Services, and Receivers

    A controlled Android IPC lab for enumerating exported components, testing intent inputs and permission gates, and proving externally reachable native behavior.

    $cat snippet_android-exported-components-exploitation.sh
    adb shell am start -n com.mhfh.ipclab/.ExportedActivity

    Executive Summary: Exported Does Not Mean Uncontrolled

    An Android exported components exploit begins when one application invokes an activity, service, or broadcast receiver belonging to another application. Android inter-process communication makes this normal: launchers start activities, the operating system delivers broadcasts, and companion apps call shared services. The weakness appears when a reachable component performs sensitive work without establishing who called, what the caller may request, and whether the request is valid in the current application state.

    This tutorial builds a controlled IPC lab around three reversible behaviors. An exported activity displays a supplied training label, an exported service writes a test job marker, and an exported receiver records a test broadcast. You will enumerate the manifest, confirm effective exposure on a running device, invoke each component with Android Debug Bridge, inspect its handling code, and distinguish reachability from real security impact.

    The result is a repeatable assessment method. It does not stop at 'android:exported="true"'. It follows the external intent through component resolution, permission enforcement, input parsing, native behavior, and evidence collection.


    Android Components as IPC Entry Points

    Android applications are built from components with defined lifecycles and communication models:

    ComponentPrimary roleExternal invocation
    ActivityPresents a user interface or flow'startActivity' or 'am start'
    ServicePerforms or exposes background work'startService', 'bindService', or 'am startservice'
    Broadcast receiverHandles event-style messages'sendBroadcast' or 'am broadcast'
    Content providerExposes structured data or files'ContentResolver' operations

    This article concentrates on the first three. Providers deserve a separate treatment because URI permissions, authorities, query operations, and path handling create a different testing model.

    The manifest's 'android:exported' attribute determines whether components outside the application can ordinarily address the component. Android's official android:exported guidance states that an exported component can be launched by other applications, while a non-exported component is limited to the same application, shared user identity, or privileged system components.

    Export status answers only the first question: can another process reach this entry point? It does not answer whether a manifest permission applies, whether code verifies the caller, whether the component is enabled, or whether supplied extras alter sensitive behavior.

    Android IPC lanes crossing exported component access controls

    Activities, services, and receivers use different delivery mechanics, but each crosses the same application boundary. Permission gates and component-local checks determine whether an external intent becomes authorized behavior.

    Effective Export Rules and Android Versions

    Do not infer exposure from an intent filter alone. Historical defaults differed between component types and Android releases. Since Android 12, applications targeting API level 31 or higher must explicitly declare 'android:exported' for activities, services, and receivers containing intent filters. The build fails when the declaration is missing.

    Explicit declarations are easier to audit:

    $cat output.xml
    <activity
        android:name=".InternalDiagnosticsActivity"
        android:exported="false" />
    
    <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="https" android:host="app.example.test" />
        </intent-filter>
    </activity>

    The deep-link activity must be reachable to serve its purpose, but its code still needs strict URI parsing and application-state checks. The diagnostics activity has no reason to accept cross-application calls, so it remains private.

    The current Android activity reference also describes stricter intent matching options on newer platforms. These features can reduce unexpected explicit-intent delivery, but they do not replace component-level authorization. Assess the application's declared target SDK and behavior on the actual test image.

    Threat Model Before Testing

    Start by naming the caller the component was designed to trust. That single question prevents a large class of false positives. A launcher activity expects the operating system and ultimately the device user to start it. A verified web link handler expects arbitrary browsers to submit public URLs. A same-vendor synchronization service may expect only packages signed by the publisher. A receiver for a public system broadcast expects the operating system, but its action might also be reproducible by an ordinary application unless Android protects that broadcast.

    Next, list the authority available to the target process. The target may hold authenticated session tokens, private application data, device-management privileges, notification access, Bluetooth permissions, or access to files that the caller cannot read. An exported component becomes a confused deputy when it applies that authority on behalf of a caller that would not independently possess it. This is more precise than saying that the component runs as the victim app: document the exact permission, credential, or trusted state crossed by the request.

    Then define the security invariant for each entry point. Examples include "only a signed companion app may enqueue a synchronization job," "any browser may open a product page but cannot select an internal administrative fragment," and "a system connectivity event may refresh cached data but cannot change the signed-in account." These statements make code review and runtime tests measurable. If observed behavior violates an invariant, the report can explain both the intended boundary and the demonstrated break.

    Finally, separate four outcomes that are often collapsed into one finding:

    1. Resolvable: Android can identify the component for the supplied intent.
    2. Deliverable: permission and platform checks allow the intent to reach its lifecycle method.
    3. Controllable: caller-supplied fields influence a code branch or data object.
    4. Security-relevant: the influenced branch crosses a confidentiality, integrity, authentication, or availability boundary.

    A good assessment records all four. A component that is resolvable but permission-denied is evidence that the intended gate operates. A component that displays an untrusted label is controllable but may be harmless. A service that exports account data is security-relevant because the result exceeds the caller's authority.

    Build the Controlled IPC Lab

    The fictional package 'com.mhfh.ipclab' exposes three intentionally weak training components. Each writes or displays a harmless marker so external invocation is visible and reversible.

    $cat output.xml
    <activity
        android:name=".ExportedActivity"
        android:exported="true" />
    
    <service
        android:name=".ExportedMarkerService"
        android:exported="true" />
    
    <receiver
        android:name=".ExportedMarkerReceiver"
        android:exported="true">
        <intent-filter>
            <action android:name="com.mhfh.ipclab.WRITE_MARKER" />
        </intent-filter>
    </receiver>

    Reset and launch the test package from an emulator snapshot:

    $cat output.bash
    adb shell pm clear com.mhfh.ipclab
    adb shell monkey -p com.mhfh.ipclab 1
    adb shell dumpsys package com.mhfh.ipclab > ipclab-package-state.txt

    Record the APK SHA-256, package version, target SDK, Android build, and tester workstation time zone. Component behavior can vary with target SDK, background-execution rules, receiver-registration flags, and device policy.

    Use a dedicated emulator or test handset and a separate caller identity. Commands issued through ADB are excellent for component discovery, but the shell UID is not identical to an ordinary third-party application UID. Some permissions and platform behaviors treat the shell specially. Once an ADB command establishes a candidate path, reproduce the call from a minimal companion APK before claiming that any installed application can do the same. The companion should request no unnecessary permissions, log its package and UID, send a single documented intent, and expose the returned result without adding business logic.

    Static Enumeration from AndroidManifest.xml

    Decode the APK and build a complete component table before sending intents:

    $cat output.bash
    sha256sum ipc-lab.apk
    apktool d ipc-lab.apk -o ipc-lab-decoded
    jadx -d ipc-lab-jadx ipc-lab.apk
    
    grep -nE "<(activity|activity-alias|service|receiver|provider)" \
      ipc-lab-decoded/AndroidManifest.xml

    For each activity, service, and receiver, record:

    • Fully qualified component class.
    • Explicit or effective exported state.
    • Enabled state and owning process.
    • Intent actions, categories, schemes, hosts, paths, and MIME types.
    • Component-level 'android:permission'.
    • Application-level permission inherited by the component.
    • Custom permission definition and protection level.
    • Code-level checks inside lifecycle methods.

    Inspect aliases separately. An activity may be private while an exported 'activity-alias' points to it. The alias has its own exported and permission attributes, so reviewing only the target class declaration misses the reachable entry point.

    OWASP's component restriction guidance recommends explicit non-exported declarations for internal components and signature-level permissions when a separate trusted application genuinely requires access.

    Confirm Exposure on the Installed Package

    The decoded manifest reflects the APK, while the package manager reflects the installed state. Product flavors, manifest merging, split APKs, and device configuration can change the effective result.

    Query the package:

    $cat output.bash
    adb shell dumpsys package com.mhfh.ipclab
    adb shell cmd package resolve-activity \
      --brief -n com.mhfh.ipclab/.ExportedActivity

    Tools such as drozer can accelerate inventory, but the result still needs manual evaluation:

    $cat output.text
    run app.package.attacksurface com.mhfh.ipclab
    run app.activity.info -a com.mhfh.ipclab
    run app.service.info -a com.mhfh.ipclab
    run app.broadcast.info -a com.mhfh.ipclab

    The OWASP MASTG drozer reference documents these enumeration and invocation modules. Treat automated output as an index into code review, not a vulnerability verdict.

    Test an Exported Activity

    The lab activity reads a string extra and displays it:

    $cat output.kotlin
    class ExportedActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val label = intent.getStringExtra("training_label") ?: "none"
            setContentView(TextView(this).apply {
                text = "External label: $label"
            })
        }
    }

    Start it explicitly with a controlled value:

    $cat output.bash
    adb shell am start \
      -n com.mhfh.ipclab/.ExportedActivity \
      --es training_label external-activity-confirmed

    An explicit intent names the exact component and can reach an exported activity even when supplied action data would not match an intent filter on older behavior. Confirm the current platform's resolution behavior rather than assuming the filter is an authorization policy.

    The lab result proves external reachability and extra processing. In a real assessment, inspect what the activity does next. High-value patterns include bypassing an expected login step, selecting an internal fragment, opening a privileged WebView, returning data through 'setResult', or accepting a nested intent that it launches without validation.

    Test missing, empty, oversized, duplicated, and wrong-type extras. A crash demonstrates robustness impact, not automatically privilege escalation. Preserve the exception and determine whether repeated external invocation creates a meaningful denial of service.

    Deep-link activities need an additional matrix. Test the declared host alongside a sibling domain, a user-info form such as trusted.example@other.example, mixed-case and encoded paths, repeated query parameters, fragments, non-default ports, and redirects processed after initial validation. Parse with a URI API and compare normalized components; substring and suffix checks routinely accept unintended hosts. If the activity loads a URL into a WebView or exchanges an authorization code, follow the value through that downstream boundary rather than ending the test at successful activity launch.

    Also observe task and back-stack behavior. Flags such as FLAG_ACTIVITY_NEW_TASK, CLEAR_TOP, or document-launch modes can create UI redressing, expose a result to the wrong task, or place a sensitive screen into an unexpected navigation sequence. Capture the visible task state and use dumpsys activity activities to distinguish a data-validation flaw from a task-management side effect.

    Test an Exported Started Service

    The lab service accepts an action and writes a reversible marker:

    $cat output.kotlin
    class ExportedMarkerService : Service() {
        override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
            if (intent?.action == "com.mhfh.ipclab.SET_MARKER") {
                val marker = intent.getStringExtra("marker") ?: return START_NOT_STICKY
                if (marker.length <= 40) {
                    getSharedPreferences("lab", MODE_PRIVATE)
                        .edit().putString("service_marker", marker).apply()
                }
            }
            stopSelf(startId)
            return START_NOT_STICKY
        }
    
        override fun onBind(intent: Intent?) = null
    }

    Invoke the lab service:

    $cat output.bash
    adb shell am startservice \
      -n com.mhfh.ipclab/.ExportedMarkerService \
      -a com.mhfh.ipclab.SET_MARKER \
      --es marker external-service-confirmed

    Modern Android background execution limits may block or alter a started-service test depending on foreground state and target SDK. Record the exact command output and logcat result. Do not reinterpret a platform background restriction as proof that access control is correct; the component may remain callable while the app is foregrounded.

    Bound services require a companion test application because ADB does not model arbitrary Binder transactions cleanly. Review 'onBind', returned Binder methods, Messenger message codes, and AIDL interfaces. Sensitive Binder operations should enforce caller permissions or verify calling UID before executing.

    Build a method-level table for a bound service. Record each transaction or AIDL method, parameter types, whether it is synchronous, the permission check reached, the caller identity used, and the observable result. Authorization at onBind is useful, but it should not be assumed to protect every future method forever: Binder objects can be passed between processes, callbacks may introduce a second caller, and asynchronous work may run after the original identity is gone. Put checks as close as practical to the operation being protected.

    Pay special attention to services that accept file paths, content URIs, PendingIntent objects, callbacks, or serialized job descriptions. These inputs carry capabilities rather than simple text. A service that opens a caller-selected path with its own privileges, grants a URI onward, or fires a supplied PendingIntent may cross a boundary even when its nominal command looks harmless. Record ownership and grant flags at each handoff.

    Android's permission-based access-control guidance explains the core failure: an exported component performs a sensitive task using permissions already granted to the target app, while the external caller lacks those permissions and the component fails to enforce them.

    Test an Exported Broadcast Receiver

    The lab receiver accepts one custom action:

    $cat output.kotlin
    class ExportedMarkerReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            if (intent.action != "com.mhfh.ipclab.WRITE_MARKER") return
            val marker = intent.getStringExtra("marker") ?: return
            if (marker.length > 40) return
            context.getSharedPreferences("lab", Context.MODE_PRIVATE)
                .edit().putString("receiver_marker", marker).apply()
        }
    }

    Send an explicit broadcast:

    $cat output.bash
    adb shell am broadcast \
      -n com.mhfh.ipclab/.ExportedMarkerReceiver \
      -a com.mhfh.ipclab.WRITE_MARKER \
      --es marker external-receiver-confirmed

    An unprotected custom action name is not a secret. Any installed application can construct the same action once it knows or guesses the string. If the receiver triggers cache deletion, session changes, uploads, notifications, or device operations, require an appropriate permission and validate state inside 'onReceive'.

    Also inspect receivers registered in code. On current Android APIs, registration overloads accept 'RECEIVER_EXPORTED' or 'RECEIVER_NOT_EXPORTED'. Manifest searches do not reveal these runtime registrations, so search for 'registerReceiver' and trace feature states that activate them.

    Receiver testing should cover both injection and interception. Injection asks whether an outsider can send an event the receiver trusts. Interception asks whether the application broadcasts sensitive information that another receiver can collect. For ordered broadcasts, examine result codes, result data, abort behavior, and receiver priority; another application may read or modify the in-flight result when the protocol lacks an effective permission. For pending or sticky state, verify current platform support and do not generalize behavior from obsolete Android versions.

    Lifecycle constraints matter here. onReceive has a short execution window, while goAsync extends work through a PendingResult that must be finished. Repeated external broadcasts can create resource pressure, duplicate jobs, or inconsistent state even if each individual event performs little work. Measure the number of scheduled jobs, notifications, database rows, or network requests produced by a bounded test count, then reset the application. This yields stronger availability evidence than merely flooding the receiver.

    Controlled Android exported component testing workflow

    The assessment moves from manifest inventory to installed-state confirmation, benign external intents, and preserved evidence. Activities, services, and receivers are tested separately because their lifecycles and enforcement points differ.

    Verify the Reversible Evidence

    Read the lab preference file through the package's debug identity:

    $cat output.bash
    adb shell run-as com.mhfh.ipclab \
      cat shared_prefs/lab.xml

    Expected markers are:

    $cat output.xml
    <string name="service_marker">external-service-confirmed</string>
    <string name="receiver_marker">external-receiver-confirmed</string>

    For the activity, preserve a screenshot showing the controlled label and capture logcat around creation. The evidence demonstrates that a request originating outside the package reached native code. It does not prove a more serious outcome than the marker itself; severity comes from the actual capability found in the reviewed application.

    Permissions and Caller Verification

    An exported component can remain intentional while access is restricted. Manifest permissions are the first enforcement layer:

    $cat output.xml
    <permission
        android:name="com.mhfh.ipclab.permission.TRUSTED_IPC"
        android:protectionLevel="signature" />
    
    <service
        android:name=".PartnerService"
        android:exported="true"
        android:permission="com.mhfh.ipclab.permission.TRUSTED_IPC" />

    A signature permission limits ordinary access to applications signed with the same certificate. This is appropriate for a suite of cooperating apps under one publisher. A normal permission is automatically granted at installation and usually does not protect a sensitive integration from arbitrary installed apps.

    Code-level checks add context. A bound service can use 'Binder.getCallingUid()' and package-manager data, but package names alone are not a durable identity boundary. Validate signing certificates for an intended partner and handle shared UIDs or multiple packages carefully. Clear Binder identity only after authorization, because clearing it too early discards the caller context needed for the decision.

    Activities and started services often receive calls after Binder dispatch has completed, making 'getCallingUid' unsuitable or misleading. Design the integration around platform permissions, verified app links, explicit authenticated protocols, or user-mediated flows instead of retrofitting unreliable caller guesses.

    Intent Input Validation

    Every exported component treats its Intent as untrusted structured input. Validate:

    1. Action is present and exactly supported.
    2. Data URI has the expected HTTPS scheme, normalized host, port, and path.
    3. MIME type matches the operation.
    4. Required extras exist with exact types and bounded sizes.
    5. Parcelable class loading cannot select unexpected application classes.
    6. Nested intents target an approved destination and discard dangerous flags.
    7. File or content URIs carry appropriate temporary grants.
    8. Operation is valid for the current account and UI state.

    Avoid generic dispatch patterns such as 'when (intent.getStringExtra("command"))' with dozens of privileged branches. Narrow components with small schemas are easier to secure and test.

    Activity, Service, and Receiver Failure Patterns

    Exported activity bypasses a navigation gate

    An internal screen assumes a previous activity authenticated the user or populated trusted extras. Direct external launch skips that sequence. The fix is to enforce the state in the destination, not merely hide its navigation button.

    Exported service proxies application permissions

    The target holds a dangerous permission and exposes an operation through an unprotected service. The external caller cannot access the resource directly but can ask the target process to do it. Align the service permission with the protected operation and validate every Binder method.

    Exported receiver treats an action string as authorization

    A custom action is documented in the APK and trivial to reproduce. Protect the receiver with a permission or make it non-exported. The Android insecure broadcast receiver guidance recommends non-exported receivers for internal events and signature-protected receivers for trusted cross-app communication.

    Nested intent forwarding expands attacker control

    An exported component receives a Parcelable Intent and launches it. This can turn one exported gateway into access to private components or unintended flags. Reconstruct a fresh internal intent from validated primitive fields instead of forwarding the supplied object.

    Component trusts referrer or calling package text

    Referrer values and string extras are input, not strong identity. Use platform-enforced permissions and cryptographic signing identity when the integration needs caller trust.

    Dynamic Tracing with Frida

    When code is obfuscated or a component has many branches, trace the target lifecycle method. This observer hook logs the activity intent without changing behavior:

    $cat output.javascript
    Java.perform(function () {
      const Activity = Java.use("com.mhfh.ipclab.ExportedActivity");
      const onCreate = Activity.onCreate.overload("android.os.Bundle");
    
      onCreate.implementation = function (state) {
        const intent = this.getIntent();
        console.log("[+] ExportedActivity created");
        console.log("    action: " + intent.getAction());
        console.log("    data:   " + intent.getDataString());
        console.log("    extras: " + intent.getExtras());
        return onCreate.call(this, state);
      };
    });

    For services, trace 'onStartCommand' or 'onBind'. For receivers, trace 'onReceive'. Keep the hook observational until the real branch and data types are understood. A Frida log is strongest when paired with the ADB command, manifest declaration, and reversible application result.

    Evidence and Severity Model

    Preserve these artifacts for every externally reachable component:

    • APK hash, version, target SDK, and Android build.
    • Decoded manifest declaration and inherited permissions.
    • Installed package-manager state.
    • Exact explicit or implicit intent used.
    • Lifecycle trace or logcat showing delivery.
    • Inputs consumed and validation branches taken.
    • Reversible output proving native behavior.
    • Caller requirements and application state.
    • Cleanup or emulator snapshot identifier.

    Rank the result by what the component enables, not by component type. Opening a public help screen is expected. Changing a harmless preference is low impact. Returning private records, bypassing authentication, invoking a privileged operation, or forwarding arbitrary intents can justify higher severity when the external call path is reliable.

    OWASP maintains separate tests for unprotected exported activities, services, and receivers. Its broadcast receiver test explicitly requires both exposure and sensitive behavior before the test fails.

    Secure Component Design Checklist

    • Every manifest component declares export intent explicitly.
    • Internal components use 'android:exported="false"'.
    • Context-registered internal receivers use 'RECEIVER_NOT_EXPORTED'.
    • Required cross-app entry points have a documented caller model.
    • Same-publisher integrations use a signature permission where appropriate.
    • Every action, URI, category, MIME type, and extra is validated.
    • Nested intents and URI grants are reconstructed narrowly.
    • Sensitive actions recheck authentication and account state.
    • Consequential user-facing actions require visible confirmation.
    • Bound service methods enforce access independently.
    • Unsupported inputs fail closed without crashing.
    • Automated tests invoke exported components from a separate test package.

    Frequently Asked Questions

    What is an exported Android component?

    It is an activity, service, receiver, or provider that Android permits another application to invoke. Exporting supports launchers, deep links, system broadcasts, and integrations. It becomes risky when externally supplied requests reach sensitive native behavior without an effective authorization and validation boundary.

    Does android:exported=true mean the app is vulnerable?

    No. It means the component is an external entry point. Evaluate its permission, caller checks, accepted intent schema, application state, and behavior. A correctly protected deep-link activity can be exported by design, while an unprotected internal service may expose privileged operations.

    How can exported components be enumerated?

    Decode the merged AndroidManifest and record component export, filters, and permissions. Confirm the installed result with 'dumpsys package' or package-manager commands. Search code for dynamically registered receivers, activity aliases, and runtime enablement because manifest-only automation can miss those paths.

    What is the best protection for an internal component?

    Set 'android:exported="false"' and remove unnecessary intent filters. When a component must serve another trusted app, use a suitable platform or signature permission, minimize the exposed operation, validate intent data, and recheck authorization inside every sensitive branch.

    Conclusion: Test Behavior, Not a Boolean

    An Android exported components exploit is a chain from external caller to meaningful native behavior. The manifest boolean reveals where to look, but the evidence lives in permission enforcement, lifecycle code, intent parsing, application state, and the result produced by the target process.

    The disciplined workflow is consistent: inventory statically, confirm installed exposure, send one controlled intent, trace the exact branch, preserve a reversible result, and score only the demonstrated capability. That method separates necessary Android IPC from doors that were genuinely left open.

    Continue with the Android WebView bridge tutorial, the Frida TLS instrumentation guide, the MobSF tool profile, or our mobile application penetration testing service.

    #Android#Exported Components#IPC#Intent#Mobile Pentesting