Java
Scala
セキュリティ
黒魔術
zip-slip

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

みなさん、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コードで書かないといけないのがげんなり…。