
內容摘要
hooks 存在 .git 裡,而別人用壓縮檔或共享文件夾交給你的 .git 目錄會把 hooks 一起帶過來。post-checkout hook 會在你切換分支的瞬間執行,早於你讀到任何程式碼。
hook 是 Git 在工作流程固定時點執行的程式,而 hooks 就住在 .git 裡。這就是為什麼正常 clone 安全,而共享文件夾不安全:把一個含 .git 文件夾的工作目錄交給別人,你就把「對方一 checkout 分支就執行程式碼」所需的一切都交出去了。
這個攻擊已經在真實世界出現。一位開發者收到看似合理的項目封存檔,被要求切到「NDA 分支」,結果發現一個真實的 post-checkout hook:它從命令與控制端點抓取對應作業系統的執行檔、標記為可執行、執行、再刪除自己。稽核 hooks 與設定大約只要三十秒。
01post-checkout hook 如何變成程式碼執行
post-checkout 會在 checkout 完成後執行。Git 以三個參數呼叫它——先前的 HEAD、新的 HEAD,以及指出是分支或檔案 checkout 的旗標——而 hook 的結束碼不會阻擋操作,所以它會無聲執行。
因為它在轉換時觸發,攻擊者不需要你主動執行指令碼。叫你 git checkout nda 就夠了。接著 hook 擁有你的完整用戶權限:它能讀 ~/.ssh、~/.aws、macOS 鑰匙圈、環境變數,以及機器上任何憑證助手。
在不受信任的倉庫裡,checkout 不是唯讀動作
若工作目錄含有一個不是你建立的
.git文件夾,請把執行任何git指令都當成執行作者的程式碼。這也包含git status,因為其他設定鍵同樣會執行程式。
02既然不會被 clone,hooks 為何還是出現了
一般的 clone 路徑確實安全:git clone <url> 不會複製遠端 hooks,而 clone 後 Git 可能執行的 post-checkout 來自你的樣板目錄,不是那個倉庫。攻擊用三種方式繞過:
- 直接交給你工作目錄。 含
.git的封存檔、Dropbox 文件夾或複製的目錄。這就是回報事件中的情況。 - 透過
core.hooksPath指向被追蹤的 hooks 目錄。 用這種方式管理 hooks 的框架,會讓 hook 指令碼成為一般已提交檔案,安裝相依套件就會默默啟用它們。 filterdriver 搭配.gitattributes。 倉庫提交引用某個 filter 的.gitattributes,而該 filter 的指令住在.git/config,checkout 時就會執行它。
只有第一種不需要安裝任何相依套件,也最容易上當,因為那個文件夾看起來就是普通項目。
03執行 git 前先稽核不受信任的倉庫
從 hooks 目錄與本機設定開始。以下每個指令都已在 Git 2.50.1 上驗證。
# 這個倉庫把 hooks 放在哪裡?
git rev-parse --git-path hooks/post-checkout
# .git/hooks/post-checkout
# 列出所有不是 .sample 的檔案。
ls -la "$(git rev-parse --git-path hooks)"
hooks 目錄裡只有 *.sample 是預設狀態。真實的非 sample 檔案就是會在觸發點執行的程式;執行 Git 前請完整讀過它。
# 任何一項都代表有 hook、filter 或 helper 會執行程式碼。
git config --local --list
git config --get core.hooksPath # 結束碼 1 代表未設定
git config --get core.fsmonitor
git config --get core.sshCommand
請注意 filter.*、core.fsmonitor、core.sshCommand、core.pager、core.editor,以及任何以 ! 開頭的別名。每一項都可能讓一般操作產生執行指令的副作用。
# 檢查 hook 的典型特徵:下載、chmod、執行、刪除。
grep -nE 'curl|wget|chmod|base64 -d|/tmp/|rm -f|exec' .git/hooks/post-checkout
讀 hook 是安全的;危險的是讓 Git 指令觸發它。若 hook 存在而你只需要看被追蹤的檔案,請用編輯器或 tar 打開,而不是用 Git。
04在可疑的 checkout 上安全執行 Git
當你必須在不受信任的倉庫裡執行 Git,請在該次指令停用 hooks,並讓工作階段保持惰性:
# 已驗證:空 hooks 路徑只會為這次指令停用所有 hooks。
git -c core.hooksPath=$(mktemp -d) status
git -c core.hooksPath=$(mktemp -d) checkout nda
# 絕不讓不受信任的倉庫提示輸入憑證。
GIT_TERMINAL_PROMPT=0 git -c core.hooksPath=$(mktemp -d) log --oneline -5
若要做的不只是看一眼,請把被追蹤的檔案複製到新目錄,重新初始化一個倉庫,而不是使用對方提供的 .git。整個攻擊都依賴那個文件夾,丟掉它就移除了攻擊途徑。
從 URL clone,不要從文件夾複製
若同事要分享項目,請向他要 Git URL 或裸 bundle,而不是含
.git的工作目錄。git clone不會執行遠端 hooks;複製文件夾則會。
05如果 hook 已經執行過
請假設機器上的憑證已外洩。先斷網,再盤點 checkout 期間可觸及的內容:SSH 金鑰與其通關密語、雲端 CLI 工作階段、瀏覽器工作階段、macOS 鑰匙圈,以及 shell 環境或 .env 檔裡的任何權杖。請從另一台機器輪替這些憑證,並檢查 Git 代管與雲端稽核紀錄中是否有陌生 IP 或 user agent 的活動。刪掉 hook 並不能撤銷那次執行。
06常見錯誤與修正
| 錯誤 | 為何失效 | 修正 |
|---|---|---|
在別人傳來的文件夾直接跑 git status | hooks 與設定可能執行程式碼 | 先檢查 .git,或使用 core.hooksPath |
因為沒有 post-checkout 就信任倉庫 | 其他 hooks 與設定鍵也會觸發 | 讀完整個 hooks 目錄與本機設定 |
| 假設 clone 永遠安全 | 只有 hooks 不在樹裡時才安全 | 從 URL clone,絕不採用別人給的 .git |
只看 core.hooksPath | filter.* 與 core.fsmonitor 也會執行 | 檢查每個能執行程式的設定鍵 |
| 事後才刪掉 hook | 載荷已經跑過 | 輪替憑證並檢視稽核紀錄 |
真正重要的習慣是:不是你建立的 .git 文件夾就是可執行內容。在你打下 git 之前先看進去,並且優先從 URL 重新 clone,這樣一開始就沒有可疑的東西可看。
建立能在 hook 指令碼裡找出 curl、chmod 與 base64 解碼的規則。
Regex 測試工具常見問題
Git hooks 會從遠端被 clone 下來嗎?
不會。一般的 `git clone` 只複製被追蹤的檔案,不會複製遠端的 `.git/hooks`。即使從 URL clone 一個惡意倉庫,也不會執行攻擊者提交的 hooks,因為 hooks 不屬於被追蹤的樹。
那 post-checkout hook 是怎麼進到我機器的?
有人交給你一個已經含 `.git` 文件夾的工作目錄——放在壓縮檔、共享文件夾或封存檔裡。那個 `.git` 帶著它的 hooks,所以在裡面執行 `git checkout` 就會觸發攻擊者指令碼。
post-checkout hook 會在什麼時候執行?
Git 會在 `git checkout` 之後、`git clone` 完成初次 checkout 之後,以及 `git switch` 之後執行 `post-checkout`。它會收到新舊 HEAD 與一個指出是分支或檔案 checkout 的旗標。
我能在不受信任的倉庫裡執行 git 而不觸發 hooks 嗎?
可以。把 `core.hooksPath` 指向空目錄即可:`git -c core.hooksPath=$(mktemp -d) status`。經本機驗證,這會在不改動倉庫設定的情況下,於該次指令停用所有 hooks。
還有哪些 Git 設定鍵會執行程式碼?
`core.fsmonitor`、`core.sshCommand`、`core.pager`、`core.editor`、搭配已提交 `.gitattributes` 的 `filter.<driver>.*`,以及 `!command` 別名都能執行程式。對於不是自己 clone 的倉庫,務必檢查 `.git/config`。
相關文章
- 如何把 Git 倉庫從 SHA-1 遷移到 SHA-256
遷移也是稽核 hooks 與本機設定的好時機。
- 用 diff 工具比對兩段文字
把可疑的 .git/config 與乾淨版本做比對。
相關工具
- Regex 測試工具
找出 hook 裡的下載、chmod 與解碼呼叫。
- Base64 編碼/解碼
在執行前先解開混淆的載荷。
- Diff 比對工具
找出設定或 hook 指令碼中被注入的行。