Apache Airflow 是一个开源平台,用于以编程方式编写、调度和监控工作流。虽然它提供了管理复杂工作流的强大功能,但它也存在安全漏洞。一个值得注意的漏洞 CVE-2024-39877 是 DAG(有向无环图)代码执行漏洞。这允许经过身份验证的 DAG 作者以一种可以在调度程序上下文中执行任意代码的方式制作 doc_md 参数,而根据 Airflow 安全模型,这是被禁止的。
补丁差异
从GitHub上修补漏洞的 pull request 中,我们可以看到 DAG 代码执行漏洞源于对 doc_md 参数的不当处理,这允许攻击者在调度器上下文中注入并执行任意代码。Airflow 的 DAG 中的 doc_md 参数允许包含 Markdown 文档。但是,由于清理不当,由于使用 Jinja2 来呈现此参数的内容,因此可以注入可执行任意 Python 代码的 Jinja2 模板。由于 Airflow 调度器会处理此参数,因此任何注入的代码都将在调度器上下文中运行。通过将 doc_md 参数内的数据视为原始数据,可以修补此漏洞。
测试实验室
1. 我们将在 Docker 上构建实验室。首先,我们需要拉取易受攻击的镜像:
airflow % docker pull apache/airflow:2.4.0
2. 然后,下载 Docker Compose 文件:
airflow % curl -LfO ‘https://airflow.apache.org/docs/apache-airflow/2.4.0/docker-compose.yaml’
3. 创建日志、dags、插件和配置文件夹,以及 .env 文件:
airflow % mkdir -p ./dags ./logs ./plugins ./config && echo -e “AIRFLOW_UID=$(id -u)” > .env
4. 检查创建的目录和文件:
airflow % ls
配置 dags docker-compose.yaml 日志插件
5. 启动Airflow:
airflow % sudo docker compose up airflow-init
6. 现在,运行 Airflow:
airflow % sudo docker compose up
我们发现它在端口 8080 上工作。用户名和密码是 airflow:airflow。
分析
现在,为了重现该漏洞,我们需要创建一个 DAG。
什么是 DAG?
有向无环图 (DAG) 是一种有向边且无环的有限图。在 Apache Airflow 中,DAG 是您要运行的所有任务的集合,以反映其关系和依赖关系的方式组织。
**有向:**图中的每个边都有一个方向,从一个节点(任务)到另一个节点(任务)。
**非循环:**图中没有循环,这意味着您不能从一项任务开始并沿着有向边回到同一项任务。
**图:**节点(任务)和边(任务之间的依赖关系)的集合。
Apache Airflow 中的 DAG
在 Apache Airflow 中,DAG 是用 Python 脚本定义的,它指定了任务之间的关系和依赖关系。以下是一些关键组件:
**任务:**单个工作单元,可以是运行 shell 命令、调用 API 或运行机器学习模型等任何内容。
**依赖关系:**任务之间的关系,指定哪些任务需要先完成,其他任务才能开始。
**调度:**定义 DAG 运行的时间和频率。
DAG 示例
以下 DAG 包含一个 doc_md 参数。此参数允许您使用 Markdown 记录您的 DAG。当您查看 DAG 详细信息时,文档将显示在 Airflow Web 界面中。
`from datetime import datetime``from airflow import DAG``from airflow.operators.empty import EmptyOperator``default_args = {` `'owner': 'airflow',` `'start_date': datetime(2023, 1, 1),` `'retries': 1``}``# Define the DAG``dag = DAG(` `'example_dag_with_doc_md',` `default_args=default_args,` `description='An example DAG with doc_md',` `schedule='@daily',` `doc_md="""` `# Example DAG` ``This is an example DAG that demonstrates the use of the `doc_md` parameter to add documentation.`` `## Description` ``This DAG has two dummy tasks: `start` and `end`.`` `## Tasks` ``- `start`: This is the starting task.`` ``- `end`: This is the ending task.`` `## Dependencies` ``The `end` task depends on the `start` task.`` `"""``)``# Define the tasks``start = EmptyOperator(` `task_id='start',` `dag=dag``)`` ``end = EmptyOperator(` `task_id='end',` `dag=dag``)``# Set the task dependencies``start >> end`
**doc_md:**此参数用于将 Markdown 文档添加到 DAG。doc_md 字符串中的内容以 Markdown 编写,当您查看 DAG 详细信息时,它将呈现在 Airflow 网页界面中。
**EmptyOperator:**这是一个不执行任何操作的简单运算符。它在这里用于创建占位符任务。
现在,让我们尝试一下 DAG
保存 DAG 文件
将上述代码保存为 Python 文件(例如,example_dag_with_doc_md.py)放在 Airflow DAGs 文件夹(在我们的 Docker 设置中为 /opt/airflow/dags/)中。
触发 DAG
转到 Airflow 网页界面并触发名为 example_dag_with_doc_md 的 DAG。
查看文档
在 Airflow 网页界面点击 DAG 查看其详细信息。您将在 Doc 选项卡中看到渲染后的 Markdown 文档。
这里究竟发生了什么?
让我们看一下漏洞代码中的 def get_doc_md(self, doc_md: str | None) -> str | None: 函数,看看它如何从 doc_md 解析 Markdown 内容:
`def get_doc_md(self, doc_md: str | None) -> str | None:` `if doc_md is None:` `return doc_md` `env = self.get_template_env(force_sandboxed=True)` `if not doc_md.endswith(".md"):` `template = jinja2.Template(doc_md)` `else:` `try:` `template = env.get_template(doc_md)` `except jinja2.exceptions.TemplateNotFound:` `return f"""` `# Templating Error!` ``Not able to find the template file: `{doc_md}`.`` `"""` `return template.render()`
get_doc_md 方法旨在处理 doc_md 参数,允许 DAG 作者将 Markdown 文档嵌入其 DAG 中。下面是其工作原理的详细说明:
1.检查doc_md是否为None:
如果 doc_md 为 None,该函数会提前返回。
2.初始化Jinja2环境:
它使用 self.get_template_env(force_sandboxed=True) 初始化启用沙盒的 Jinja2 环境。
3.处理doc_md内容:
如果 doc_md 不以 .md 结尾,它会使用 template = jinja2.Template(doc_md) 直接从 doc_md 字符串创建 Jinja2 模板。此步骤非常危险,因为它允许将 doc_md 中提供的任何字符串视为 Jinja2 模板,而无需进行任何清理。如果攻击者可以操纵此内容,他们就可以轻松地将恶意 Jinja2 表达式甚至任意 Python 代码注入模板。
如果 doc_md 以 .md 结尾,该方法将尝试使用 env.get_template(doc_md) 从环境中加载模板。如果未找到模板文件,它将返回模板错误消息。然而,这部分不如直接创建模板那么重要。
4.渲染模板:
最后一步 template.render() 执行渲染的模板,即注入的代码在这里执行
所以,该漏洞是注入攻击(服务器端模板注入,SSTI)的经典示例。
EXP
让我们看看如何利用此漏洞:
攻击场景 - 详细步骤
1.发送恶意doc_md有效负载:
攻击者通过doc_md参数向Web服务器发送恶意负载。
2. 将有效载荷转发至Airflow:
Web 服务器将此有效负载转发给 Airflow 应用程序,然后该应用程序调用 get_doc_md 方法。
3.调用get_doc_md方法:
该方法检查 doc_md 参数是否为 None 并继续初始化 Jinja2 环境。
4.创建Jinja2模板:
接下来,它会使用doc_md内容创建一个Jinja2模板并渲染该模板。在渲染过程中,doc_md参数中嵌入的恶意代码会被操作系统执行。
5.执行注入的代码:
操作系统执行命令并将输出返回给 Airflow 应用程序。
6.发送回复:
最后,Airflow 将渲染的模板输出发送回 Web 服务器,然后 Web 服务器将包括命令输出在内的响应返回给攻击者。
注入代码示例
为了证明这一点,让我们注入代码来转储可用的类:
`doc_md="""` `{{ ''.__class__.__mro__[1].__subclasses__() }}` `"""`
Jinja2 模板代码中的 {{”.__class__.__mro__[1].__subclasses__()}} 利用 Python 的自省功能列出对象类的所有子类,从而有效地揭示当前 Python 环境中加载的所有类。其工作原理如下:
”.__class__ 检索空字符串的类,即 str。
访问此类上的 .__mro__ 可提供方法解析顺序 (MRO),这是一个包含 str 类本身及其基类(包括对象)的元组。
表达式 .__mro__[1] 从这个元组中选择对象类。
最后,.__subclasses__() 列出了 object 类的所有已知子类,使我们能够枚举运行时可用的类。这可用于识别有用的类(如 os.system),以便在操作系统上执行命令并实现代码执行。
更新 DAG 后,我们注入的表达式得到渲染并转储所有可用的类。
在这里,我们可以看到像 subprocess.Popen 这样的有用类可用于执行命令。利用取决于环境和类的可用性。
结论
在本次分析中,我们发现了 CVE-2024-39877 漏洞,该漏洞允许经过身份验证的 DAG 作者利用 doc_md 参数在调度程序上下文中执行任意代码,从而违反 Airflow 的安全模型。该漏洞源于对 doc_md 参数的不当处理和清理,该参数使用 Jinja2 模板呈现。这一疏忽允许攻击者注入可以执行 Python 代码的恶意 Jinja2 表达式。
存在漏洞的代码中的 get_doc_md 方法会初始化 Jinja2 环境,如果 doc_md 字符串不是以 .md 结尾,则直接根据该字符串创建模板,从而在未经过充分清理的情况下呈现模板。攻击者可以通过注入有效载荷来利用此过程,利用 Python 的自省功能枚举可用类并执行命令,从而入侵系统。
为了缓解这种情况,补丁确保正确处理 doc_md 作为原始数据,从而防止执行任意代码。
https://blog.securelayer7.net/arbitrary-code-execution-in-apache-airflow/
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里