问题的引出

众所周知,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

 

版权声明:本文为goodhacker原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/goodhacker/p/12216087.html