一个回车符搞崩Git,甚至能触发远程代码执行?

图片

一个看似无害的回车符(Carriage Return),竟然能让 Git 的子模块克隆逻辑彻底“失控”,甚至引发远程代码执行(RCE)!近日,研究人员 David Leadbeater 披露了一个严重漏洞(CVE-2025-48384),攻击者可以通过精心构造的 .gitmodules 文件,在类 Unix 系统上实现任意文件写入,最终控制用户系统。这一漏洞利用的是 Git 配置解析中对 \r 字符处理的不一致性,看似微小的逻辑差异,却构成了实质性安全威胁。

该研究者表示:“在类 Unix 平台上,如果你对不可信的仓库执行了 git clone --recursive 操作,极有可能会导致远程代码执行(RCE)。请尽快更新 Git 及其他嵌入 Git 的软件(包括 GitHub Desktop)到修复版本。”

原文链接:https://dgl.cx/2025/07/git-clone-submodule-cve-2025-48384#_

作者 | David Leadbeater   翻译 | 苏宓

出品 | 优快云(ID:优快云news)

如果你曾用过老式的机械打字机,就会知道:每打完一行,必须通过某种物理操作将打字头移动回行首。有些打字机通过杠杆完成这个动作,后来产出的一些型号则使用了按钮。

这一动作被称为 Carriage Return(回车),它与换行(Line Feed)是两个独立的动作,因此在字符集中也有各自的表示。

比如,在 ASCII 中,Carriage Return 表示为 “␍”,编号为 13。而你在现代键盘的“Enter”或“Return”键上常见的“↵”图标,实际上就是“回车”和“换行”两个动作的结合,其中 Line Feed 表示为“␊”。

带回车杆的 Olympia SM9 打字机

这种做法可以追溯到 1901 年 Murray 编码时代,如今我们仍然要处理它留下的“历史遗产”。Unix 系统曾试图简化这件事,仅用 LF(在 C 字符串中是 \n)来分隔行,而 Windows 以及一些互联网协议则使用 CR+LF(即 \r\n)。

那为什么要在这里说这些?

Git 使用一种简单的 .ini 风格的配置文件格式,大致长这样:

[section]    key = value

如果这种格式仅用于用户本地的配置文件,倒也无所谓格式的问题。但问题在于,这种配置格式也被用于 .gitmodules 文件,而 .gitmodules 是一个随仓库一同提交、用于追踪子模块的文件。

Git 的配置文件解析器支持 DOS 风格的换行符,它会在读取时去掉行尾的 \r。下面是 Git 源码中 config.c 文件的 get_next_char 函数:

static int get_next_char(struct config_source *cs){    int c = cs->do_fgetc(cs);    if (c == '\r') {        // 处理类 DOS 系统        c = cs->do_fgetc(cs);        if (c != '\n') {            if (c != EOF)                cs->do_ungetc(c, cs);            c = '\r';        }    }

这里的 cs->do_fgetc 本质上等价于标准 C 函数 fgetc(),即便你不了解 Git 的内部函数,也能看懂这段逻辑:

如果读取到一个 \r,就接着看看下一个字符是不是 \n。如果是,就“吃掉”回车只保留换行;如果不是,就把读取到的下一个字符“退回去”,然后返回 \r。

重点在于:这是按行处理的。

所以如果某一行恰好以 \r 结尾,不管整个文件的格式如何,这个 \r 都会被干掉。

除了读取配置,Git 也会写入配置文件,比如用户执行 git config 设置值时,它会用下面这段代码把 key = value 写入文件:

static ssize_t write_pair(int fd, const char *key, const char *value, [...]{       [...]       /*         * Check to see if the value needs to be surrounded with a dq pair.         * Note that problematic characters are always backslash-quoted; this         * check is about not losing leading or trailing SP and strings that         * follow beginning-of-comment characters (i.e. ';' and '#') by the         * configuration parser.         */        if (value[0] == ' ')                quote = "\"";        for (i = 0; value[i]; i++)                if (value[i] == ';' || value[i] == '#')                        quote = "\"";        if (i && value[i - 1] == ' ')                quote = "\"";        strbuf_addf(&sb, "\t%s = %s", key + store->baselen + 1, quote);

真正的 bug 就出在 get_next_char 与上面写入逻辑的配合问题上。

Git 在解析配置文件时支持用双引号包住字符串,比如 "foo" 这样的写法没问题。但当 Git 把配置项写回文件时,只有在字符串里包含空格、; 或 # 这些特定字符时,它才会自动加上引号。如果这些字符不出现,就不会加引号。

这就导致了一个漏洞:当 Git 之后再次读取这个配置文件时,如果原本的字符串结尾有一个 \r(回车符),就可能被悄悄丢掉——因为 Git 的解析器会默认把结尾的 \r 干掉

举个例子,如果有配置项 key = "foo^M"(其中 ^M 是回车字符),写回时就变成了 key = foo^M(没有引号,^M 仍在)。但当下次读取这个配置时,Git 会把末尾的 \r 忽略掉,结果值就变成了 foo,跟原来的不一样了。

而我前面也提到过,.gitmodules 文件是不可信的外部输入,也就是说,攻击者完全可以构造这样的内容来“骗过” Git 的配置解析器

在类 Unix 系统(不是 Windows)上,文件名中是允许包含控制字符的。所以在 .gitmodules 文件中,如果写了这样一段:

[submodule "foo"]  path = "foo^M"

Git 会尝试将子模块检出到名为 foo^M 的目录中。

然而,Git 在将这个路径写入 .git/modules/foo/config 时,会写成:

[core]  workdir = ../../../foo^M

Git 会先对 .gitmodules 文件中读取到的路径做一次校验,但这个路径是不可信的外部输入。问题在于,Git 在读取配置文件时会自动去掉路径结尾的 \r(回车符),这就导致了一个非常微妙的问题:验证时看到的是一个路径,实际使用时却变成了另一个路径

所以,最终的结果是:在执行 submodule clone 操作时,Git 可能读取的是 path = foo^M 这样的路径,但在实际写入配置时却变成了 path = foo,中间的 ^M(回车符)被悄悄删掉了。

就是这么一个小细节,就足以让 Git“认错路径”。也就是说,Git 可能会把子模块的内容写到一个完全不同的目录中。

这个利用思路和之前的 CVE-2024-32002 很相似。那次是通过子模块路径的大小写敏感性问题来迷惑 Git。讽刺的是,上一个漏洞只在大小写不敏感的文件系统(比如 Windows)上有效,而这次漏洞则要求文件系统允许文件名中包含控制字符。因此,Windows 对这个新漏洞基本免疫,而 macOS 两个都中招。

目前一个手动的缓解办法是:在命令行使用 Git 时,不要直接加上 --recursive 参数执行 git clone

而是先普通 clone 一下,然后手动检查 .gitmodules 文件内容是否安全,最后再执行 git submodule init 初始化子模块。

但问题是:GitHub Desktop 默认开启了递归 clone(带 --recursive),所以如果你是用 GitHub Desktop 来克隆仓库,就可能在毫无察觉的情况下触发这个漏洞。

这个漏洞的修复其实出奇地简单:只需要确保在 write_pair 函数中,如果写入的字符串中包含回车字符(Carriage Return, \r),就要对它加上引号。(严格来说,只需要在结尾有 \r 的情况下加引号,但为了安全起见,统一加引号更可靠。)

修复代码如下:

for (i = 0; value[i]; i++)-		if (value[i] == ';' || value[i] == '#')+		if (value[i] == ';' || value[i] == '#' || value[i] == '\r') 			quote = "\"";

这种“写错路径”的原语(confused write primitive)可以被利用,将子模块中的恶意文件写入文件系统的几乎任意位置,实现任意文件写入。由于已经绕过了路径校验,它甚至可以跟随符号链接(symlink)写到仓库之外的路径。

最直接的攻击方式是:将文件写入 .git 目录中,并创建一个 hook 脚本。这样,当 Git 运行这个钩子时,就会触发攻击者控制的代码执行。当然,也可以用来覆盖 .git/config 等关键文件。

目前我还没有公开 PoC(概念验证代码),但这基本上是对 CVE-2024-32002 利用代码的一个小改动而已。而且,在修复此漏洞的 commit 中就有一个测试用例,也已经给出了相当明确的线索。

这也不是 Carriage Return 第一次给 Git 带来麻烦了。今年 1 月,RyotaK 就发现 Git 的凭证辅助工具协议(credential helper protocol)也可能被 \r 字符所欺骗。

同样地,配置解析逻辑的问题也不是第一次出现。2023 年,André Baptista 和 Vítor Pinho 就发现了一个与此相关的逻辑错误。

我认为这类问题特别有意思的一点是:它并不是因为 Git 是用 C 写的才出问题。这类逻辑漏洞在几乎所有语言中都可能出现。共性在于:这些漏洞往往出现在组件之间的进程通信逻辑中(比如 Git 与外部进程之间,或者 Git 自身不同组件之间)。

你可以把这个问题类比为 HTTP 中的 CRLF 注入,甚至是 SMTP 投毒(smuggling)。长期以来,互联网界一直信奉 Postel 原则

“在发送时要保守,在接收时要宽容。”

但现在看来,这条原则可能已经不再适用。关于这一点,《RFC 9413》(https://www.rfc-editor.org/rfc/rfc9413.html)中有更详细的讨论,远超我在这里能讲的内容。

这个漏洞是我在一次 Git 审计中发现的。在今天发布的 Git 更新中,我还修复了其他几个不同严重程度的漏洞。

推荐阅读:

写代码顺手埋了个「彩蛋」!苹果工程师亲述:我差点被“炒”了

“等到Linux 6.17就「分手」!”Linus再被Bcachefs惹怒:公开要求为新特性“开后门”?

曝印度工程师一人兼4份全职,还拿下年薪20万美元Offer:请病假的时候,竟在GitHub上给别家写代码?

2025 全球产品经理大会

8月15–16日·北京威斯汀酒店

互联网大厂&AI 创业公司产品人齐聚

12 大专题,趋势洞察 × 实战拆解

扫码领取大会 PPT,抢占 AI 产品新红利

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

优快云资讯

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

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

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

打赏作者

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

抵扣说明:

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

余额充值