2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Scala】【Java】Zip4jのZip解凍時のディレクトリトラバーサル脆弱性をバイトコード操作で直してみた

Last updated at Posted at 2019-04-20

みなさん、Zip-SlipというZip解凍時のディレクトリトラバーサル脆弱性をご存知でしょうか?

詳細は↓のような他の記事に譲りますが、今回はzip4jというライブラリをバイトコード操作して直してみます。
Zip Slipディレクトリトラバーサル脆弱性の影響は多くのJavaプロジェクトに

ちなみに、Zipを扱う各ライブラリの脆弱性対策バージョンはここに記載してあります。
snyk/zip-slip-vulnerability: Zip Slip Vulnerability (Arbitrary file write through archive extraction)
zip4jはjarの配布が無いっぽいですね…。
ソースコード配布するから各自でjarにしてくださいというスタンスだと、最新版を使ってくれない場合が発生しやすくなると思うんだけどなあ…。

↓zip-slipのテストデータ
zip-slip-vulnerability/archives at master · snyk/zip-slip-vulnerability

直してみたコード

import javassist._
import javassist.expr.ExprEditor
import javassist.expr.MethodCall
import net.lingala.zip4j.core.ZipFile

/**
  * バイトコード書き換えによるZip4jの動的なセキュリティパッチ
  */
object Zip4jSecurity {
  def main(args: Array[String]): Unit = {
    patch()

    // バリデーションが効いているかテスト。zip-slipバリデーションによる例外が発生する。
    val zipFile = new ZipFile("/home/momose/Documents/zip-slip.zip")
    zipFile.extractAll("/home/momose/Documents/tmp/")
  }

  /**
    * パッチを当てます
    */
  def patch(): Unit = {
    val cp = ClassPool.getDefault
    val classLoader = Thread.currentThread().getContextClassLoader()
    cp.appendClassPath(new LoaderClassPath(classLoader))
    val cc = cp.get("net.lingala.zip4j.unzip.Unzip")
    val zipSlipValidationEditor = new Zip4jSecurity.ZipSlipValidationEditor
    cc.instrument(zipSlipValidationEditor)
    // クラスローダーに登録する
    cc.toClass(null, clazz.getProtectionDomain)
  }

  private class ZipSlipValidationEditor extends ExprEditor {
    // 1つ目のメソッド呼び出しにのみ適用させるフラグ
    private var unedited = true

    override def edit(m: MethodCall): Unit = {
      if (unedited &&
        m.getClassName.equals("net.lingala.zip4j.model.CentralDirectory") &&
        m.getMethodName.equals("getFileHeaders")) {
        // バリデーションのコード
        val statement ="""
java.io.File outputDir = new java.io.File(outPath);
java.util.List fileHeaders = $0.getFileHeaders($$);
for( int i=0; i<fileHeaders.size(); i++ ){
    net.lingala.zip4j.model.FileHeader e = (net.lingala.zip4j.model.FileHeader)fileHeaders.get(i);
    if (!new java.io.File(outputDir, e.getFileName()).getCanonicalPath().startsWith(outputDir.getCanonicalPath())) {
            throw new RuntimeException("不正なZIPです");
    }
}
$_ = fileHeaders;"""
        m.replace(statement)
        unedited = false
      }
    }
  }

}

実行結果

Exception in thread "main" java.lang.RuntimeException: 不正なZIPです
	at net.lingala.zip4j.unzip.Unzip.extractAll(Unzip.java:50)
	at net.lingala.zip4j.core.ZipFile.extractAll(ZipFile.java:488)
	at net.lingala.zip4j.core.ZipFile.extractAll(ZipFile.java:451)
	at com.github.momosetkn.zip.Zip4jSecurity$.main(Zip4jSecurity.scala:18)
	at com.github.momosetkn.zip.Zip4jSecurity.main(Zip4jSecurity.scala)

C言語やLinuxコマンドでいうとrealpathのようなJavaのメソッドgetCanonicalPathの結果をバリデーションしています。
↓参考資料
IDS02-J. パス名は検証する前に正規化する

net.lingala.zip4j.core.ZipFile#extractAllメソッドでを使うケースしか、
対策になることを確認していないため、他のケースでは脆弱性が発現するかもしれない。
そのへんは各自でテストしてほしい。

↓この部分のnet.lingala.zip4j.model.CentralDirectory#getFileHeaders()メソッド呼び出しのところに仕込んでいます。
zip4j/Unzip.java at master · supasate/zip4j

感想

Scalaだと文字列リテラルが扱いやすくていいですね。
それと、例外が全て非チェック例外のような扱いになるのがいいですね。
javassistで書き換えるとき、バイトコード書き換え対象のJavaのバージョンに合わせたJavaコードで書かないといけないのがげんなり…。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?