type
status
date
slug
summary
tags
category
icon
password
0x00 引言
Android 操作系统通过为每个应用程序分配自己的专用数据和内存空间来强制隔离。为了促进数据和文件共享,Android 提供了一个称为
Content Provider
的组件,它充当一个接口,用于以安全的方式管理数据并将数据公开给其他已安装的应用程序。如果使用得当,内容提供商可以提供可靠的解决方案。但是,实施不当可能会引入漏洞,从而绕过应用程序主目录中的读/写限制。就像应用可以向其他应用发送数据一样,它也可以从其他应用接收数据。考虑用户如何与您的应用互动,以及希望从其他应用接收哪些类型的数据。例如,社交网络应用可能希望从其他应用接收文本内容。
其他应用的用户经常通过
Android Sharesheet
或 intent
解析器向您的应用发送数据。向您的应用发送数据的应用必须为该数据设置 MIME 类型。应用可以通过以下方式接收其他应用发送的数据0x01 漏洞说明及利用
漏洞详细信息请看参考中的BlackHat2023议题,这里用一张图概况,就是恶意App将错误的文件内容或者文件参数传递给Share Target App,以使得Share Target App错误的处理接收到的信息,实现一系列攻击。
该仓库为Github Demo仓库
Android-DirtyStream
LLeavesG • Updated May 8, 2024
1. 文件覆盖
首先分析存在漏洞的
APP
,在Manifest.xml
中存在FileActivity
作为一个ShareTarget
用于处理分享者提供的内容。在这里加入android.intent.action.SEND
和android.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
打开后返回,由消费者进行读取。在
query
中new MatrixCursor(new String[]{"_display_name", "_size"});
只包含了两个属性,但是实际上可以自定义更多(见BlackHat PPT
),比如可以添加encode
属性对请求的编码进行说明,以绕过某些检查。_display_name
这里设置读取uri
中的name
请求以控制_display_name
字段。同理,也可以进行目录穿越修改
files
外的文件Uri uri = Uri.
parse
(
"content://com.test.android.fileprovider/file.txt?name=../shared_prefs/shared_pref.xml&_size=11&path="
+ getFilesDir() +
"/file.txt"
);
2. 任意代码执行
众所周知,App会在
/data/app
目录下存放base.apk
以及动态链接库so
文件。即使我们可以覆盖文件,App也没有权限访问/data/app
目录下的文件,但是如果App本身还可能从其他文件夹加载so的话就可以通过覆盖该文件来实现代码任意执行。例如下文会提到的小米文件管理器。
3. 文件读取
对于文件覆盖,是由
VulnApp
通过ShartTarget Acitivty
接收到AttackApp
传递的含有恶意data
的intent
。在对恶意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
(这里注意因为是VulnApp
的provider
,所以无法通过自定义的provider
控制某些字段,完全依赖于VulnApp
采用的provider
)中_display_name
由String displayName = uri.getQueryParameter("
displayName
");
获得,所以对于使用默认FileProvider
的VulnApp
在这里直接指定displayName
,如果VulnApp
并未对displayName
进行进一步检查的话将会成功完成文件泄露。0x02 小米文件管理器利用
版本选择In Xiaomi Inc.’s File Manager, we were able to obtain arbitrary code execution in versionV1-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 version16.8.1
. After our disclosure, WPS published and informed us that the vulnerability has been addressed as of version17.0.0
.所以这里选择V1-210567
版本
小米文件管理器中为了方便文件的复制和移动操作设置了很多
activity-alias
,其目标都是com.android.fileexplorer.activity.FileActivity
,但是文件管理器会根据Intent指定的别名判断是要进行Copy操作还是Move操作,这里使用Copy操作,即指定com.android.fileexplorer.activity.CopyFileActivity
。编写一个简单的demo,实现简单的文件复制,需要手动指定复制到的位置。
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:/
开头的字符串,该字符串始终会通过有效性检查。在选择完要复制到的目的路径后,点击粘贴将会在
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
控制一切我们想要控制的字段。2. 文件覆盖
由于可以进行任意文件写,所以可以尝试进行文件覆盖,选择
/data/data/com.mi.android.globalFileexplorer/shared_prefs/com.mi.android.globalFileexplorer_preferences.xml
进行覆盖。但是实际发现,即使点击确定也无法进行替换,但是可以参考BlackHat相关议题中提到的在进行
getSharedPreferences
读取sharedpref
文件时,会将example.xml.bak
替换为example.xml
。那么就可以写入新的.bak
文件,等待下次getSharedPreferences
由App完成覆盖操作。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"));
一段时间后成功实现覆写。
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
到该目录进行加载。那么要想实现任意代码执行只需要先向
/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。4. 修复
小米直接对传入的数据进行检测,检测其中是否存在
../
和..
如果存在返回值为2,从而判断存在无效路径。0x03 修复建议
为了防止这些问题,在处理其他应用程序发送的文件流时,最安全的解决方案是在缓存接收到的内容时完全忽略远程文件提供程序返回的名称。例如使用随机生成的名称,即使传入流的内容格式错误,也不会篡改应用程序。
如果这种方法不可行,开发人员需要采取额外的步骤来确定缓存文件是否写入专用目录。由于传入的文件流通常由内容 URI 标识,因此第一步是可靠地识别和
sanitize
相应的文件名。除了过滤可能导致路径遍历的字符以及在执行任何写入操作之前,开发人员还必须通过调用 File.getCanonicalPath
并验证返回值的前缀来验证缓存的文件是否位于专用目录中。另一个需要保护的领域是开发人员尝试从内容 URI 中提取文件名的方式。开发人员经常使用Uri.getLastPathSegment()
,它返回最后一个路径 URI 段的(URL)解码值。攻击者可以在此段中使用 URL 编码字符(包括用于路径遍历的字符)构造URI。参考
- 作者:LLeaves
- 链接:https://lleavesg.top//article/Android-DirtyStream
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章