Do you know Zip-Slip, a directory traversal vulnerability when unzipping Zip?
I will leave the details to other articles such as ↓, but this time I will try to fix the library called zip4j by bytecode operation. The impact of the Zip Slip directory traversal vulnerability affects many Java projects
By the way, the vulnerability countermeasure version of each library that handles Zip is listed here. snyk/zip-slip-vulnerability: Zip Slip Vulnerability (Arbitrary file write through archive extraction) It seems that zip4j does not have a jar distribution ... If you take the stance of distributing the source code and making it into a jar by yourself, I think that it is likely that you will not use the latest version ...
↓ zip-slip test data 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 dynamic security patch with bytecode rewriting
*/
object Zip4jSecurity {
def main(args: Array[String]): Unit = {
patch()
//Test if validation is working. zip-An exception occurs due to slip validation.
val zipFile = new ZipFile("/home/momose/Documents/zip-slip.zip")
zipFile.extractAll("/home/momose/Documents/tmp/")
}
/**
*Apply the patch
*/
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)
//Register with class loader
cc.toClass(null, clazz.getProtectionDomain)
}
private class ZipSlipValidationEditor extends ExprEditor {
//Flags that apply only to the first method call
private var unedited = true
override def edit(m: MethodCall): Unit = {
if (unedited &&
m.getClassName.equals("net.lingala.zip4j.model.CentralDirectory") &&
m.getMethodName.equals("getFileHeaders")) {
//Validation code
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("Illegal ZIP");
}
}
$_ = fileHeaders;"""
m.replace(statement)
unedited = false
}
}
}
}
Exception in thread "main" java.lang.RuntimeException:Illegal 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)
It validates the result of the Java method getCanonicalPath
, such as realpath
in C and Linux commands.
↓ Reference materials
IDS02 \ -J . Normalize pathnames before validation
Only when used in the net.lingala.zip4j.core.ZipFile # extractAll
method
Vulnerabilities may develop in other cases because we have not confirmed that they will be countermeasures.
I want you to test it yourself.
↓ It is prepared in the net.lingala.zip4j.model.CentralDirectory#getFileHeaders ()
method call of this part.
zip4j/Unzip.java at master · supasate/zip4j
With Scala, string literals should be easy to handle. Also, it would be nice if all exceptions were treated like unchecked exceptions. When rewriting with javassist, it is awkward to write in Java code that matches the Java version to be rewritten.
Recommended Posts