
مختبر Docker شامل من البداية إلى النهاية يعيد إنتاج Apache log4j2 #4255 — تجاوز قائمة السماح في FilteredObjectInputStream عبر java.rmi.MarshalledObject (إلغاء تسلسل غير مفلتر → تنفيذ تعليمات برمجية عن بُعد) على Log4j 2.26.1 / JDK 17.
FilteredObjectInputStream عبر java.rmi.MarshalledObjectمختبر مستقل ومعزول بالحاويات يعيد إنتاج مشكلة Apache log4j2 #4255 من البداية إلى النهاية ضد القطع الرسمية Log4j 2.26.1 على JDK 17، مع ضوابط إيجابية، ومرجع مُحصّن، وتخفيف مُتحقق منه.
⚠️ الاستخدام المسؤول
يحتوي هذا المختبر على ثغرة إلغاء تسلسل تعمل بتنفيذ أوامر عن بُعد (RCE) ضد مشكلة Log4j غير مُصححة حاليًا (#4255 مفتوحة /
waiting-for-maintainerوقت كتابة هذا؛ لا يوجد CVE معيّن). يعمل بالكامل داخل حاويات Docker مؤقتة على جهازك الخاص ولا يتصل بأي شيء خارجي سوى Maven Central (لتنزيل ملفات jar الرسمية) — لا يتم الاتصال بأي هدف.
- المُبلّغ الأصلي (U-Sec / Wujie Security) يحجب PoC الخاص به بانتظار إصلاح. هذا إعادة إنتاج مستقلة بُنيت للتحقق الدفاعي وهندسة الكشف.
- لا تشغّل هذا ضد أنظمة لا تملكها أو غير مصرح لك باختبارها صراحةً.
- هذا يوضح ثغرة RCE مشروطة بالتطبيق، وليست ثغرة RCE عامة في Log4j (انظر ).
FilteredObjectInputStream (FOIS) في Log4j هو قائمة سماح لإلغاء التسلسل تعتمد على resolveClass.
تتضمن قائمة السماح الخاصة به java.rmi.MarshalledObject. يخزّن MarshalledObject حمولته كـ
byte[] غير شفاف، وMarshalledObject.get() يلغي تسلسل تلك الحمولة على ObjectInputStream
جديد وغير مُصفّى — لذلك لا تفحص قائمة السماح أبدًا الرسم البياني الداخلي.
يُطلق Log4j هذا بنفسه: Log4jLogEvent$LogEventProxy (الشكل السلكي المتسلسل لـ LogEvent،
منذ 2.8) يحمل رسالة الحدث Message داخل MarshalledObject ويستدعي .get() تلقائيًا
أثناء إلغاء التسلسل (readResolve() ← message()). أي تطبيق يقرأ LogEvent متسلسلًا
عبر FOIS يقوم بالتالي بإلغاء تسلسل غير مُصفّى لبايتات المهاجم — ولأن message()
تبتلع الاستثناء الناتج وتتراجع إلى SimpleMessage، يسجّل المستقبِل حدثًا حميدًا
ويستمر في العمل. الاستغلال صامت.
rel/2.26.1)| # | الموقع | الخلل |
|---|---|---|
| 1 | log4j-api …/util/internal/SerializationUtil.java | REQUIRED_JAVA_CLASSES يحتوي على java.rmi.MarshalledObject |
| 2 | log4j-api …/util/FilteredObjectInputStream.java | يتجاوز فقط resolveClass()؛ حمولة objBytes الخاصة بـ MarshalledObject غير مرئية له |
| 3 | log4j-core …/impl/Log4jLogEvent.java | LogEventProxy.marshalledMessage هو MarshalledObject<Message> |
| 4 | log4j-core …/impl/Log4jLogEvent.java | message() تستدعي marshalledMessage.get() (بدون تصفية) وتبتلع جميع الاستثناءات |
بديل أمين لـ ObjectInputStreamLogEventBridge المُهمَل من log4j-samples: مستقبِل TCP
غير مُصادَق يقرأ LogEvent متسلسلًا واحدًا لكل اتصال عبر FOIS.
يرسل المهاجم كائنًا متسلسلًا واحدًا؛ المرجع هو ملف إثبات يُكتب في دليل
مُثبّت بالربط فقط داخل حاوية المستقبِل، لذا فإن ظهوره يثبت أن الكود نُفّذ داخل
المستقبِل عبر إلغاء التسلسل.
| # | السيناريو | مسار فئات الضحية | jdk.serialFilter | المتوقع |
|---|---|---|---|---|
| S1 | أداة محظورة مُرسلة على المستوى الأعلى | + أداة | لا شيء | رفض — FOIS يفرض قائمة السماح الخاصة به |
| S2 | نفس الأداة ملفوفة في LogEvent | + أداة | لا شيء | rce — تشغيل تلقائي (يتطلب فئة الأداة على الضحية) |
| S3 | CommonsCollections6 خام على المستوى الأعلى | log4j + cc-3.2.1 | لا شيء | رفض — FOIS يحظر CC |
| S4 | CC6 مُدمجة داخل MarshalledObject | log4j + cc-3.2.1 فقط | لا شيء | rce — لا توجد فئة مهاجم على الضحية |
| S5 | حمولة S4 | log4j + cc-3.2.1 | !java.rmi.MarshalledObject | رفض — تخفيف |
| S6 | حمولة S4 | log4j + cc-3.2.1 | maxdepth=5;maxbytes=1000000 | صامت — التدفق الداخلي يرث الفلتر؛ CC6 عميقة جدًا |
| S7 | حمولة S4 | log4j + cc-3.2.2 | لا شيء | صامت — 3.2.2 يعطّل إلغاء تسلسل الدوال غير الآمنة |
rce = تم تنفيذ الكود. reject = FOIS رمى استثناءً على التدفق الخارجي. silent = تمت معالجة الكائن الخارجي،
لم يتم تنفيذ أي كود (محظور في العمق، أو إصدار الأداة آمن).
./run.sh # أو: make run
المتطلبات: Docker فقط (يتم سحب JDK كـ eclipse-temurin:17-jdk). يقوم السكربت بتنزيل
ملفات jar الرسمية ويتحقق منها مقابل SHA-1 في Maven Central قبل الاستخدام. ثبّت إصدار Log4j مختلفًا
في النطاق الضعيف باستخدام LOG4J_VERSION=2.20.0 ./run.sh.
LogEvent متسلسل إلى
مستقبِل قائم على FOIS. لا يمكن تشغيله عن طريق تسجيل سلسلة نصية (على عكس Log4Shell) — بل
يتطلب وصول بايتات متسلسلة خام إلى جسر المقبس.LogEvent ← writeReplace()/writeObject() في Log4j يلف Message
في MarshalledObject ← الأداة تختبئ داخل byte[] غير الشفاف ← على المستقبِل،
readResolve() ← message() ← MarshalledObject.get() يفتح تدفقًا غير مُصفّى جديدًا ←
أداة ← Runtime.exec. src/attacker/Attacker.java (poc2) يدمج رسمًا بيانيًا نقيًا من CommonsCollections6
في objBytes الخاص بـ MarshalledObject، لذا لا توجد حاجة لفئة مهاجم على الضحية.-Djdk.serialFilter='!java.rmi.MarshalledObject' على JVM المستقبِل (S5).
تحذير: هذا يحظر أيضًا كائنات LogEventProxy المتسلسلة الشرعية (فهي تستخدم
MarshalledObject أيضًا) — إنه فعّال لكنه ليس شفافًا لنقل السجلات المتسلسلة.maxdepth/maxbytes العامة. الفلتر على مستوى العملية ينتشر إلى
تدفق MarshalledObject الداخلي ويبدأ العمق من جديد هناك، لذا maxdepth=5 يحظر CC6 (S6)
لكن أداة ضحلة ستمر. يعتمد على عمق السلسلة، وليس حدًا فاصلًا.MarshalledObject من قائمة السماح ونقل
الرسالة المُغلّفة إلى writeWrappedObject/readWrappedObject المُصفّاة في Log4j.LogEvent متسلسلًا غير مُصادَق قائمًا على FOIS ولديه إصدار أداة قابل للاستخدام على
مسار الفئات الخاص به. عمليات نشر Log4j العادية لا تشغّل مثل هذا المستقبِل.net.server.TcpSocketServer
نُقل خارج log4j-core في 2017؛ وغائب من 2.9.0 فصاعدًا). المستقبلات الحديثة هي
كود تطبيق/عينة، وهذا ما يُمثّله هذا المختبر.uid=0 في المختبر هو جذر الحاوية — لا يوجد هروب من Docker؛ RCE يعمل كـ
عملية المستقبِل.MarshalledObject يتغلب على فلتر resolveClass) هي فن سابق معروف؛
انظر نقاش Apache #4168
("تقوية إلغاء التسلسل في Log4j 2.x"). التشغيل التلقائي الخاص بـ Log4j هو مساهمة #4255.run.sh مشغّل محمول (مثبّت بالمجموع الاختباري، مرجع مُحصّن)
Makefile make build | run | clean
src/victim/Receiver.java مستقبِل سجلات FOIS (بديل ObjectInputStreamLogEventBridge)
src/attacker/Attacker.java باني الحمولة: ضوابط، PoC-1، PoC-2 (CC6 + دمج البايتات)
src/attacker/EvilMessage.java أداة PoC-1 ذاتية الاحتواء
docs/RESULTS.md مصفوفة الأدلة + التحليل
الثغرة أُبلغت بواسطة U-Sec (Wujie Security) في Apache log4j2 #4255. هذا المستودع هو مختبر إعادة إنتاج/تحقق مستقل للبحث الدفاعي وهندسة الكشف.