android平台提供了Content Provider,將一個應用程序的指定數據集提供給其它應用程序。這些數據可以存儲在文件系統、SQLite數據庫中,或以任何其它合理的方式存儲。其他應用可以通過ContentResolver類從該內容提供者中獲取或存入數據。
Content Provider通過URI(統一資源定位符)來訪問數據,URI可以理解為訪問數據的唯一地址。
限制app對敏感Content Provider的訪問
Content Provider類提供了一種機制用來管理以及與其它應用程序共享數據。在與其它應用程序共享Content Provider的數據時,應該實現訪問控制,禁止對敏感數據未經授權的訪問。
有三種方法來限制對內容提供者的訪問:
- 公開的
- 私有的
- 內部的
公開的
通過在AndroidManifest.xml文件中指定android:exported屬性為true,可以設置將Content Provider公開給其它應用程序。對於API Level低於17的Android應用程序,Content Provider默認是public的,除非顯式指定android:exported="false"。例如:
<provider android:exported="true" android:name="MyContentProvider" android:authorities="com.example.mycontentprovider" />
如果一個Content Provider為公開的,Content Provider中存儲的數據就可以被其它應用程序訪問。因此,它只能用於處理非敏感信息。
私有的
通過在AndroidManifest. xml文件中指定android:exported屬性為true,可以設置將Content Provider設置為私有的。從API Level17及以后,Content Provider默認是私有的,除非顯式指定android:exported="true"。例如:
<provider android:exported="false" android:name="MyContentProvider" android:authorities="com.example.mycontentprovider" />
開發建議
- 如果不需要與其他應用程序進行數據共享,就應該在manifest文件中設置android:exported="false"。
- 但是值得注意的是,在API Level低於8時,即使顯式地聲明了android:exported="false",其它應用程序仍然可以訪問你的Content Provider。
- 需要向外部提供數據的content provider則需設置訪問權限,如:
下面的元素請求對用戶詞典的讀權限:
<uses-permission android:name="android.permission.READ_USER_DICTIONARY">
申請某些protectionLevel="dangerous"的權限:
<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注入
數據查詢
傳遞給ContentProvider的參數應該被視為不可信的,不能直接用於sql查詢。
一個程序要訪問暴露的provider,首先要知道訪問的目標地址,類似http協議,provider也有自己的規范,即類似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);
這是調用contentResolver的query方法進行數據庫查詢,返回一個cursor對象,即類似DataReader的東西,里面是返回結果。
來看看query的參數
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內容.
sortOrder 即order by xxx的內容。
那么例子中的查詢整個構造的語句即:
select user,pwd from tablename;
2.Sql注入問題
綜合上面的內容,我們可以看到,query里至少兩個參數我們可控,一個是projection,一個是selection,這兩個都會影響SQL與的組成,也就為注入提供了可能。
這里以某app為例,該app對外暴露了一個content provider,uri為:content://com.masaike.mobile.downloads/my_downloads,其中com.masaike.mobile為包名,downloads為庫的名字,my_downloads為表名(不一定,可自定義的哦)。
現在語句這么寫:
Cursor cursor = contentResolver.query("content://com.masaike.mobile.downloads/my_downloads", new String[]{"_id'","method"},null, null, null);
其中_id和method為兩個字段名,我們在_id后面加個單引號,運行下看logcat內容:
從上圖很容易看出來,SQL語句因為有個單引號,導致出錯。所以注入是存在的。
而如果我們修改projection的內容為"* from sqlite_master where type='table';--",這樣子即在閉合后面查詢的情況下顯示出來全部的表名。當然也可以構造其他語句了。
(參考cnrstar http://lcx.cc/?i=4462)
開發建議
1.傳遞給ContentProvider
的參數應該被視為不可信的輸入,不應該在沒有經過任何處理的情況下直接用於SQL查詢。如果查詢語句中包含SQL代碼則可以返回數據或者允許攻擊者未授權地訪問應用數據庫的數據。
2.使用ContentProvider
提供外部應用程序進行數據庫存取時應使用帶占位符的參數化查詢防SQL注入。
3.SQLiteDatabase對象的部分內置方法是可以有效防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是用戶可控制的輸入數據 */
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()方法(僅以查詢為例):
DatabaseHelper dbhelper = new DatabaseHelper(SqliteActivity.this,"sqliteDB"); SQLiteDatabase db = dbhelper.getReadableDatabase();
/* userInputID和userInputName是用戶可控制的輸入數據*/
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(),未綁定參數*/
String sql = "select * from user where id=‘" + userInputID +"‘";//錯誤!動態拼接,未綁定變量 Cursor curALL = db.rawQuery(sql, null);
/*案例2:使用execSQL()方法*/
String sql = "INSERT INTO user values(‘"+userInputID +"‘,‘"+userInputName +"‘)";//錯誤同上 db.execSQL(sql);
4.ContentProvider
支持對指定的Uri分別設置讀權限和寫權限,建議只開放能完成任務的最小權限。
(參考http://shikezhi.com/html/2015/android_0819/134898.html)
規范ContentProvider的url
沒有正確的覆寫openFile方法,導致攻擊者可以通過更改訪問目錄,遍歷系統中所有文件。
通過使用ContentProvider.openFile()
方法,可以方便
其它應用訪問你的應用程序數據。根據ContentProvider
的實現方式,使用
openFile
方法可以導致目錄遍歷漏洞。因此,當通過
ContentProvider
交換文件的時候,文件路徑在使用之前應該被規范化。
不合規代碼Example 1
在這個不合規代碼示例中,試圖通過調用android.net.Uri.getLastPathSegment()
獲取
paramUri
路徑的最后一段,即文件名稱。此文件存在於預先配置的父目錄
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); }
然而,當這個文件路徑被URL編碼后,就意味着被訪問的這個文件可能會存在於預配置的父目錄之外的一個不可預知的目錄中。
從Android 4.3.0_r2.2開始, Uri.getLastPathSegment()
方法在內部調用了
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()
方法首先通過調用getEncoded()
獲取了文件路徑,然后使用
"/"作為分隔符將路徑分割成幾部分,任何被編碼的部分都將通過decode()
方法進行URL解碼。
如果文件路徑被URL編碼,那么分隔符就變成了"%2F",而不再是"/",getLastPathSegment()
就可能不會正確地返回路徑的最后一段,從而導致目錄遍歷漏洞的產生。
如果Uri.getPathSegments()
在進行路徑分割之前對路徑進行解碼,那么經過
URL
編碼的路徑就會被正確地處理,可惜沒有這么實現。
不合規代碼Example 2
在本不合規代碼示例中,試圖通過調用Uri.getLastPathSegment()
兩次來修復第一個不合規代碼示例中的漏洞。第一個調用意在進行
URL
解碼,第二個調用是用於獲取開發人員需要的字符串。
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編碼字符串傳遞給content provider后會發生什么情況呢?
..%2F..%2F..%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml
第一次調用Uri.getLastPathSegment()
函數會返回以下字符串:
../../../data/data/com.example.android.app/shared_prefs/Example.xml
字符串通過Uri.parse()
轉換成了
Uri
對象
, 然后作為第二次調用Uri.getLastPathSegment()
函數的參數。
得到的結果如下:
Example.xml
這個字符串是用來創建一個文件對象的。然而,如果攻擊者提供了一個特殊的字符串,該字符串在第一次調用Uri.getLastPathSegment()
時不能被解碼,那么就獲取不到分割路徑的最后一段。這樣的字符串可以使用雙重編碼技術創建:
雙重編碼
例如,下述雙重編碼字符串就能繞過該漏洞修復:
%252E%252E%252F%252E%252E%252F%252E%252E%252Fdata%252Fdata%252Fcom.example.android.app%252Fshared_prefs%252FExample.xml
第一次調用Uri.getLastPathSegment()
會將 "%25" 解碼為"%",得到如下字符串:
%2E%2E%2F%2E%2E%2F%2E%2E%2Fdata%2Fdata%2Fcom.example.android.app%2Fshared_prefs%2FExample.xml
當把這個字符串傳遞給第二次調用的 Uri.getLastPathSegment()時,"%2E"和"%2F" 就會被解碼,得到如下字符串:
../../../data/data/com.example.android.app/shared_prefs/Example.xml
從而導致目錄遍歷的可能。
僅僅通過解碼字符串來防止示例中的目錄遍歷攻擊是不夠的,還必須檢查解碼后的路徑是否在目標目錄下。
PoC
以下惡意代碼可對第一個不合規代碼示例中的漏洞進行利用:
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)
以下惡意代碼可對第二個不合規代碼示例中的漏洞進行利用:
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()
對其
進行了解碼。同樣的,在文件對象創建后,通過調用File.getCanonicalPath()
將路徑進行了規范,同時檢查它是否存在於
IMAGE_DIRECTORY
目錄中。
通過使用規范化后的路徑,即使文件路徑被雙重編碼,目錄遍歷漏洞也可以得到緩解。
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); }
開發建議
ContentProvider.openFile()方法提供了一種方便其它應用程序訪問自己的數據(文件)的方式,但是使用這個方法會導致一個目錄遍歷漏洞。因此,當通過ContentProvider訪問一個文件的時候,路徑應該被規范化。
1.使用ContentProvider.openFile()之前需要調用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.設置exported=“false”
3.設置恰當的訪問權限
附:
使用adb命令測試content provider的方法
usage: adb shell content query --uri <URI> [--user <USER_ID>] [--projection <PROJECTION>] [--where <WHERE>] [--sort <SORT_ORDER>] <PROJECTION> is a list of colon separated column names and is formatted: <COLUMN_NAME>[:<COLUMN_NAME>...] <SORT_OREDER> is the order in which rows in the result should be sorted. Example: # Select "name" and "value" columns from secure settings where "name" is equal to "new_setting" and sort the result by name in ascending order. adb shell content query --uri content://settings/secure --projection name:value --where "name=\'new_setting\'" --sort "name ASC"