
التحليل الفني لثغرة CVE-2026-33701، وهي ثغرة إلغاء تسلسل غير آمن في أداة تتبع OpenTelemetry Java Agent الخاصة بـ RMI، بما في ذلك شروط الاستغلال وتفاصيل التصحيح واستراتيجيات التخفيف.
الخطورة: حرجة (CVSS v4.0: 9.3)
الإصدارات المتأثرة: opentelemetry-javaagent < 2.26.1
تم التصحيح في: 2.26.1 (صدر في 23 مارس 2026)
يُعد OpenTelemetry على الأرجح إطار المراقبة الأكثر انتشارًا في نظام Java البيئي حاليًا. يُستخدم وكيل Java تحديدًا من قبل آلاف الخدمات الإنتاجية لأتمتة تزويد التطبيقات بأدوات التتبع والمقاييس والسجلات دون الحاجة إلى أي تغييرات في الكود من المطور. فقط تقوم بإرفاقه كعلامة javaagent وهو يقوم بكل شيء خلف الكواليس.
ما يجعل CVE-2026-33701 مثيرًا للاهتمام ليس فقط الثغرة نفسها، بل طبيعة كيفية إدخالها. يقوم الوكيل بهدوء بتسجيل نقطة نهاية RMI مخصصة كأثر جانبي لأدوات RMI الخاصة به. المطورون لا يعلمون بوجودها. ليست في أي كود تطبيق. ليست شيئًا قاموا بتكوينه. الوكيل وضعها تلقائيًا، وبالنسبة للإصدارات الأقل من 2.26.1 كانت تلك النقطة تقوم بإلغاء تسلسل البيانات الواردة دون أي عوامل تصفية مطبقة.
إذا كان التطبيق يمتلك بالفعل منفذ RMI أو JMX مكشوفًا، يمكن لمهاجم لديه وصول شبكي إلى ذلك المنفذ إرسال حمولة متسلسلة مصممة بعناية إلى نقطة النهاية المخصصة للوكيل وربما تحقيق تنفيذ عن بُعد للكود بصلاحيات عملية JVM. العائق هو أن RCE يتطلب وجود سلسلة أدوات (gadget chain) متوافقة على مسار فئات التطبيق. المزيد عن ذلك لاحقًا.
عندما يلتصق وكيل OTel Java بجهاز JVM، يقوم بتزويد استدعاءات RMI بالأدوات لنشر السياق. الفكرة هي أنه عندما يستدعي عميل RMI طريقة عن بُعد، يحتاج الوكيل إلى نشر سياق التتبع الحالي إلى جانب الخادم حتى تتصل الامتدادات (spans) بشكل صحيح عبر حدود الخدمات.
للقيام بذلك، يسجل الوكيل كائن RMI المخصص الخاص به باستخدام ObjID مبرمج بشكل ثابت. في :
ContextPropagator.javapublic static final ObjID CONTEXT_CALL_ID =
new ObjID("io.opentelemetry.javaagent.context-call".hashCode());
هذا ObjID هو كيفية تحديد الوكيل لنقطة النهاية الخاصة به داخل بيئة RMI. عندما يتصل عميل RMI مُزوَّد بالأدوات بخادم، يتحقق أولاً مما إذا كان الخادم يحتوي على هذا ObjID مسجلاً. إذا كان كذلك، يرسل حمولة نشر السياق إليه. إذا لم يكن كذلك، يتخطى نشر السياق ويقوم بالاستدعاء العادي فقط.
المشكلة هي أن هذه النقطة مسجلة داخل بيئة RMI الخاصة بـ JVM، مما يعني أنها تشارك نفس النقل (transport) مع أي منفذ RMI أو JMX مفتوح لدى التطبيق. إذا كان التطبيق يحتوي على -Dcom.sun.management.jmxremote.port مضبوطًا، أو إذا كان يصدر خدمات RMI الخاصة به، فإن نقطة نهاية الوكيل تكون قابلة للوصول على نفس المنفذ من قبل أي شخص يمكنه الاتصال به.
يحدث إلغاء التسلسل في ContextPayload.java. في الإصدارات الأقل من 2.26.1 كانت طريقة read() تبدو هكذا:
@SuppressWarnings("BanSerializableRead") // fine
public static ContextPayload read(ObjectInput oi) throws IOException {
try {
Object object = oi.readObject();
if (object instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> map = (Map<String, String>) object;
return new ContextPayload(map);
}
} catch (ClassCastException | ClassNotFoundException ex) {
logger.log(FINE, "Error reading object", ex);
}
return null;
}
التعليق التوضيحي @SuppressWarnings("BanSerializableRead") مع التعليق // fine هو في الواقع كاشف إلى حد ما. شخص ما في الفريق أشار إلى هذا كمصدر قلق في وقت ما، تمت إضافة القمع، وكان التعليق يهدف إلى تبريره. من الواضح أنه لم يكن جيدًا.
oi.readObject() بدون عوامل تصفية تسلسل هو النمط الكلاسيكي لإلغاء التسلسل غير الآمن. سيقوم تسلسل Java بسعادة بإنشاء أي فئة على مسار الفئات أثناء إلغاء التسلسل، وإذا كان المهاجم يمكنه التحكم في التدفق المتسلسل، يمكنه اختيار الفئات التي سيتم إنشاؤها وبأي ترتيب. هذا هو أساس هجمات سلسلة الأدوات (gadget chain).
يقوم الكود بالتحقق من instanceof Map بعد إلغاء التسلسل، ولكن بحلول تلك النقطة يكون الضرر قد حدث بالفعل. يتم تنفيذ سلسلة الأدوات أثناء استدعاء readObject() نفسه، قبل أي فحص للأنواع. فحص instanceof غير ذي صلة تمامًا من منظور أمني.
الإصلاح في 2.26.1 يستبدل نهج إلغاء التسلسل بالكامل. بدلاً من تسلسل كائن Map واستدعاء readObject()، يقوم التنفيذ الجديد بقراءة إدخالات السياق يدويًا كأنواع بدائية:
@Nullable
public static ContextPayload read(ObjectInput oi) throws IOException {
int size = oi.readInt();
if (size > MAX_CONTEXT_ENTRIES) {
logger.log(
FINE,
"RMI context propagation payload size {0} exceeds maximum allowed of {1}, skipping context propagation.",
new Object[] {size, MAX_CONTEXT_ENTRIES});
return null;
}
Map<String, String> map = new HashMap<>();
for (int i = 0; i < size; i++) {
String key = oi.readUTF();
String value = oi.readUTF();
map.put(key, value);
}
return new ContextPayload(map);
}
وتم تحديث جانب الكتابة ليتوافق:
public void write(ObjectOutput out) throws IOException {
int size = context.size();
if (size > MAX_CONTEXT_ENTRIES) {
out.writeInt(0);
return;
}
out.writeInt(size);
for (Map.Entry<String, String> entry : context.entrySet()) {
out.writeUTF(entry.getKey());
out.writeUTF(entry.getValue());
}
}
هذا النهج يقرأ فقط الأنواع البدائية (أعداد صحيحة وسلاسل UTF) من التدفق. لا يوجد إنشاء كائنات. لا يمكن لأي سلسلة أدوات التنفيذ عبر readInt() أو readUTF(). الإصلاح صحيح وكامل.
هناك أيضًا تغيير ثانٍ يستحق الملاحظة. تمت إضافة إصدار إلى ObjID المستخدم لتحديد نقطة النهاية المخصصة للوكيل:
// Before (2.26.0)
new ObjID("io.opentelemetry.javaagent.context-call".hashCode());
// After (2.26.1)
new ObjID("io.opentelemetry.javaagent.context-call-v2".hashCode());
هذا يعني أن الوكيل المُصحح لن يستجيب لـ ObjID القديم على الإطلاق. المهاجم الذي يستهدف نقطة النهاية القديمة لن يحصل على أي استجابة من خادم مُصحح. هذا إجراء تقوية إضافي جيد فوق إصلاح إلغاء التسلسل، فهو يكسر فعليًا أي استغلال تم بناؤه ضد نقطة النهاية v1 حتى لو كان إلغاء التسلسل لا يزال قابلاً للوصول بطريقة ما.
يجب أن تكون جميع الشروط الثلاثة التالية صحيحة في نفس الوقت ليكون هذا قابلاً للاستغلال:
1. وكيل OpenTelemetry Java ملتصق على JDK 16 أو أقل
هذا هو القيد الأكثر أهمية. قدم JDK 17 تغييرات كبيرة في داخل RMI ويفرض تغليفًا أكثر صرامة للوحدات عبر نظام منصة Java للوحدات (Java Platform Module System). معظم سلاسل الأدوات التي تعمل ضد إلغاء تسلسل Java تعتمد على الانعكاس (reflection) للوصول إلى فئات JDK الداخلية أو لاستدعاء طرق لن تكون قابلة للوصول عادةً. يكسر JDK 17 مسارات الانعكاس تلك افتراضيًا بسبب التغليف القوي، مما يعني أن غالبية سلاسل الأدوات المعروفة ببساطة لا تعمل على JDK 17 وما فوق.
على JDK 8 و11 و16، ومع ذلك، يكون وصول الانعكاس أكثر تساهلاً بكثير وتعمل سلاسل الأدوات كما هو متوقع. لا تزال هذه الإصدارات من JDK تمثل جزءًا كبيرًا من عمليات نشر Java الإنتاجية، خاصة في بيئات المؤسسات الأقدم.
2. منفذ RMI أو JMX قابل للوصول شبكيًا من قبل المهاجم
يحتاج التطبيق إلى شيء مثل -Dcom.sun.management.jmxremote.port=9010 مضبوطًا، أو يحتاج إلى تصدير خدمات RMI الخاصة به. نقطة نهاية وكيل OTel تركب على أي نقل RMI قيد الاستخدام بالفعل. إذا لم يكن هناك منفذ RMI مفتوح، فإن نقطة النهاية غير قابلة للوصول.
3. مكتبة متوافقة مع سلسلة الأدوات موجودة على مسار فئات التطبيق
لا يحتوي ملف الوكيل نفسه على أي فئات متوافقة مع سلسلة الأدوات. أكدنا ذلك من خلال فحص قائمة الفئات المستخرجة من ملف وكيل 2.26.0. يحتوي الوكيل على كود أدوات OTel، وفئات API مظللة، وتعريفات semconv وبنية تحتية للإقلاع. لا يتم تضمين أي من مصادر الأدوات الشائعة الاستغلال مثل Commons Collections أو Commons BeanUtils أو فئات Spring Framework داخل الوكيل.
هذا يعني أن سلسلة الأدوات يجب أن تأتي من التطبيق. يجب أن يكون المطور قد أدرج مكتبة تحتوي على أدوات تسلسل قابلة للاستغلال، مما يعني أن قابلية الاستغلال تختلف اعتمادًا على ما يعتمد عليه التطبيق.
قد تبدو الشروط الثلاثة أعلاه مقيدة ولكنها في الواقع شائعة جدًا معًا في بيئات Java المؤسسية الحقيقية. ضع في اعتبارك سيناريو إنتاج نموذجي: خدمة خلفية تعمل على JDK 11 أو JDK 17 (مع علامات --add-opens، الشائعة في عمليات النشر الحاوية ويمكنها إعادة تمكين سلاسل الأدوات جزئيًا)، مُزوَّدة بوكيل OTel للمراقبة، مع تمكين JMX للمراقبة التشغيلية، وشجرة تبعيات غنية بما يكفي لاحتواء فئات متوافقة مع الأدوات.
البصيرة الرئيسية هي أن المطورين يثقون في وكلاء المراقبة كبنية تحتية سلبية. يتوقعون من الوكيل المراقبة، وليس فتح نقاط نهاية جديدة قابلة للوصول شبكيًا. سطح الهجوم الذي تم إنشاؤه هنا غير مرئي من منظور مطور التطبيق. لم يكتبوا أي كود RMI. لم يقوموا بتكوين أي نقطة نهاية RMI. الوكيل فعل ذلك بصمت كنتيجة للأدوات.
لمزيد من البحث حول متطلب سلسلة الأدوات، يوثق مشروع ysoserial (متاح للعموم، ويُشار إليه على نطاق واسع في أبحاث الأمن الأكاديمية والمهنية) عدة سلاسل أدوات لإلغاء تسلسل Java ذات صلة بهذه الفئة من الثغرات. سلاسل مثل CommonsCollections وSpring1 وSpring2 هي أمثلة موثقة جيدًا على كيفية ربط فئات المكتبات الموجودة لتحقيق تنفيذ الكود من خلال إلغاء التسلسل غير الآمن. أي السلاسل المحددة قابلة للتطبيق في بيئة معينة يعتمد كليًا على المكتبات الموجودة على مسار فئات ذلك التطبيق.
قم بالترقية إلى إصدار opentelemetry-javaagent 2.26.1 أو أحدث. هذا هو الإصلاح الكامل الوحيد.
إذا لم تكن الترقية الفورية ممكنة، يمكن تعطيل أدوات RMI بالكامل عن طريق إضافة خاصية النظام التالية إلى علامات بدء تشغيل JVM:
-Dotel.instrumentation.rmi.enabled=false
هذا يعطل الأدوات التي تسجل نقطة النهاية القابلة للاستغلال، مما يزيل سطح الهجوم بالكامل. لن يعمل نشر السياق عبر استدعاءات RMI أثناء تعيين هذه العلامة، ولكن هذا مقبول كتخفيف مؤقت.
بشكل مستقل عن هذا CVE، إذا كان JMX مكشوفًا على منفذ قابل للوصول شبكيًا دون مصادقة، فيجب التعامل معه كخطأ تكوين حرج بغض النظر عن وجود وكيل OTel أم لا.