FlawOpen

flawopen.com/Teardowns/cve-2024-32002-git-submodule-rce

● CVE-2024-32002 · CVSS 9.0 · 严重
安全研究 · FlawOpen

CVE-2024-32002: Git 子模块符号链接远程代码执行漏洞 (RCE)

CVE-2024-32002 使特制 Git 仓库能够在递归克隆期间通过大小写冲突的符号链接写入 .git,从而可能在用户检查前运行 hook。Git 上游修复会在克隆前后检查子模块目标目录,并在内容异常时中止;请升级到已修复的 Git 版本。

💡 通俗易懂的原理解析 (ELI5)

想象一个仓库的标牌把 A 和 a 当成同一扇门。如果子模块路径经过一块指向档案室(.git)的标牌,本应放进某个货架的文件就可能进入 Git 的控制记录。Git 的修复会在克隆前后检查目标目录,发现意外内容便停止。

核心概念与专有名词

Git 子模块 (Submodule)
作为子目录嵌入到父仓库中的独立 Git 仓库。
符号链接 (Symlink)
指向文件或目录路径的文件系统重定向对象。
大小写冲突 (Case Collision)
NTFS 和 APFS 等不区分大小写的文件系统中,仅大小写不同的路径映射至同一底层目录。
Git 钩子 (Git Hooks)
位于 .git/hooks/ 目录下的可执行脚本,在 post-checkout 等动作完成时自动触发运行。

框架映射

根本原因剖析 (Root Cause)

在支持符号链接且不区分大小写的文件系统上,特制仓库可以让子模块路径在大小写折叠后与某个符号链接冲突。递归克隆时,Git 可能用该链接替换为克隆创建的目录,并把文件写到子模块工作树之外的 .git 中。写入其中的 hook 可能在用户检查 checkout 前运行。上游补丁会拒绝克隆到已存在且非空的子模块目标目录,并在克隆后拒绝包含 .git 元数据以外内容的目标目录。

攻击执行流程分解

步骤 1

递归克隆处理仓库控制的元数据

用户在支持符号链接且不区分大小写的文件系统上递归克隆不可信仓库。仓库提供的子模块路径和跟踪条目在大小写折叠后发生冲突。

步骤 2

路径冲突越过信任边界

Git 可能通过符号链接解析冲突的子模块路径,而不是将其放入预期的子模块工作树。受影响的边界是递归克隆子模块时 Git 对文件系统路径的处理。

步骤 3

文件写入 Git 元数据

被重定向的写入可能在仓库的 .git 区域放置 hook。Git 项目公告确认 hook 可以写入此处,并在克隆仍进行时执行。

步骤 4

克隆可能在检查前执行代码

Git 会在克隆过程中运行 hook,早于用户检查检出的文件。已修复版本会检查目标状态,并在发现意外文件时中止。

源代码对比:漏洞与安全实现

这是 Git 上游 C 语言改动中 clone_submodule() 的代码片段,并非可独立使用的完整函数。修复前,所示克隆路径没有相应的目标内容检查。修复会拒绝已存在且非空的 checkout 目录,并在克隆后确认目录中只有 .git 元数据;辅助函数负责检查目录项。完整实现见上游提交。

存在漏洞的实现
if (!file_exists(sm_gitdir)) {
    if (safe_create_leading_directories_const(sm_gitdir) < 0)
        die(_("could not create directory '%s'"), sm_gitdir);

    if (run_command(&cp))
        die(_("clone of '%s' into submodule path '%s' failed"),
            clone_data->url, clone_data_path);
}
加固后的安全修复
struct stat st;

if (!file_exists(sm_gitdir)) {
    if (clone_data->require_init && !stat(clone_data_path, &st) &&
        !is_empty_dir(clone_data_path))
        die(_("directory not empty: '%s'"), clone_data_path);

    if (safe_create_leading_directories_const(sm_gitdir) < 0)
        die(_("could not create directory '%s'"), sm_gitdir);

    if (run_command(&cp))
        die(_("clone of '%s' into submodule path '%s' failed"),
            clone_data->url, clone_data_path);

    if (clone_data->require_init && !stat(clone_data_path, &st) &&
        !dir_contains_only_dotgit(clone_data_path)) {
        char *dot_git = xstrfmt("%s/.git", clone_data_path);
        unlink(dot_git);
        free(dot_git);
        die(_("directory not empty: '%s'"), clone_data_path);
    }
}

工程与系统安全加固清单

参考来源