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