Go 部落格
Go 語言中的命令 PATH 安全性
今天的 Go 安全性更新 修復了一個涉及在不受信任的目錄中進行 PATH 查找的問題,該問題可能導致在執行 go
get
命令期間發生遠端程式碼執行。我們預計大家會對這究竟意味著什麼,以及他們自己的程式中是否可能存在問題產生疑問。這篇文章詳細說明了這個錯誤、我們已套用的修復程式、如何判斷您自己的程式是否容易受到類似問題的影響,以及如果它們容易受到影響,您可以採取哪些措施。
Go 命令與遠端程式碼執行
go
命令的設計目標之一是,大多數命令(包括 go
build
、go
doc
、go
get
、go
install
和 go
list
)都不會執行從網際網路下載的任意程式碼。有一些明顯的例外:顯然 go
run
、go
test
和 go
generate
會 執行任意程式碼,那是它們的工作。但其他的命令不能這樣做,原因有很多,包括可重現的建構和安全性。因此,當 go
get
可以被誘騙執行任意程式碼時,我們認為這是一個安全漏洞。
如果 go
get
不能執行任意程式碼,那麼不幸的是,這意味著它呼叫的所有程式(例如編譯器和版本控制系統)也都在安全邊界內。例如,我們過去曾遇到過這樣的情況:巧妙地利用編譯器的隱晦功能或版本控制系統中的遠端程式碼執行漏洞,就會變成 Go 中的遠端程式碼執行漏洞。(關於這一點,Go 1.16 旨在通過引入 GOVCS 設定來改善這種情況,該設定允許精確配置允許使用哪些版本控制系統以及何時允許使用。)
然而,今天的漏洞完全是我們的錯,而不是 gcc
或 git
的漏洞或隱晦功能。這個漏洞涉及 Go 和其他程式如何找到其他可執行檔,因此在我們深入了解細節之前,需要花一點時間來了解這一點。
命令、PATH 和 Go
所有作業系統都有一個可執行檔路徑的概念(Unix 上的 $PATH
,Windows 上的 %PATH%
;為了簡單起見,我們將只使用術語 PATH),它是一個目錄列表。當您在 Shell 提示符中輸入命令時,Shell 會依次在列出的每個目錄中查找名稱與您輸入的名稱匹配的可執行檔。它會執行找到的第一個可執行檔,或者會顯示一條類似“找不到命令”的消息。
在 Unix 上,這個概念最早出現在第七版 Unix 的 Bourne shell(1979 年)中。手冊中解釋道
Shell 參數
$PATH
定義了包含命令的目錄的搜索路徑。每個備用目錄名稱用冒號 (:
) 分隔。預設路徑是:/bin:/usr/bin
。如果命令名稱包含 /,則不會使用搜索路徑。否則,將在路徑中的每個目錄中搜索可執行檔。
請注意預設值:當前目錄(此處用空字串表示,但我們稱之為“點”)列在 /bin
和 /usr/bin
之前。MS-DOS 然後是 Windows 選擇將該行為硬編碼:在這些系統上,始終會先自動搜索點,然後再考慮 %PATH%
中列出的任何目錄。
正如 Grampp 和 Morris 在他們的經典論文“UNIX 作業系統安全性”(1984 年)中指出的那樣,將點放在 PATH 中系統目錄之前意味著,如果您 cd
到一個目錄並運行 ls
,您可能會從該目錄中獲得一個惡意副本,而不是系統工具。如果您能誘騙系統管理員以 root
身分登錄時在您的主目錄中運行 ls
,那麼您就可以運行任何您想要的程式碼。由於這個問題和其他類似問題,基本上所有現代 Unix 發行版都將新用戶的預設 PATH 設定為排除點。但 Windows 系統仍然會首先搜索點,無論 PATH 如何設定。
例如,當您輸入命令
go version
在典型配置的 Unix 上,Shell 會從 PATH 中的系統目錄運行 go
可執行檔。但是當您在 Windows 上輸入該命令時,cmd.exe
會首先檢查點。如果 .\go.exe
(或 .\go.bat
或許多其他選項)存在,cmd.exe
會運行該可執行檔,而不是 PATH 中的可執行檔。
對於 Go 來說,PATH 搜索由 exec.LookPath
處理,該函式由 exec.Command
自動呼叫。為了更好地適應主機系統,Go 的 exec.LookPath
在 Unix 上實現 Unix 規則,在 Windows 上實現 Windows 規則。例如,此命令
out, err := exec.Command("go", "version").CombinedOutput()
的行為與在作業系統 Shell 中輸入 go
version
相同。在 Windows 上,當 .\go.exe
存在時,它會運行該檔案。
(值得注意的是,Windows PowerShell 更改了這種行為,放棄了對點的隱式搜索,但 cmd.exe
和 Windows C 函式庫 SearchPath 函式
繼續保持原來的行為。Go 繼續匹配 cmd.exe
。)
漏洞
當 go
get
下載並建構包含 import
"C"
的套件時,它會運行一個名為 cgo
的程式來準備與相關 C 程式碼等效的 Go 程式碼。go
命令在包含套件原始碼的目錄中運行 cgo
。一旦 cgo
生成了 Go 輸出檔案,go
命令本身就會呼叫 Go 編譯器處理生成的 Go 檔案,並呼叫主機 C 編譯器(gcc
或 clang
)來建構套件中包含的任何 C 原始碼。所有這些都運行良好。但是 go
命令在哪裡找到主機 C 編譯器呢?當然是在 PATH 中查找。幸運的是,雖然它在套件原始碼目錄中運行 C 編譯器,但它是在呼叫 go
命令的原始目錄中進行 PATH 查找的
cmd := exec.Command("gcc", "file.c")
cmd.Dir = "badpkg"
cmd.Run()
因此,即使 Windows 系統上存在 badpkg\gcc.exe
,這段程式碼片段也不會找到它。在 exec.Command
中發生的查找不知道 badpkg
目錄。
go
命令使用類似的程式碼來呼叫 cgo
,在這種情況下甚至沒有路徑查找,因為 cgo
總是來自 GOROOT
cmd := exec.Command(GOROOT+"/pkg/tool/"+GOOS_GOARCH+"/cgo", "file.go")
cmd.Dir = "badpkg"
cmd.Run()
這甚至比前一段程式碼片段更安全:沒有機會運行任何可能存在的壞 cgo.exe
。
但事實證明,cgo 本身也會呼叫主機 C 編譯器,處理它建立的一些臨時檔案,這意味著它本身會執行以下程式碼
// running in cgo in badpkg dir
cmd := exec.Command("gcc", "tmpfile.c")
cmd.Run()
現在,因為 cgo 本身是在 badpkg
中運行的,而不是在運行 go
命令的目錄中運行的,所以如果 badpkg\gcc.exe
檔案存在,它會運行該檔案,而不是找到系統 gcc
。
因此,攻擊者可以建立一個使用 cgo 並包含 gcc.exe
的惡意套件,然後任何運行 go
get
以下載並建構攻擊者套件的 Windows 用戶都會優先運行攻擊者提供的 gcc.exe
,而不是系統路徑中的任何 gcc
。
Unix 系統首先避免了這個問題,因為點通常不在 PATH 中,其次是因為模組解包不會對其寫入的檔案設定執行權限。但是,如果 Unix 用戶的 PATH 中點位於系統目錄之前,並且正在使用 GOPATH 模式,那麼他們將與 Windows 用戶一樣容易受到攻擊。(如果您屬於這種情況,那麼今天是從您的路徑中移除點並開始使用 Go 模組的好日子。)
修復程式
go
get
命令下載並運行惡意的 gcc.exe
顯然是不可接受的。但是導致這種情況發生的真正錯誤是什麼?那麼修復程式是什麼?
一個可能的答案是,錯誤在於 cgo
在不受信任的原始碼目錄中搜索主機 C 編譯器,而不是在呼叫 go
命令的目錄中搜索。如果這是錯誤,那麼修復程式就是更改 go
命令,將主機 C 編譯器的完整路徑傳遞給 cgo
,這樣 cgo
就不需要在不受信任的目錄中進行 PATH 查找。
另一個可能的答案是,錯誤在於在 PATH 查找期間查找點,無論是在 Windows 上自動發生,還是由於 Unix 系統上的顯式 PATH 項目。用戶可能希望在點中查找他們在控制台或 Shell 視窗中輸入的命令,但他們不太可能也希望在那裡查找輸入命令的子進程的子進程。如果這是錯誤,那麼修復程式就是更改 cgo
命令,使其在 PATH 查找期間不查找點。
我們認為這兩個都是錯誤,所以我們套用了兩個修復程式。go
命令現在將主機 C 編譯器的完整路徑傳遞給 cgo
。除此之外,cgo
、go
以及 Go 發行版中的所有其他命令現在都使用 os/exec
套件的一個變體,如果它之前使用過點中的可執行檔,則會報告錯誤。套件 go/build
和 go/import
對其呼叫 go
命令和其他工具使用相同的策略。這應該可以杜絕任何可能潛伏的類似安全問題。
為了謹慎起見,我們也在 goimports
和 gopls
等指令,以及 golang.org/x/tools/go/analysis
和 golang.org/x/tools/go/packages
等函式庫中做了類似的修正,這些函式庫會將 go
指令作為子程序調用。如果您在不受信任的目錄中執行這些程式(例如,如果您 git
checkout
不受信任的儲存庫並 cd
進入它們,然後執行像這樣的程式,並且您使用的是 Windows 或在 PATH 中包含點號的 Unix 系統),那麼您也應該更新這些指令的副本。如果您的電腦上唯一不受信任的目錄是由 go
get
管理的模組快取,那麼您只需要新的 Go 版本即可。
更新到新的 Go 版本後,您可以使用以下指令更新到最新的 gopls
:
GO111MODULE=on \
go get golang.org/x/tools/gopls@v0.6.4
您可以使用以下指令更新到最新的 goimports
或其他工具:
GO111MODULE=on \
go get golang.org/x/tools/cmd/goimports@v0.1.0
即使在作者更新之前,您也可以通過在 go
get
期間明確升級依賴項來更新依賴 golang.org/x/tools/go/packages
的程式:
GO111MODULE=on \
go get example.com/cmd/thecmd golang.org/x/tools@v0.1.0
對於使用 go/build
的程式,您只需使用更新的 Go 版本重新編譯它們即可。
同樣,只有當您是 Windows 使用者或 PATH 中包含點號的 Unix 使用者,並且您在可能包含惡意程式的不可信來源目錄中執行這些程式時,才需要更新這些其他程式。
您自己的程式是否受到影響?
如果您在自己的程式中使用 exec.LookPath
或 exec.Command
,則只有在您(或您的使用者)在包含不可信內容的目錄中執行您的程式時才需要擔心。如果是這樣,則可以使用點號中的可執行檔而不是系統目錄中的可執行檔來啟動子程序。(同樣,在 Windows 上始終會使用點號中的可執行檔,而在 Unix 上只有在不常見的 PATH 設定下才會使用。)
如果您擔心,那麼我們已經發布了 os/exec
的更受限制的變體,即 golang.org/x/sys/execabs
。您可以通過簡單地將以下程式碼:
import "os/exec"
替換為:
import exec "golang.org/x/sys/execabs"
並重新編譯來在您的程式中使用它。
預設保護 os/exec
我們一直在 golang.org/issue/38736 上討論是否應該更改 Windows 在 PATH 查找中始終優先使用當前目錄的行為(在 exec.Command
和 exec.LookPath
期間)。贊成更改的論點是,它可以解決本部落格文章中討論的各種安全問題。一個支持論點是,雖然 Windows 的 SearchPath
API 和 cmd.exe
仍然始終搜尋當前目錄,但 cmd.exe
的繼任者 PowerShell 卻沒有,這顯然表明原始行為是一個錯誤。反對更改的論點是,它可能會破壞打算在當前目錄中查找程式的現有 Windows 程式。我們不知道有多少這樣的程式存在,但如果 PATH 查找開始完全跳過當前目錄,它們將會出現無法解釋的錯誤。
我們在 golang.org/x/sys/execabs
中採取的方法可能是一個合理的折衷方案。它會找到舊 PATH 查找的結果,然後返回一個明確的錯誤,而不是使用當前目錄中的結果。當 prog.exe
存在時,exec.Command("prog")
返回的錯誤如下所示:
prog resolves to executable in current directory (.\prog.exe)
對於確實更改行為的程式,此錯誤應該非常清楚地說明發生了什麼。打算從當前目錄執行程式的程式可以使用 exec.Command("./prog")
代替(該語法適用於所有系統,甚至包括 Windows)。
我們已將此想法作為一個新的提案提交,golang.org/issue/43724。
下一篇文章:VS Code Go 擴充套件中預設啟用 Gopls
上一篇文章:將泛型添加到 Go 的提案
部落格索引