一個看似無害的回車符(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 翻譯 | 蘇宓
出品 | CSDN(ID:CSDNnews)
如果你曾用過老式的機械打字機,就會知道:每打完一行,必須通過某種物理操作將打字頭移動回行首。有些打字機通過杠桿完成這個動作,后來產出的一些型號則使用了按鈕。
這一動作被稱為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 更新中,我還修復了其他幾個不同嚴重程度的漏洞。
2025 全球產品經理大會
8月15–16日·北京威斯汀酒店
互聯網大廠&AI 創業公司產品人齊聚
12 大專題,趨勢洞察 × 實戰拆解
掃碼領取大會 PPT,搶占 AI 產品新紅利
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.