Skip to content

[Security] Unauthenticated Java deserialization RCE #67

Description

@AnalogyC0de

Unauthenticated Java deserialization RCE on the unit/node server

CWE: CWE-502 (Deserialization of Untrusted Data)
Affected versions: <=V20260507

Summary

The esProc unit/node server deserializes objects straight from a raw TCP socket using ObjectInputStream.readUnshared() with no ObjectInputFilter or any other class restriction. The server listens on TCP port 8281 by default. A network attacker who can reach the port sends a crafted serialized object that, combined with a gadget present on the classpath, yields remote code execution. No authentication or handshake is required before the read.

Vulnerability chain

Stage Component Location
TCP source Unit/node server accepts connections and builds a SocketData for each parallel/UnitWorker.java:45-48
Stream build ObjectInputStream wraps the raw socket stream, no filter set parallel/SocketData.java:65-68
Read (before auth) UnitWorker.run calls socketData.read() at the top of its loop parallel/UnitWorker.java:57
Deser sink SocketData.readois.readUnshared() on the unfiltered stream parallel/SocketData.java:123
Default port Unit context defaults the listener to port 8281 parallel/UnitContext.java:180

For each accepted connection, UnitWorker.run() immediately calls socketData.read() (line 57) before any request is interpreted. That read reaches SocketData.read()ois.readUnshared() (line 123), where ois is a plain ObjectInputStream constructed over the socket's input stream (lines 65-68). Because there is no ObjectInputFilter, any serializable type resolvable on the server classpath is instantiated during deserialization. The IP-whitelist gate (errorCheck) only affects later DFX task execution (line 69); it does not gate the deserialization itself, and it does not authenticate the connection.

Key code

The deserialization sink, with the unfiltered ObjectInputStream:

// src/main/java/com/scudata/parallel/SocketData.java:65-68  (server-side stream build)
public void holdCommunicateStreamServer() throws IOException {
    InputStream is = socket.getInputStream();
    BufferedInputStream bis = new BufferedInputStream(is);
    ois = new ObjectInputStream(bis);          // no ObjectInputFilter
    ...
}

// SocketData.java:121-125  (read sink)
public Object read() throws IOException, ClassNotFoundException {
    Object obj = ois.readUnshared();           // :123 — attacker object
    return obj;
}

The read happens before the request is parsed or the IP check is applied:

// src/main/java/com/scudata/parallel/UnitWorker.java:53-69
public void run() {
    try {
        Response response = null;
        while (!stop) {
            Object obj = socketData.read();    // :57 — deser before any gate

            if (obj == null || !(obj instanceof Request)) {
                break;
            }
            Request req = (Request) obj;
            switch (req.getActionType()) {
            case Request.TYPE_DFX:
                ...
                if (errorCheck) {              // :69 — IP-whitelist only affects task exec

Default listener port:

// src/main/java/com/scudata/parallel/UnitContext.java:177-180
public UnitContext(String specifyHost, int specifyPort) throws Exception {
    UnitConfig uc = getUnitConfig();
    String host = null;
    int port = 8281;                           // :180 — default port

Proof of Concept

No gadget bytes are included. This is the network-level trigger showing how the sink is reached unauthenticated.

The unit/node server accepts a plain TCP connection on the configured port (default 8281). Upon connect, UnitWorker immediately calls SocketData.read(), which performs ObjectInputStream.readUnshared() over the socket stream. An attacker opens a TCP connection to the port and writes a serialized Java object stream; the object is deserialized with no class filtering before any authentication or application-level request handling.


## Impact

A network attacker who can reach the unit/node server port (default 8281) achieves remote code execution as the JVM user running esProc, with no authentication and no prior handshake. Exploitability depends on a usable gadget chain being present on the classpath; even absent a gadget, untrusted deserialization can cause crashes / denial of service through crafted object graphs. The default bind host is taken from the configured node IP (commonly a reachable interface rather than loopback), so exposure depends on how the deployment is configured.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions