长亭百川云 - 文章详情

CVE-2024-39877:Apache Airflow 任意代码执行

Ots安全

40

2024-08-06

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/

感谢您抽出

.

.

来阅读本文

点它,分享点赞在看都在这里

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2