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

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. 创建 logs、dags、plugins 和 config 文件夹以及 .env 文件:

airflow % mkdir -p ./dags ./logs ./plugins ./config && echo -e “AIRFLOW_UID=$(id -u)” > .env

4.检查创建的目录和文件:

airflow % ls

配置 dags docker-compose.yaml 日志插件

\5. 启动气流:

airflow % sudo docker compose up airflow-init

6.现在,运行 Airflow:

气流 %s udo 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)的经典示例。

开发

让我们看看如何利用此漏洞:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

攻击场景

详细步骤

1.发送恶意doc_md有效负载:

攻击者通过doc_md参数向Web服务器发送恶意负载。

2. 将有效载荷转发至气流:

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 作为原始数据,从而防止执行任意代码。

  • 12
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

H_kiwi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值