对PHP Composer的供应链攻击
供应链攻击是当今开发组织的热门话题。去年,有18,000名网络安全管理软件产品客户感染了后门程序,这是有史以来规模最大的软件供应链攻击。今年,一名安全研究人员使用一种新的供应链攻击技术成功入侵了苹果、微软、PayPal和其他科技巨头。这些攻击所利用的潜在问题是,所有现代软件都构建在其他第三方软件组件之上,并且通常不可能清楚地看到所有下载的包。虽然重用许多组件可以加快开发过程,但感染供应链是一种非常有效且微妙的攻击媒介,可以同时危害许多组织。
在PHP生态系统中,Composer是管理和安装软件依赖项的主要工具。世界各地的开发团队都使用它来简化更新过程并确保应用程序在不同环境和版本中轻松工作。为此,Composer使用了一个名为Packager的工具,它可以确定包下载的正确供应链。短短一个月内,公共包装基础设施就开始运转14亿下载请求!
在我们的安全研究中,我们发现了源代码包装商使用的作曲者。它允许我们在Packagist.org服务器上执行任意系统命令。这样一个中心组件中的一个漏洞,每月服务超过1亿个包元数据请求,具有巨大的影响,因为这种访问可能被用来窃取维护者的凭据或将包下载重定向到提供后门依赖关系的第三方服务器。
下面介绍了检测到的代码漏洞以及如何修补这些漏洞。一些易受攻击的代码从10年前Composer的第一个版本就存在了。例如,我们将详述的一个错误被引入2011年11月。发现问题后,我们向Packagist团队报告了所有问题,该团队仅在12小时内就迅速部署了修复程序并分配了CVE-2021-29472。据他们所知,该漏洞尚未被利用。
技术细节
当要求下载软件包时,Composer将首先查询PackageGist以获取其元数据(例如,这里是Composer本身的元数据)。除其他外,对于每个版本,该元数据包含两个关于从何处获取代码的字段:source,指向开发存储库,dist,指向预构建的归档。当从存储库下载代码时,Composer将使用外部系统命令来避免重新实现每个版本控制软件(VCS)的特定逻辑。为此,使用包装器执行此类调用ProcessExecutor:
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由在外壳中执行Symfony组件流程过程。最过程执行者调用在负责远程和本地存储库的任何操作(克隆、提取信息等)的VCS驱动程序中执行,例如在Git驱动程序中:
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进行转义,以防止对子命令($(…),`…`)求值在shell中,没有任何东西可以阻止用户提供以破折号(-)开头的值,并在最终命令中附加额外的参数。这种类型的漏洞称为参数或参数注入。
在所有其他驱动程序中都可以找到相同的易受攻击模式,其中用户控制的数据被正确转义,但连接到一个系统命令:
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
);
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;
}
虽然已知用户控制的值应该使用escapeshellarg()没有警告说它们仍然可以被当作选择。
但是,我们不太可能强迫用户将Composer指向攻击者控制下的任意URL。最坏的情况:如果我们已经可以这样做了,那么发布我们自己的恶意包并强迫Composer在目标服务器上下载它就容易多了。
妥协packagist.org
如果您不熟悉PHP打包生态系统,那么只要您添加一个名为composer.json在顶层目录中。然后,你只需要在packagist.org上创建一个帐户,提交你的库URL,它就会自动获取你的项目,解析你的composer.json如果一切顺利,就创建相关的包:您的包现在是公开的,在Packagist上可见,任何人都可以安装!
Packagist.org在创建时会依赖composer的API(可以作为CLI工具使用,也可以直接使用API)来取包,从而支持Git、Subversion、Mercurial等各种VCS。正如您在中看到的packagist/src/Entity/package . PHP,它将执行以下操作:
$io = new NullIO();
$config = Factory::createConfig();
$io->loadConfiguration($config);
$httpDownloader = new HttpDownloader($io, $config);
$repository = new VcsRepository(['url' => $this->repository], $io, $config, $httpDownloader); // [1]
$driver = $this->vcsDriver = $repository->getDriver(); // [2]
if (!$driver) {
return;
}
$information = $driver->getComposerInformation($driver->getRootIdentifier());
if (!isset($information['name'])) {
return;
}
if (null === $this->getName()) {
$this->setName(trim($information['name']));
}
类VcsRepository([1])来自Composer,对getDriver()的调用([2])将触发对以下VCS“驱动程序”的方法supports()和initialize()的调用:
- GitHubDriver
- GitLabDriver
- GitBitbucketDriver
- GitDriver
- HgBitbucketDriver
- HgDriver
- PerforceDriver
- FossilDriver
- SvnDriver
这些类是我们发现参数注入错误的地方!