Android应用安全之Content Provider安全
android平臺提供了Content Provider,將一個應(yīng)用程序的指定數(shù)據(jù)集提供給其它應(yīng)用程序。這些數(shù)據(jù)可以存儲在文件系統(tǒng)、SQLite數(shù)據(jù)庫中,或以任何其它合理的方式存儲。其他應(yīng)用可以通過ContentResolver類從該內(nèi)容提供者中獲取或存入數(shù)據(jù)。
Content Provider通過URI(統(tǒng)一資源定位符)來訪問數(shù)據(jù),URI可以理解為訪問數(shù)據(jù)的唯一地址。
限制app對敏感Content Provider的訪問
Content Provider類提供了一種機(jī)制用來管理以及與其它應(yīng)用程序共享數(shù)據(jù)。在與其它應(yīng)用程序共享Content Provider的數(shù)據(jù)時,應(yīng)該實現(xiàn)訪問控制,禁止對敏感數(shù)據(jù)未經(jīng)授權(quán)的訪問。
有三種方法來限制對內(nèi)容提供者的訪問:
- 公開的
- 私有的
- 內(nèi)部的
公開的
通過在AndroidManifest.xml文件中指定android:exported屬性為true,可以設(shè)置將Content Provider公開給其它應(yīng)用程序。對于API Level低于17的Android應(yīng)用程序,Content Provider默認(rèn)是public的,除非顯式指定android:exported="false"。例如:
<provider android:exported="true" android:name="MyContentProvider" android:authorities="com.example.mycontentprovider" />如果一個Content Provider為公開的,Content Provider中存儲的數(shù)據(jù)就可以被其它應(yīng)用程序訪問。因此,它只能用于處理非敏感信息。
私有的
通過在AndroidManifest. xml文件中指定android:exported屬性為true,可以設(shè)置將Content Provider設(shè)置為私有的。從API Level17及以后,Content Provider默認(rèn)是私有的,除非顯式指定android:exported="true"。例如:
<provider android:exported="false" android:name="MyContentProvider" android:authorities="com.example.mycontentprovider" />開發(fā)建議
下面的元素請求對用戶詞典的讀權(quán)限:
<uses-permission android:name="android.permission.READ_USER_DICTIONARY">
申請某些protectionLevel="dangerous"的權(quán)限:
<uses-permission android:name="com.huawei.dbank.v7.provider.DBank.READ_DATABASE"/> <permission android:name="com.huawei.dbank.v7.provider.DBank.READ_DATABASE" android:protectionLevel="dangerous"></permission>防止SQL注入
?數(shù)據(jù)查詢
傳遞給ContentProvider的參數(shù)應(yīng)該被視為不可信的,不能直接用于sql查詢。?
一個程序要訪問暴露的provider,首先要知道訪問的目標(biāo)地址,類似http協(xié)議,provider也有自己的規(guī)范,即類似content://com.aaaa.bbb.class/tablename
其中,com.aaaa.bbb為包名,class為類名,tablename為表名,一般是這樣子,具體看自己定義了。
看一個查詢例子:
Cursor cursor = contentResolver.query(Words_CONTENT_URI, new String[]{"user","pwd"},null, null, null);這是調(diào)用contentResolver的query方法進(jìn)行數(shù)據(jù)庫查詢,返回一個cursor對象,即類似DataReader的東西,里面是返回結(jié)果。
來看看query的參數(shù)
Cursor android.content.ContentResolver.query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)uri即content://com.aaaa.bbb.class/tablename
projection即你要查詢的列名
selection和selectionArgs共同控制后面的sql語句中的where內(nèi)容.
sortOrder即order by xxx的內(nèi)容。
那么例子中的查詢整個構(gòu)造的語句即:
select user,pwd from tablename;2.Sql注入問題
綜合上面的內(nèi)容,我們可以看到,query里至少兩個參數(shù)我們可控,一個是projection,一個是selection,這兩個都會影響SQL與的組成,也就為注入提供了可能。
這里以某app為例,該app對外暴露了一個content provider,uri為:content://com.masaike.mobile.downloads/my_downloads,其中com.masaike.mobile為包名,downloads為庫的名字,my_downloads為表名(不一定,可自定義的哦)。
現(xiàn)在語句這么寫:
Cursor cursor = contentResolver.query("content://com.masaike.mobile.downloads/my_downloads", new String[]{"_id'","method"},null, null, null);其中_id和method為兩個字段名,我們在_id后面加個單引號,運行下看logcat內(nèi)容:
?
從上圖很容易看出來,SQL語句因為有個單引號,導(dǎo)致出錯。所以注入是存在的。
而如果我們修改projection的內(nèi)容為"* from sqlite_master where type='table';--",這樣子即在閉合后面查詢的情況下顯示出來全部的表名。當(dāng)然也可以構(gòu)造其他語句了。
(參考cnrstar?http://lcx.cc/?i=4462)
開發(fā)建議
1.傳遞給ContentProvider的參數(shù)應(yīng)該被視為不可信的輸入,不應(yīng)該在沒有經(jīng)過任何處理的情況下直接用于SQL查詢。如果查詢語句中包含SQL代碼則可以返回數(shù)據(jù)或者允許攻擊者未授權(quán)地訪問應(yīng)用數(shù)據(jù)庫的數(shù)據(jù)。
2.使用ContentProvider提供外部應(yīng)用程序進(jìn)行數(shù)據(jù)庫存取時應(yīng)使用帶占位符的參數(shù)化查詢防SQL注入。
3.SQLiteDatabase對象的部分內(nèi)置方法是可以有效防SQL注入的,比如query(),insert(),update和delete(),另外,正確地使用rawQuery()也可以防止SQL注入,而execSQL()方法建議避免使用。
(1)、使用SQLiteDatabase對象自帶的防SQL注入的方法,比如query(),insert(),update和delete():
DatabaseHelper dbhelper = new DatabaseHelper(SqliteActivity.this,"sqliteDB");SQLiteDatabase db = dbhelper.getReadableDatabase();/*查詢操作,userInputID和userInputName是用戶可控制的輸入數(shù)據(jù) */
Cursor cur = db.query("user", new String[]{"id","name"}, "id=? and name=?", new String[]{userInputID,userInputName}, null, null, null);/* 插入記錄操作*/
ContentValues val = new ContentValues();val.put("id", userInputID);val.put("name", userInputName);db.insert("user", null, val);/*更新記錄操作*/
ContentValues val = new ContentValues();val.put("id", userInputName);db.update("user", val, "id=?", new String[]{userInputID });/*刪除記錄操作*/
db.delete("user", "id=? and name=?", new String[]{userInputID , userInputName });(2)、正確地使用SQLiteDatabase對象的rawQuery()方法(僅以查詢?yōu)槔?#xff09;:
DatabaseHelper dbhelper = new DatabaseHelper(SqliteActivity.this,"sqliteDB");SQLiteDatabase db = dbhelper.getReadableDatabase();/* userInputID和userInputName是用戶可控制的輸入數(shù)據(jù)*/
String[] selectionArgs = new String[]{userInputID , userInputName };String sql = "select * from user where id=? and name=?";//正確!此處綁定變量Cursor curALL = db.rawQuery(sql, selectionArgs);(3)、以下為錯誤案例!僅供參考:
DatabaseHelper dbhelper = new DatabaseHelper(SqliteActivity.this,"sqliteDB");SQLiteDatabase db = dbhelper.getReadableDatabase();/*案例1:錯誤地使用rawQuery(),未綁定參數(shù)*/
String sql = "select * from user where id=‘" + userInputID +"‘";//錯誤!動態(tài)拼接,未綁定變量Cursor curALL = db.rawQuery(sql, null);/*案例2:使用execSQL()方法*/
String sql = "INSERT INTO user values(‘"+userInputID +"‘,‘"+userInputName +"‘)";//錯誤同上db.execSQL(sql);4.ContentProvider支持對指定的Uri分別設(shè)置讀權(quán)限和寫權(quán)限,建議只開放能完成任務(wù)的最小權(quán)限。
(參考http://shikezhi.com/html/2015/android_0819/134898.html)
規(guī)范ContentProvider的url
沒有正確的覆寫openFile方法,導(dǎo)致攻擊者可以通過更改訪問目錄,遍歷系統(tǒng)中所有文件。
通過使用ContentProvider.openFile()方法,可以方便其它應(yīng)用訪問你的應(yīng)用程序數(shù)據(jù)。根據(jù)ContentProvider的實現(xiàn)方式,使用openFile方法可以導(dǎo)致目錄遍歷漏洞。因此,當(dāng)通過ContentProvider交換文件的時候,文件路徑在使用之前應(yīng)該被規(guī)范化。
不合規(guī)代碼Example 1
在這個不合規(guī)代碼示例中,試圖通過調(diào)用android.net.Uri.getLastPathSegment()獲取paramUri路徑的最后一段,即文件名稱。此文件存在于預(yù)先配置的父目錄IMAGE_DIRECTORY中。
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);}然而,當(dāng)這個文件路徑被URL編碼后,就意味著被訪問的這個文件可能會存在于預(yù)配置的父目錄之外的一個不可預(yù)知的目錄中。
從Android 4.3.0_r2.2開始, Uri.getLastPathSegment()方法在內(nèi)部調(diào)用了Uri.getPathSegments():
public String getLastPathSegment() {// TODO: If we haven't parsed all of the segments already, just// grab the last one directly so we only allocate one string.List<String> segments = getPathSegments();int size = segments.size();if (size == 0) {return null;}return segments.get(size - 1);}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) {// This check keeps us from adding a segment if the path starts// '/' and an empty segment for "//".if (previous < current) {String decodedSegment = decode(path.substring(previous, current));segmentBuilder.add(decodedSegment);}previous = current + 1;}// Add in the final path segment.if (previous < path.length()) {segmentBuilder.add(decode(path.substring(previous)));}return pathSegments = segmentBuilder.build();}Uri.getPathSegments()?方法首先通過調(diào)用getEncoded()獲取了文件路徑,然后使用"/"作為分隔符將路徑分割成幾部分,任何被編碼的部分都將通過decode()方法進(jìn)行URL解碼。
如果文件路徑被URL編碼,那么分隔符就變成了"%2F",而不再是"/",getLastPathSegment()就可能不會正確地返回路徑的最后一段,從而導(dǎo)致目錄遍歷漏洞的產(chǎn)生。
如果Uri.getPathSegments()在進(jìn)行路徑分割之前對路徑進(jìn)行解碼,那么經(jīng)過URL編碼的路徑就會被正確地處理,可惜沒有這么實現(xiàn)。
不合規(guī)代碼Example 2
在本不合規(guī)代碼示例中,試圖通過調(diào)用Uri.getLastPathSegment()兩次來修復(fù)第一個不合規(guī)代碼示例中的漏洞。第一個調(diào)用意在進(jìn)行URL解碼,第二個調(diào)用是用于獲取開發(fā)人員需要的字符串。
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);}例如,當(dāng)以下的URL編碼字符串傳遞給content provider后會發(fā)生什么情況呢?
..%2F..%2F..%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml第一次調(diào)用Uri.getLastPathSegment()函數(shù)會返回以下字符串:
../../../data/data/com.example.android.app/shared_prefs/Example.xml字符串通過Uri.parse()轉(zhuǎn)換成了Uri對象, 然后作為第二次調(diào)用Uri.getLastPathSegment()函數(shù)的參數(shù)。得到的結(jié)果如下:
Example.xml這個字符串是用來創(chuàng)建一個文件對象的。然而,如果攻擊者提供了一個特殊的字符串,該字符串在第一次調(diào)用Uri.getLastPathSegment()時不能被解碼,那么就獲取不到分割路徑的最后一段。這樣的字符串可以使用雙重編碼技術(shù)創(chuàng)建:
雙重編碼
例如,下述雙重編碼字符串就能繞過該漏洞修復(fù):
第一次調(diào)用Uri.getLastPathSegment()?會將 "%25" 解碼為"%",得到如下字符串:
%2E%2E%2F%2E%2E%2F%2E%2E%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml當(dāng)把這個字符串傳遞給第二次調(diào)用的 Uri.getLastPathSegment()時,"%2E"和"%2F" 就會被解碼,得到如下字符串:?
../../../data/data/com.example.android.app/shared_prefs/Example.xml從而導(dǎo)致目錄遍歷的可能。
僅僅通過解碼字符串來防止示例中的目錄遍歷攻擊是不夠的,還必須檢查解碼后的路徑是否在目標(biāo)目錄下。
PoC
以下惡意代碼可對第一個不合規(guī)代碼示例中的漏洞進(jìn)行利用:
String target = "content://com.example.android.sdk.imageprovider/data/" +"..%2F..%2F..%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml"; ContentResolver cr = this.getContentResolver();FileInputStream fis = (FileInputStream)cr.openInputStream(Uri.parse(target)); byte[] buff = new byte[fis.available()];in.read(buff);PoC (Double Encoding)
以下惡意代碼可對第二個不合規(guī)代碼示例中的漏洞進(jìn)行利用:
String target = "content://com.example.android.sdk.imageprovider/data/" + "%252E%252E%252F%252E%252E%252F%252E%252E%252Fdata%252Fdata%252Fcom.example.android.app%252Fshared_prefs%252FExample.xml";ContentResolver cr = this.getContentResolver();FileInputStream fis = (FileInputStream)cr.openInputStream(Uri.parse(target)); byte[] buff = new byte[fis.available()];in.read(buff);?解決方案
在下述解決方案中,在使用文件路徑前通過Uri.decode()對其進(jìn)行了解碼。同樣的,在文件對象創(chuàng)建后,通過調(diào)用File.getCanonicalPath()將路徑進(jìn)行了規(guī)范,同時檢查它是否存在于IMAGE_DIRECTORY目錄中。
通過使用規(guī)范化后的路徑,即使文件路徑被雙重編碼,目錄遍歷漏洞也可以得到緩解。
private static String IMAGE_DIRECTORY = localFile.getAbsolutePath();public ParcelFileDescriptor openFile(Uri paramUri, String paramString)throws FileNotFoundException {String decodedUriString = Uri.decode(paramUri.toString());File file = new File(IMAGE_DIRECTORY, Uri.parse(decodedUriString).getLastPathSegment());if (file.getCanonicalPath().indexOf(localFile.getCanonicalPath()) != 0) {throw new IllegalArgumentException();}return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);}
開發(fā)建議
ContentProvider.openFile()方法提供了一種方便其它應(yīng)用程序訪問自己的數(shù)據(jù)(文件)的方式,但是使用這個方法會導(dǎo)致一個目錄遍歷漏洞。因此,當(dāng)通過ContentProvider訪問一個文件的時候,路徑應(yīng)該被規(guī)范化。
1.使用ContentProvider.openFile()之前需要調(diào)用Uri.decode()
private static String IMAGE_DIRECTORY = localFile.getAbsolutePath();public ParcelFileDescriptor openFile(Uri paramUri, String paramString)throws FileNotFoundException {String decodedUriString = Uri.decode(paramUri.toString());File file = new File(IMAGE_DIRECTORY, Uri.parse(decodedUriString).getLastPathSegment());if (file.getCanonicalPath().indexOf(localFile.getCanonicalPath()) != 0) {throw new IllegalArgumentException();}return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);}2.設(shè)置exported=“false”
3.設(shè)置恰當(dāng)?shù)脑L問權(quán)限
?
原文地址:?http://www.cnblogs.com/goodhacker/p/5249122.html
總結(jié)
以上是生活随笔為你收集整理的Android应用安全之Content Provider安全的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android中SO文件动态调试
- 下一篇: android sina oauth2.