PHP Composer漏洞可能引發供應鏈攻擊
Composer是PHP中管理和安全軟件依賴的主要工具,被開發團隊廣泛應用于更新過程等。因此,Composer使用名為Packagist 的在線服務來確定包下載供應鏈的正確性。而Packagist每個月的下載請求在14億次左右。
研究人員在進行安全研究時,在 Packagist使用的Composer源碼中發現了一個嚴重的安全漏洞,漏洞CVE編號為CVE-2021-29472。攻擊者利用該漏洞可以在Packagist.org 服務器上執行任意系統命令。此外,攻擊者還可以進一步竊取維護者憑證,或將包下載重定向到傳播后門依賴的第三方服務器。
漏洞分析
在請求下載包時,Composer 首先會查詢Packagist來獲取元數據。元數據中包含2個獲取代碼源的域source和dist。Source只想開發庫,dist只想預構建的庫。Composer在從庫中下載代碼時會使用外部系統命令來避免重新實現針對每隔版本控制軟件的邏輯。因此,這些調用都是用wrapper ProcessExecutor來執行的:
- composer/src/Composer/Util/ProcessExecutor.php
- use Symfony\Component\Process\Process;
- // [...]
- class ProcessExecutor
- {
- // [...]
- public function execute($command, &$output = null, $cwd = null)
- {
- if (func_num_args() > 1) {
- return $this->doExecute($command, $cwd, false, $output);
- }
- return $this->doExecute($command, $cwd, false);
- }
- // [...]
- private function doExecute($command, $cwd, $tty, &$output = null)
- {
- // [...]
- if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) {
- // [1]
- $process = Process::fromShellCommandline($command, $cwd, null, null, static::getTimeout());
- } else {
- // [2]
- $process = new Process($command, $cwd, null, null, static::getTimeout());
- }
- if (!Platform::isWindows() && $tty) {
- try {
- $process->setTty(true);
- } catch (RuntimeException $e) {
- // ignore TTY enabling errors
- }
- }
- $callback = is_callable($output) ? $output : array($this, 'outputHandler');
- $process->run($callback);
在 [1]和[2]中,可以看到參數 $command 是在shell中執行的。大多數的ProcessExecutor 調用都是在版本控制軟件驅動中執行的,版本控制軟件負載原創和本地庫的所有操作。比如,在Git驅動中:
- composer/src/Composer/Repository/Vcs/GitDriver.php
- public static function supports(IOInterface $io, Config $config, $url, $deep = false)
- {
- if (preg_match('#(^git://|\.git/?$|git(?:olite)?@|//git\.|//github.com/)#i', $url)) {
- return true;
- }
- // [...]
- try {
- $gitUtil->runCommand(function ($url) {
- return 'git ls-remote --heads ' . ProcessExecutor::escape($url); // [1]
- }, $url, sys_get_temp_dir());
- } catch (\RuntimeException $e) {
- return false;
- }
使用ProcessExecutor::escape() 可以將參數$url 逃逸以預防子命令($(...), `...`) ,但是無法預防用戶提供(--)開頭的值,只要加上其他的參數就可以成為最終的命令。這類漏洞就叫做參數注入。
類似的有漏洞的模式也出現在其他驅動中,用戶控制的數據可以成功繞過檢查并連接在一起成為系統命令:
- composer/src/Composer/Repository/Vcs/SvnDriver.php
- public static function supports(IOInterface $io, Config $config, $url, $deep = false)
- {
- $url = self::normalizeUrl($url);
- if (preg_match('#(^svn://|^svn\+ssh://|svn\.)#i', $url)) {
- return true;
- }
- // [...]
- $process = new ProcessExecutor($io);
- $exit = $process->execute(
- "svn info --non-interactive ".ProcessExecutor::escape($url),
- $ignoredOutput
- );
- composer/src/Composer/Repository/Vcs/HgDriver.php
- public static function supports(IOInterface $io, Config $config, $url, $deep = false)
- {
- if (preg_match('#(^(?:https?|ssh)://(?:[^@]+@)?bitbucket.org|https://(?:.*?)\.kilnhg.com)#i', $url)) {
- return true;
- }
- // [...]
- $process = new ProcessExecutor($io);
- $exit = $process->execute(sprintf('hg identify %s', ProcessExecutor::escape($url)), $ignored);
- return $exit === 0;
- }
更多技術細節參見:https://blog.sonarsource.com/php-supply-chain-attack-on-composer
補丁
研究人員將該漏洞提交給Packagist團隊后,該團隊快速反應,在12個小時內就部署了安全補丁。
本文翻譯自:https://blog.sonarsource.com/php-supply-chain-attack-on-composer