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