Mukai Systems

開けるな危険

Zip Slip Vulnerabilityという、Zip などのアーカイブファイルの展開処理における脆弱性がある。業務で触れる機会があったため少し調べてみた。

Zip Slip Vulnerabilityについて

JPCERT/CCはZip Slip Vulnerabilityについて以下のように言及している。

Snyk セキュリティチームによると、ファイル名に特定の文字列を含んだファイル (../../ファイル名) をアーカイブファイルにいれて処理させることで、Web アプリケーションの権限で特定の場所にファイルを置くことができることが言及されています。攻撃者がこの方法を悪用することで、実行ファイルの上書きを行ったり、設定ファイルの書き換えを行ったりすることが可能となり、結果として、リモートから任意のコードを実行される恐れがあります。

Snykのセキュリティチームが以下のように述べているように、

The vulnerability has been found in multiple ecosystems, including JavaScript, Ruby, .NET and Go, but is especially prevalent in Java, where there is no central library offering high level processing of archive (e.g. zip) files. The lack of such a library led to vulnerable code snippets being hand crafted and shared among developer communities such as StackOverflow .

  1. 高水準なライブラリがない
  2. 脆弱性のあるコードが開発者のコミュニティーで拡散されている

といった理由で、Javaを使用している場合は顕著にこの脆弱性に該当するとのことである。

Zip Slip Vulnerabilityの検証

以下のシナリオを想定して検証してみた。

  1. 攻撃者が悪意のあるスクリプト「evil.sh」を含むZipファイル「good.zip」を作成する。
  2. アプリケーションが脆弱性のあるプログラムで「good.zip」を展開する。

悪意のあるZipファイルの作成

悪意のあるZipファイルを作成するために、以下のParenスクリプト「evilzip.p」を作成した。

; make evil zip.

(import :zip)

(function compress ()
  (with-open ($out "good.zip" :write)
    (let (wr (.new ZipWriter))
      (.add wr (path "good.txt"))
      (.add wr (path "evil.sh") "../evil.sh")    ; directory traversal file name.
      (.write wr))))

(function! main (args)
  (compress))

「evilzip.p」は実行すると、「good.txt」と「evil.sh」からなる、「good.zip」を作成する。ここで、「evil.sh」はdirectory traversal file name(展開先のディレクトリの外に配置されるような名前)を指定してある。これにより、Zip Slip Vulnerabilityがあるプログラムで展開した際に、「evil.sh」が予期せぬ場所に展開される。

以下のようにスクリプトを実行して「good.zip」を生成した。

$ paren ls
evil.sh
evilzip.p
good.txt

$ paren evilzip

$ paren ls
evil.sh
evilzip.p
good.txt
good.zip

zipinfoで確認すると、「evil.sh」が不正なファイル名「../evil.sh」になっていることがわかる。

$ paren zipinfo good.zip
      5           5 100% 2022-06-07 Tue 11:16:46 good.txt
      5           5 100% 2022-06-07 Tue 11:16:50 ../evil.sh

これにて悪意のあるZipファイルは作成完了である。今回は勉強のために自作したが、SnykのセキュリティーチームがGitHubでそのようなZipファイルを公開している。

脆弱性のあるJavaプログラムの作成・実行

今回はStackOverflowを参考に脆弱性のあるプログラム「UnZip.java」を作成した。

import java.io.*;
import java.util.*;
import java.util.zip.*;

public class UnZip {

  static int BUF_SIZE = 2048;

  public static void unzip(File zipFile, File dst) throws IOException {
    try (ZipFile zip = new ZipFile(zipFile)) {
      Enumeration<? extends ZipEntry> zipFileEntries = zip.entries();
      while (zipFileEntries.hasMoreElements()) {
        ZipEntry entry = zipFileEntries.nextElement();
        if (!entry.isDirectory()) {
          File file = new File(dst, entry.getName());
          file.getParentFile().mkdirs();
          try (BufferedInputStream bis = new BufferedInputStream(zip.getInputStream(entry));
              FileOutputStream fos = new FileOutputStream(file);
              BufferedOutputStream bos = new BufferedOutputStream(fos, BUF_SIZE))
          {
            int size;
            byte[] buf = new byte[BUF_SIZE];
            while ((size = bis.read(buf, 0, BUF_SIZE)) != -1)
              bos.write(buf, 0, size);
            bos.flush();
          }
        }
      }
    }
  }

  public static void main(String[] args) throws IOException {
    unzip(new File(args[0]), new File(args[1]));
  }

}

検証しやすいように、コマンドライン引数にZipファイルと展開先を指定するように作成した。典型的なJavaによるZipファイル展開処理であり、特筆すべき点は特にない。

作成したプログラムを用いて確認する。

$ javac UnZip.java
$ java UnZip good.zip good

実行すると、以下のようにdirectory traversalが観測できた。

good/
 |  good.txt
  evil.sh    -- goodディレクトリの外に展開されている!

Zip Slip Vulnerabilityの対策

「UnZip.java」のZip Slip Vulnerabilityを修正した「UnZip2.java」を以下に示す。

import java.io.*;
import java.util.*;
import java.util.zip.*;

public class UnZip2 {

  static int BUF_SIZE = 2048;

  public static boolean isDirectoryTraversal(File root, File file) throws IOException {
    return !file.getCanonicalPath().startsWith(root.getCanonicalPath() + File.separator);
  }

  public static void unzip(File zipFile, File dst) throws IOException {
    try (ZipFile zip = new ZipFile(zipFile)) {
      Enumeration<? extends ZipEntry> zipFileEntries = zip.entries();
      while (zipFileEntries.hasMoreElements()) {
        ZipEntry entry = zipFileEntries.nextElement();
        if (!entry.isDirectory()) {
          File file = new File(dst, entry.getName());
          if (isDirectoryTraversal(dst, file)) {
            throw new RuntimeException("directory traversal detected");
          }
          file.getParentFile().mkdirs();
          try (BufferedInputStream bis = new BufferedInputStream(zip.getInputStream(entry));
              FileOutputStream fos = new FileOutputStream(file);
              BufferedOutputStream bos = new BufferedOutputStream(fos, BUF_SIZE))
          {
            int size;
            byte[] buf = new byte[BUF_SIZE];
            while ((size = bis.read(buf, 0, BUF_SIZE)) != -1)
              bos.write(buf, 0, size);
            bos.flush();
          }
        }
      }
    }
  }

  public static void main(String[] args) throws IOException {
    unzip(new File(args[0]), new File(args[1]));
  }

}

Zip Slip Vulnerabilityを防ぐためには、展開するファイルが対象のディレクトリの中かどうか判定できればよい。「UnZip2」では、そのための関数isDirectoryTraversalを追加して検証するように修正してある。

実行すると、以下のようにdirectory traversalが観測できた。

$ java UnZip2 good.zip good
Exception in thread "main" java.lang.RuntimeException: directory traversal detected
        at UnZip2.unzip(UnZip2.java:21)
        at UnZip2.main(UnZip2.java:40)

まとめ

この脆弱性を用いると、攻撃者は任意の実行可能ファイルを被害者が予期せぬ場所に展開できる。これをリモートで実行したり、利用者が誤って実行したりすることにより、攻撃者は目的を達成することができる。

したがって、信頼できないZipファイルを展開する場合はその中身を十分に検証しなければならない。

この脆弱性を調べていて、書籍「97 Things Every Programmer Should Know」の中のMake Interfaces Easy to Use Correctly and Hard to Use Incorrectlyという話を思いだした。

本質的にはJavaのAPIに非はなく、十分な検証を行わずに脆弱性のあるコードを使用する技術者が悪いのかもしれない。一方で、容易に誤った使い方をすることが想定されてしまうAPIは改善の余地があるのかもしれない。1

Source code


[1] 別途高水準なAPIを用意するとか。