关于Android contentprovider sql注入问题
问题的引出
众所周知,ContentProviders是android用于跨进程共享数据的一种方式。ContentProvider需要媒介进行数据存储, 最常用的就是SQLite数据库。我们知道,web应用常常由于数据库操作语句编写不当造成sql注入漏洞,其实,Android也不例外。在导出的 ContentProviders 中实现 query、update 和 delete 时,如果将未经处理的输入传递给 SQL 语句,就会容易受到 SQL 注入攻击。恶意应用可能会提供蓄意创建的输入,进而访问隐私数据或损坏数据库内容。下面我们来看一段android contentprovider中的数据库查询代码示例1:
1 /** 2 * 显示书籍 3 * 4 * @param view 视图 5 */ 6 public void showBooks(View view) { 7 String content = ""; 8 Uri bookUri = BookProvider.BOOK_CONTENT_URI; 9 Cursor bookCursor = getContentResolver().query(bookUri, new String[]{"_id", "name"}, null, null, null); 10 if (bookCursor != null) { 11 while (bookCursor.moveToNext()) { 12 Book book = new Book(); 13 book.bookId = bookCursor.getInt(0); 14 book.bookName = bookCursor.getString(1); 15 content += book.toString() + "\n"; 16 Log.e(TAG, "query book: " + book.toString()); 17 mTvShowBooks.setText(content); 18 } 19 bookCursor.close(); 20 } 21 }
通常,一个简单的sql查询语句结构可以用如下语句表示。攻击者可以通过select clause、projection以及sortOrder传入恶意语句进行SQL注入。
SELECT [projection] FROM [table] WHERE [select clause] ORDER BY [sortOrder].
在我们的代码示例1中的projection为_id,name,select子句以及ORDER BY为null,代入具体字段就是:
select _id, name from [bookUri]
然后使用drozer来测试这段代码是存在SQL注入的,drozer测试命令为:
dz> run app.provider.query [bookUri] –-selection "'"
这里的–selection表示对后面的select子句进行注入。
当然这里也可以使用以下命令进行测试:
dz> run app.provider.query [URI] –-selection "1-1=0" dz> run app.provider.query [URI] –-selection "0=0" dz> run app.provider.query [URI] –-selection "(1+random())*10 > 1"
如图,我们看到“’”代入到了select子句进行查询,查询结果报如下错误:
示例1中的SQL查询语句既没有对用户输入进行过滤,也没有使用预编译和参数化查询等方式进行SQL注入防护,必然是存在SQL注入的,但是我们再来看下述修改后的代码示例2:
public void showBooks(View view) { String content = ""; Uri bookUri = BookProvider.BOOK_CONTENT_URI; String _id = "3"; String name = "Android"; Cursor bookCursor = getContentResolver().query(bookUri, new String[]{"_id", "name"}, "_id=? and name=?", new String[] {_id, name}, null, null); if (bookCursor != null) { while (bookCursor.moveToNext()) { Book book = new Book(); book.bookId = bookCursor.getInt(0); book.bookName = bookCursor.getString(1); content += book.toString() + "\n"; Log.e(TAG, "query book: " + book.toString()); mTvShowBooks.setText(content); } bookCursor.close(); } }
其中的查询语句用sql语句表示为:
select _id,name from [bookUri] where _id= ? and name = ?
理论上,这条语句是不应该存在sql注入的。但是,当使用如下drozer命令对selection进行注入时,发现还是存在SQL注入:
run app.provider.query content://org.***.book.provider/book --selection "'"
我们在代码中已经对selection参数使用了带占位符的参数化查询了,怎么还存在SQL注入呢?
仔细查看上图报错信息发现SQL语句变成了“select * from book where ‘”,查询条件已经变成我们输入的“’”单引号了。所以我们猜想,这里drozer进行的SQL注入其实跟web sql注入不太相同。这里应该是直接将用户输入替代了原来的SQL语句,而不是简单的替换某个查询变量。为了验证这个猜想,我们分析了drozer的源代码。Drozer在执行上述SQL注入命令时,会解析输入的相应参数,然后调用下图中的query函数进行查询操作。
为了验证前面的猜想,我们在代码中添加打印语句,打印projection、selection等参数信息,再执行drozer命令进行测试。
(1)对select子句是否替换进行测试
run app.provider.query content://org.***.book.provider/book --selection "1=1"
(2)对projection是否替换进行测试
run app.provider.query content://org.***.book.provider/book --projection "1=1"
我们发现selection子句和projection分别被替换了。下面的图(来自网络)比较形象地说明了这个过程:
所以,不管我们怎么实现查询,只要有权限读取ContentProvider uri,就可以实现SQL注入。
如何利用?
利用上面测试的payload,攻击者只能获取所查询表内的数据,然而这些数据第三方本来就有权限读取的,这是ContentProvider权限设置的锅(关于权限设置请参考:https://www.futurelearn.com/courses/secure-android-app-development/0/steps/21592),不是SQL的问题。如果攻击者想要从支持ContentProvider的数据库中提取一些有价值的信息,例如通过读取sqlite_master表的内容,是没有通过ContentResolver查询该表的直接方法的,因为FROM子句通常是根据URI参数在ContentProvider内部确定的。但是,仍有一些攻击媒介可用。
(1)通过projection
projection = { "* from sqlite_master;--" };
代入SQL语句查询:
SELECT * from sqlite_master;-- FROM <table>
–是注释符,将后面的语句注释了。这条语句将dump整个sqlite_master数据库的内容,使得攻击者可以获取到私有的未公开的数据库表和列名。
用drozer测试如下:
run app.provider.query content://org.***.book.provider/book/ --projection "* FROM sqlite_master --;"
(2)通过select clause
如:
projection = { "a", "b", "c" } // 3 valid column names selection = "0 UNION SELECT type, name, sql FROM sqlite_master;--" OR projection = { "a", "b", "c" } // 3 valid column names selection = "0 UNION SELECT type, name, sql FROM sqlite_master WHERE (1" OR projection = { "a", "b", "c" } // 3 valid column names selection = "0/*" sortOrder = "*/UNION SELECT type, name, sql FROM sqlite_master"
具体用到这个例子中:
所以,我们必须限制这种输入。
解决方式
Google给出了这个问题的解决方式,如下:
1. 如果受影响的 ContentProvider 无需供其他应用使用,请执行以下操作:
- 您可以在清单中修改受影响的 ContentProvider 的 <provider> 标签,以便设置 android:exported=”false”。这样可以阻止其他应用将 Intent 发送到受影响的 ContentProvider。
- 您还可以设置 android:permission 属性,将 permission 设为 android:protectionLevel=”signature”,从而阻止其他开发者编写的应用将 Intent 发送到受影响的 ContentProvider。
2. 如果受影响的 ContentProvider 需要供其他应用使用,请执行以下操作:
您可以通过结合使用严格模式和投影映射来防止 SQLiteDatabase.query 受到 SQL 注入攻击。严格模式(setStrict(true))可防止出现恶意选择子句,投影映射(setProjectionMap)可防止出现恶意投影子句。您必须同时使用这两项功能来确保查询安全无虞。
代码示例3:
private static HashMap<String, String> values = new HashMap<>(); // 关联Uri和Uri_Code static { sUriMatcher.addURI(AUTHORITY, "book", BOOK_URI_CODE); sUriMatcher.addURI(AUTHORITY, "user", USER_URI_CODE); values.put("_id", "_id"); values.put("name", "name"); } public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables(getTableName(uri)); qb.setStrict(true); switch (sUriMatcher.match(uri)) { case BOOK_URI_CODE: qb.setProjectionMap(values); break; default: break; } Cursor cursor = qb.query(mDb, projection, selection, selectionArgs, null, null, sortOrder); return cursor; }
修改成示例3的代码后再用drozer测试,是不能获取sqlite_master 表的内容的,如图:
(1)测试projection
(2)测试selection
我们发现程序自动给select clause加上了(),我们使用如下payload测试以尝试闭合该“)”试试。
dz> run app.provider.query content://org.***.book.provider/book --selection "0) UNION SELECT name, sql FROM sqlite_master;--"
发现又在外面加了一层括号,这种方式行不通。
如果注释掉严格模式(setStrict(true)),再测试就行了,这大概就是严格模式的作用了:
dz> run app.provider.query content://org.***.book.provider/book --selection "0) UNION SELECT name, sql FROM sqlite_master;--"
3.对于update和delete操作可以通过以下方式来防止 SQLiteDatabase.update 和 SQLiteDatabase.delete 受到 SQL 注入攻击:
使用以“?”作为可替换参数的选择子句,并使用一组单独的选择参数。不应使用不信任的输入构建选择子句。
参考文档:
http://chalup.github.io/blog/2013/11/06/sql-injection-through-contentprovider/
https://oldbam.github.io/android/security/android-vulnerabilities-insecurebank-content-providers
https://support.google.com/faqs/answer/7668308
https://github.com/yahoo/squidb/wiki/Protecting-against-SQL-Injection