Android中的目录穿越漏洞

概述

在Linux/Unix系统中“..”代表的是向上级目录跳转,如果程序在处理到诸如用“../../../../../../../../../../../etc/hosts”表示的文件时没有进行防护,则会跳转出当前工作目录,跳转到到其他目录中,即目录穿越。

在 Android 中由于目录穿越造成任意文件写入的漏洞较为常见。首先是写文件的接口可能本身设计上就允许传入任意路径的参数,另一种情况就是直接拼接路径导致可以 “…/” 进行目录穿越。

由于存在目录穿越漏洞的代码执行者通常是应用本身(非webview沙箱进程),因此一旦存在任意文件写入漏洞,就有可能覆盖原有的文件。此时若应用中存在动态加载代码的逻辑,配合目录穿越则有可能实现任意代码执行。

add:
后来又多个团队或研究人员研究目录穿越问题,并起来各式各样的名字,现留下链接以备参考:

  1. zip-slip总结了多个存在目录穿越的第三方语言或库,并包含了具体的代码实例 https://github.com/snyk/zip-slip-vulnerability
    https://snyk.io/research/zip-slip-vulnerability
  2. zipperdown iOS平台下多个APP存在目录穿越漏洞

目录穿越场景

1. 下载文件到指定的路径

该场景适用于下载路径由用户可控,例如一些下载工具、保存的目录为用户名等特殊情况。

2. 解压 zip 文件时未对 ZipEntry 文件名检查

Android应用中通常会直接下载一些皮肤等资源文件,为了节约流量,一些资源文件通常打包成zip。

Java代码在解压ZIP文件时会使用到ZipEntry类的getName()方法。如果ZIP文件中包含“../”的字符串,该方法返回值里面会原样返回。如果在这里没有进行防护,继续解压缩操作,就会将解压文件创建到其他目录中。

漏洞代码如下:

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
public void UnZipFolder(String zipFileString, String outPathString) throws Exception {
//ZipInputStream inZip = new ZipInputStream(new FileInputStream(zipFileString));
ZipInputStream inZip = new ZipInputStream(getAssets().open("a.zip"));
ZipEntry zipEntry;
String szName = "";
while ((zipEntry = inZip.getNextEntry()) != null) {
//调用zipEntry的getName方法没有检查是否包含"../"
szName = zipEntry.getName();
if (zipEntry.isDirectory()) {
// get the folder name of the widget
szName = szName.substring(0, szName.length() - 1);
File folder = new File(outPathString + File.separator + szName);
folder.mkdirs();
} else {

File file = new File(outPathString + File.separator + szName);
file.createNewFile();
// get the output stream of the file
FileOutputStream out = new FileOutputStream(file);
int len;
byte[] buffer = new byte[1024];
// read (len) bytes into buffer
while ((len = inZip.read(buffer)) != -1) {
// write (len) byte from buffer at the position 0
out.write(buffer, 0, len);
out.flush();
}
out.close();
}
}
inZip.close();
}

此时,构造一个zip文件,其中某一个文件的名字为“../../../../../../sdcard/exec.so”,调用上述代码时就会触发漏洞。

在主流操作系统中,文件名是不允许包含“/”的,那么也就无法通过直接压缩的方式构造zip包了。那么如何构造一个包含“../”的zip文件?

可以先计算含“../”的文件名长度,例如“../../../../../../sdcard/exec.so”长度为32,我们先创建一个文件“aaaaaaaaaaaaaaaaaaaaaaaaaaaaa.so”,之后压缩成为zip,然后再用010editor找到对应字段直接修改即可。

upload successful

3. 下载时未对 Content-Disposition进行合法性检查

Content-Disposition 是常见的 HTTP 协议 header,在文件下载时可以告诉客户端浏览器下载的文件名。例如服务器返回 Content-Disposition: attachment; filename=”my.html” ,浏览器将弹出另存为对话框(或直接保存),默认的文件名就是 my.html。

但这个 filename 参数显然是不可信任的。例如恶意网站返回的文件名包含 ../,当 Android 应用尝试将这个文件保存到 /sdcard/Downloads 时,攻击者就有机会把文件写入到 /data/ 目录中了:

例如如下的代码逻辑:

服务端:

1
2
3
4
<?php
header('Content-Disposition: attachment;filename=../../../../../../../../sdcard/hack.php');
echo "hello,m4bln";
?>

客户端

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
public void downloadFiles(String urlStr){
FileOutputStream output;

try {

URL url=new URL(urlStr);
//创建一个HTTP链接
HttpURLConnection urlConn=(HttpURLConnection)url.openConnection();
urlConn.connect();

//根据header中的Content-Disposition 获取文件名
String fileName = urlConn.getHeaderField("Content-Disposition");
fileName = URLDecoder.decode(fileName.substring(fileName.indexOf("filename")+10,fileName.length()-1),"UTF-8");

InputStream inputStream=urlConn.getInputStream();

File file=new File(getApplicationContext().getFilesDir().toString()+"/"+fileName);

output=new FileOutputStream(file);
byte buffer[]=new byte[4*1024];//每次存4K
int temp;
//写入数据
while((temp=inputStream.read(buffer))!=-1){
output.write(buffer,0,temp);
}
output.flush();
} catch (Exception e) {

System.out.println("读写数据异常:"+e);

}

}

注: 当使用默认浏览器下载时,浏览器会把“../”替换为”.._”
upload successful

几种常见的 Android 下动态加载可执行代码

如果应用动态加载代码之前未做签名校验,利用目录穿越漏洞进行覆盖,可实现稳定的任意代码执行。此外由于在文件系统中写入了可执行文件,还可以实现持久化攻击的效果。

  1. DexClassLoader 动态载入应用可写入的 dex 可执行文件
  2. java.lang.Runtime.exec 方法执行应用可写入的 elf 文件
  3. System.load 和 System.loadLibrary 动态载入应用可写入的 elf 共享对象
  4. 本地代码使用 system、popen 等类似函数执行应用可写入的 elf 文件
  5. 本地代码使用 dlopen 载入应用可写入的 elf 共享对象
  6. 利用 Multidex 机制:A Pattern for Remote Code Execution using Arbitrary File Writes and MultiDex Applications

关于动态加载需要注意以下几点:

1. 在Android中,System.loadLibrary()是从应用的lib目录中加载.so文件,而System.load()是用某个.so文件的绝对路径加载,这个.so文件可以不在应用的lib目录中,可以在SD卡中,或者在应用的files目录中,只要应用有读的权限目录中即可。
2. 在files目录中,应用具有写入权限,利用ZIP文件目录遍历漏洞可以替换掉原先的so文件,达到远程命令执行的目的。而应用的lib目录是软链接到了/data/app-lib/应用目录,属于system用户,第三方应用在执行时没有写入/data/app-lib目录的权限

常见的修复方法

  1. 对重要的ZIP压缩包文件进行数字签名校验,校验通过才进行解压。
  2. 检查Zip压缩包中使用ZipEntry.getName()获取的文件名中是否包含”../”或者”..”
    检查”../”的时候不必进行URI Decode(以防通过URI编码”..%2F”来进行绕过),测试发现ZipEntry.getName()对于Zip包中有“..%2F”的文件路径不会进行处理。

参考链接

  1. https://jaq.alibaba.com/community/art/show?articleid=383
  2. https://zhuanlan.zhihu.com/p/28107901