type
status
date
slug
summary
tags
category
icon
password
 

0x00 引言

 
Android 操作系统通过为每个应用程序分配自己的专用数据和内存空间来强制隔离。为了促进数据和文件共享,Android 提供了一个称为Content Provider的组件,它充当一个接口,用于以安全的方式管理数据并将数据公开给其他已安装的应用程序。如果使用得当,内容提供商可以提供可靠的解决方案。但是,实施不当可能会引入漏洞,从而绕过应用程序主目录中的读/写限制。
就像应用可以向其他应用发送数据一样,它也可以从其他应用接收数据。考虑用户如何与您的应用互动,以及希望从其他应用接收哪些类型的数据。例如,社交网络应用可能希望从其他应用接收文本内容。
notion image
其他应用的用户经常通过 Android Sharesheetintent解析器向您的应用发送数据。向您的应用发送数据的应用必须为该数据设置 MIME 类型。应用可以通过以下方式接收其他应用发送的数据

0x01 漏洞说明及利用

漏洞详细信息请看参考中的BlackHat2023议题,这里用一张图概况,就是恶意App将错误的文件内容或者文件参数传递给Share Target App,以使得Share Target App错误的处理接收到的信息,实现一系列攻击。
 
notion image
该仓库为Github Demo仓库
Android-DirtyStream
LLeavesGUpdated May 8, 2024

1. 文件覆盖

首先分析存在漏洞的APP ,在Manifest.xml 中存在FileActivity 作为一个ShareTarget 用于处理分享者提供的内容。在这里加入android.intent.action.SENDandroid.intent.action.SEND_MULTIPLE 表明该ShareTarget 可以接收一个或多个文件,并且声明了文件类型等信息。
针对该Activity,通过handleIntent 对接收到的intent数据进行处理,其中包括模拟对data数据的安全检查checkForbidenPath。该部分检查是从小米文件管理器抄过来的,下面也会有。
然后针对性的编写EXP APP ,指明 shareTarget目标是com.test.dirtystreamvuln.FileActivity 这里也可以使用系统的选择器去选择目标,但是考虑隐蔽性会使用直接的跳转。Uri.parse("content://com.test.android.fileprovider/file.txt?name=file.txt&_size=11&path=" + getFilesDir() + "/file.txt"); 则指明了源文件路径path 、名称name 以及size
然后是自定义的TestContentProvider ,其中对两个实际使用到的方法进行了实现。
其中openFile 读取uri中的path请求,并且通过ParcelFileDescriptor 打开后返回,由消费者进行读取。
querynew MatrixCursor(new String[]{"_display_name", "_size"}); 只包含了两个属性,但是实际上可以自定义更多(见BlackHat PPT),比如可以添加encode属性对请求的编码进行说明,以绕过某些检查。_display_name 这里设置读取uri中的name请求以控制_display_name 字段。
notion image
同理,也可以进行目录穿越修改files外的文件
Uri uri = Uri.parse("content://com.test.android.fileprovider/file.txt?name=../shared_prefs/shared_pref.xml&_size=11&path=" + getFilesDir() + "/file.txt");
notion image
 

2. 任意代码执行

众所周知,App会在/data/app目录下存放base.apk以及动态链接库so 文件。即使我们可以覆盖文件,App也没有权限访问/data/app目录下的文件,但是如果App本身还可能从其他文件夹加载so的话就可以通过覆盖该文件来实现代码任意执行。
例如下文会提到的小米文件管理器。
 

3. 文件读取

对于文件覆盖,是由VulnApp 通过ShartTarget Acitivty 接收到AttackApp传递的含有恶意dataintent 。在对恶意data进行进行安全检查后通过content provider访问Uri连接,例如Uri uri = Uri.parse("content://com.test.android.fileprovider/file.txt?name=../shared_prefs/shared_pref.xml&_size=11&path=" + getFilesDir() + "/file.txt");中实际是读取AttackApp 中的共享文件,然后将其覆盖到沙箱中的某一文件。
而对于文件读取,需要转变思路,需要读取的文件是VulnApp 沙箱或其他目录私有下的文件,读取到之后将其复制到一个共享文件夹,该文件夹必须可以被AttackApp 访问,从而实现对私有文件的泄露和读取。
为了达到这一目标,VulnApp 首先必须要存在一个content provider ,进而通过构造URI链接使其指向沙箱内部文件,例如/data/data/vuln/shared_prefs/shared_pref.xml 。然后由AttackApp 构造Intent,包含恶意数据,通过共享机制传递给VulnApp ,如果此时VulnApp 并不对收到的数据进行进一步安全校验,将会读出shared_pref.xml 并且复制到AttackApp 指定的路径中。
这里在VulnApp 引入provider 并且通过<external-path name="root" path="../../../"/> 这样的路径配置来进行内容访问,如果无法找到合适的路径配置则无法实现任意文件的读取,如果通过../进行路径穿越则会抛出异常并提升无法穿越到配置的路径外。
例如在AttackApp中构造 Uri uri = Uri.parse("content://com.test.vulnapp.fileprovider/root/data/user/0/com.test.dirtystreamvuln/shared_prefs/shared_pref.xml?displayName=../../../../../../../sdcard/test.xml"); 这样的Uri ,其中displayName=../../../../../../../sdcard/test.xml 指明了复制的目的地,在androidx.core.content.FileProvider (这里注意因为是VulnAppprovider ,所以无法通过自定义的provider控制某些字段,完全依赖于VulnApp 采用的provider_display_nameString displayName = uri.getQueryParameter("displayName"); 获得,所以对于使用默认FileProviderVulnApp 在这里直接指定displayName ,如果VulnApp 并未对displayName 进行进一步检查的话将会成功完成文件泄露。
notion image
 

0x02 小米文件管理器利用

版本选择
In Xiaomi Inc.’s File Manager, we were able to obtain arbitrary code execution in version V1-210567. After our disclosure, Xiaomi published versionV1-210593, and we verified that the vulnerability has been addressed. In WPS Office, we were able to obtain arbitrary code execution in version 16.8.1. After our disclosure, WPS published and informed us that the vulnerability has been addressed as of version 17.0.0.
所以这里选择V1-210567 版本
小米文件管理器中为了方便文件的复制和移动操作设置了很多activity-alias ,其目标都是com.android.fileexplorer.activity.FileActivity ,但是文件管理器会根据Intent指定的别名判断是要进行Copy操作还是Move操作,这里使用Copy操作,即指定com.android.fileexplorer.activity.CopyFileActivity
编写一个简单的demo,实现简单的文件复制,需要手动指定复制到的位置。
notion image
 

1. 任意文件写

FileActivity 中通过handleIntent对收到的Intent进行处理,由于设定了action为android.intent.action.SEND ,所以直接走到handleUri(data, intent, str);
而在handleUri(data, intent, str); 会通过checkIntent 完成进一步检查和复制初始化initCopyOrMoveIntent ,在内部存在多个安全检查,但是都是没有实际效果的。
initCopyOrMoveIntent方法调用checkValid方法,并将内容 URI 作为参数传递。但是,checkValid方法旨在处理文件路径,而不是内容 URI。对于内容 URI,它始终返回 true。相反,更安全的做法是将字符串解析为 URI,包括确保方案是预期值(在本例中为 file,而不是content)。checkValid方法验证复制或移动操作不会影响私有目录通过使用传入的字符串作为File类构造函数的参数来初始化文件对象,并将其规范路径与对应于应用程序主目录的路径进行比较(步骤 3 和 4)。给定一个内容 URI 作为路径,File构造函数会对其进行规范化(遵循 Unix 文件系统规范化),因此getCanonicalPath方法返回一个以/content:/开头的字符串,该字符串始终会通过有效性检查。
notion image
在选择完要复制到的目的路径后,点击粘贴将会在doInBackground 处理任务。
String absolutePath = new File(this.new_file.path, orig_file.name).getAbsolutePath(); 这里取了源文件的名称,然后和选定的路径拼接得到复制后文件的路径,可以看出新的路径是由两部分拼接,前一部分是不可控的,但是后一部分确是可控的( com.android.fileexplorer.model.c.a 负责进行query操作获取关键信息并复制),可以直接由自定义的Provider实现query请求,自定义_display_name_data
exp的原理也十分简单构造如下的数据intent.setData(Uri.parse("content://com.test.attackprovider/files/test.txt?path=" + getFilesDir() + "/test.txt" + "&name=../../../../../../../../data/user/0/com.mi.android.globalFileexplorer/files/test.txt" + "&size=10"));指定name 的值(即目的File路径拼接两部分其中的可控部分),进行路径穿越,穿越到沙箱内部。而具体控制接收者获取到的字段的方法就是自定义MyContentProvider ,这样就可以实现query 控制一切我们想要控制的字段。
notion image
 

2. 文件覆盖

由于可以进行任意文件写,所以可以尝试进行文件覆盖,选择/data/data/com.mi.android.globalFileexplorer/shared_prefs/com.mi.android.globalFileexplorer_preferences.xml 进行覆盖。
notion image
notion image
但是实际发现,即使点击确定也无法进行替换,但是可以参考BlackHat相关议题中提到的在进行getSharedPreferences 读取sharedpref文件时,会将example.xml.bak替换为example.xml 。那么就可以写入新的.bak文件,等待下次getSharedPreferences 由App完成覆盖操作。
notion image
intent.setData(Uri.parse("content://com.test.attackprovider/files/test.txt?path=" + getFilesDir() + "/test.txt" + "&name=../../../../../../../../data/user/0/com.mi.android.globalFileexplorer/shared_prefs/com.mi.android.globalFileexplorer_preferences.xml.bak" + "&size=10"));
一段时间后成功实现覆写。
notion image
 

3. 任意代码执行

由于应用程序没有对/data/app文件夹的写入权限,因此无法替换存储在那里的libixiaomifileu.so文件。
在App加载该so之前会先计算/data/app下面的libixiaomifileu.so 0x1000字节的MD5值,并与/data/data/com.mi.android.globalFileexplorer/shared_prefs/com.mi.android.globalFileexplorer_preferences.xml 中的libixiaomifileu.so_hm5 值比较,并且拿libixiaomifileu.so_2属性的值比较长度,如果一致则加载,如果不一致则从/data/data/com.mi.android.globalFileexplorer/files/lib 目录下取libixiaomifileu.so 并取前0x1000字节计算md5并且同样比较长度,若一致则加载,若还是不一致则复制/data/app下面的libixiaomifileu.so 到该目录进行加载。
notion image
那么要想实现任意代码执行只需要先向/data/data/com.mi.android.globalFileexplorer/files/lib 下写入so,然后修改com.mi.android.globalFileexplorer_preferences.xml 中的libixiaomifileu.so_hm5字段和libixiaomifileu.so_2长度字段的值并覆盖,等待替换完成即可实现自定义so的加载。
编译so反弹shell,使用文件写将该so写入到files/lib ,然后再次使用写文件(写.bak)覆写com.mi.android.globalFileexplorer_preferences.xml,修改长度和前0x1000字节的MD5值,然后下次进行垃圾清理或者主动打开导出的垃圾清理Activity时就会加载恶意的so,从而反弹shell。
notion image

4. 修复

小米直接对传入的数据进行检测,检测其中是否存在../.. 如果存在返回值为2,从而判断存在无效路径。
notion image
notion image
notion image

0x03 修复建议

为了防止这些问题,在处理其他应用程序发送的文件流时,最安全的解决方案是在缓存接收到的内容时完全忽略远程文件提供程序返回的名称。例如使用随机生成的名称,即使传入流的内容格式错误,也不会篡改应用程序。
如果这种方法不可行,开发人员需要采取额外的步骤来确定缓存文件是否写入专用目录。由于传入的文件流通常由内容 URI 标识,因此第一步是可靠地识别和sanitize相应的文件名。除了过滤可能导致路径遍历的字符以及在执行任何写入操作之前,开发人员还必须通过调用 File.getCanonicalPath并验证返回值的前缀来验证缓存的文件是否位于专用目录中。另一个需要保护的领域是开发人员尝试从内容 URI 中提取文件名的方式。开发人员经常使用Uri.getLastPathSegment()它返回最后一个路径 URI 段的(URL)解码值。攻击者可以在此段中使用 URL 编码字符(包括用于路径遍历的字符)构造URI

参考

  1. ShareTarget
  1. https://developer.android.com/training/sharing/receive?hl=zh-cn
  1. “Dirty stream” attack: Discovering and mitigating a common vulnerability pattern in Android apps
  1. dirty-stream-attack-turning-android-share-targets-into-attack-vectors | BlackHat
  1. https://www.youtube.com/watch?v=oZTGR9vJVMQ
  1. Android应用程序间的内容分享机制-----SEND/SEND_MULTIPLE
 
ByteDance-AppShark静态分析工具Pixel5 内核编译并嵌入rwProcMem33
  • Twikoo