关于PHP PEAR漏洞导致供应链攻击的分析
研究人员在PHP供应链的一个核心组件中发现了关键代码漏洞。这些漏洞本可以很容易地被威胁行为体识别和利用,只需要会一点技术专长,就可以在世界各地造成重大破坏和安全漏洞。
欧盟网络安全局(EnISA)最近的一份报告研究了2021年1月至2021年7月初的24起袭击事件,并强调50%的攻击来自已知的威胁行动者,并预测2021的勒索软件组加入这一趋势会增加四倍。此类攻击对开发人员工具(如PEAR)的影响甚至更大,因为他们可能在将其部署到生产服务器之前在自己的计算机上运行该工具,从而为攻击者提供了一个进入公司内部网络的机会。
据估计,大约有2.85亿个软件包被从pear.php.net下载,最流行的是PEAR客户端本身、控制台、存档和邮件。虽然Composer拥有更大的市场份额,但这些PEAR软件包每月仍能获得数千次下载。
下面介绍两个漏洞,利用第一个漏洞的攻击者可以接管任何开发人员帐户并发布恶意版本,而第二个漏洞将允许攻击者获得对中央PEAR服务器的持久访问。
技术细节
接下来介绍这两个漏洞的技术特性,描述它们的根本原因,以及如何在真实场景中利用它们。我们在本地虚拟机上执行了所有测试,以避免中断正式的PEAR实例,并使用了commit f3333c2上的正式Git存储库。
pear背后的源代码pear.php.net可以在GitHub上的一个名为pearweb的项目中找到。我们的发现影响了1.32之前的所有pearweb实例,在该版本中,维护人员修复了我们发现的漏洞。
该软件的作用是在软件包的名称(例如,Console_Getopt)和下载它的绝对URL(例如http://download.pear.php.net/package/Console_Getopt-1.4.3.tgz)。它的妥协将允许更改此关联,并迫使包管理器从攻击者控制下的意外来源下载包。
初始立足点:密码重置期间的弱熵
pearweb实例不允许自注册:帐户保留给愿意提出包以包含在官方PEAR存储库中的开发人员。申请账户可以通过申请账户表单完成,申请者必须提供有关其身份和想要分发的项目的信息。然后由PEAR管理员手动验证请求。
这是一个有趣的选择,可以减少滥用并最小化服务的攻击面:除了bug跟踪器,没有帐户的情况下唯一“有趣”的功能是此帐户请求表、身份验证和密码重置功能。
在SonarCloud上扫描该项目后,我们的引擎在一个名为resetPassword()的方法中发现了一个安全热点:
这段代码生成一个随机值,用MD5散列,然后将其与密码重置所需的其他详细信息一起插入数据库。MD5的使用在这里不是问题,只要哈希值足够强且唯一。
SonarCloud规则描述中详细解释了这个问题:出于安全敏感的原因,不应使用mt_rand()。让我们回顾一下连接在一起的值,然后用md5()散列:
- mt_rand(4,13):介于4和13之间的整数(包括边界);
- $user:攻击者已知并控制的要重置的帐户的用户名;
- time():当前时间戳;
- $pass1:攻击者知道并控制的新密码。
从攻击者的角度来看,最终值仅基于两个未知数,即mt_rand()和time()的输出:第一个值不能产生多个值(10),第二个值很容易被攻击者近似。此外,pear的HTTP服务器pear.php.net在其响应中添加了一个日期头,将其缩小到只有几个值(<5)。
我们可以得出结论,攻击者可以在不到50次的尝试中发现一个有效的密码重置令牌,我们开发了一个脚本来利用这个弱点并确认其影响.
作为一则轶事,这个bug是在2007年3月首次实现这个功能时引入的。
通过对现有开发人员或管理员帐户使用此漏洞,攻击者可以在现有软件包中包含恶意代码后发布其新版本。然后,每当有人从PEAR获取这些包时,它就会自动下载并执行。
获得持久性:CVE-2020-36193存档
在找到一种方法来访问保留给经批准的开发人员的功能后,威胁参与者可能会寻求在服务器上远程执行代码。这样的发现将赋予他们相当多的操作能力:即使前面提到的错误最终得到修复,后门也将允许保持对服务器的持久访问,并继续更改软件包版本。它还可以帮助他们通过修改访问日志来隐藏自己的行踪。
识别
通过第一个bug获得的初始访问将攻击面扩展到没有帐户无法访问的新功能,并且可能不太安全。
在我们的测试虚拟机上部署pearweb时,我们注意到它在一个旧版本(1.4.7,而最后一个版本是1.4.14)中拉取了依赖项归档文件:
root@pearweb:/var/www/html/pearweb# pear list
Installed packages, channel pear.php.net:
=========================================
Package Version State
Archive_Tar 1.4.7 stable
查看这个包的changelog条目,我们可以注意到,在Archive_Tar 1.4.12之前,创建指向提取目录之外的绝对路径的符号链接是可能的;该漏洞被追踪为CVE-2020-36193。
这个bug类非常强大,因为它允许在HTTP服务器提供的目录中编写PHP文件,最终导致任意代码执行。
此库用于提取临时目录中的包内容,以使用phpDocumentor进行处理,然后发布生成的文件:
cron/apidoc-queue.php
$query = "SELECT filename FROM apidoc_queue WHERE finished = '0000-00-00 00:00:00'";
$rows = $dbh->getCol($query);
foreach ($rows as $filename) {
$info = $pkg_handler->infoFromTgzFile($filename);
$tar = new Archive_Tar($filename);
// [...]
/* Extract files into temporary directory */
$tmpdir = PEAR_TMPDIR . "/apidoc/" . $name;
// [...]
$tar->extract($tmpdir);
此代码使用cron定期触发,每次发布包的新版本时,新记录都会添加到表文件名中:由于攻击者通过我们介绍的第一个bug获得了初始访问权限,因此可以访问对Archive_Tar::extract()的调用。
剥削
为了了解此漏洞背后的技术细节,有必要了解一些关于Tar档案的背景知识。归档文件按顺序存储,每个条目的前缀为512字节的头,其内容与512字节对齐。条目的结尾用两条512字节的空记录表示。文件模式、所有者和组数字标识符以及文件大小等字段使用ASCII数字存储为八进制数。
这种归档格式支持将多种类型的“对象”写入磁盘,其中包括符号链接:根据CVE描述,我们可以假设缺陷在于归档Tar对此类条目提取的实现。在源代码中很容易找到它的实现:在[1]中,我们匹配任何类型为“符号链接”的条目,删除[2]中的目标(标题条目文件名),然后最终在[3]中创建链接:
Archive/Tar.php
elseif ($v_header['typeflag'] == "2") { // [1]
if (@file_exists($v_header['filename'])) {
@unlink($v_header['filename']); // [2]
}
if (!@symlink($v_header['link'], $v_header['filename'])) { // [3]
$this->_error(
'Unable to extract symbolic link {'
. $v_header['filename'] . '}'
);
return false;
}
与$v_header['link']不同,$v_header['filename']事先使用_maliciousFilename()进行验证,以确保没有目录遍历字符和危险的方案包装:
Archive/Tar.php
private function _maliciousFilename($file)
{
if (strpos($file, 'phar://') === 0) {
return true;
}
if (strpos($file, '../') !== false || strpos($file, '..\\') !== false) {
return true;
}
return false;
}
还应该提到的是,通过始终在目标文件夹($p_path)前面加前缀,绝对路径的提取是安全的:
Archive/Tar.php
if (($p_path != './') && ($p_path != '/')) {
while (substr($p_path, -1) == '/') {
$p_path = substr($p_path, 0, strlen($p_path) - 1);
}
if (substr($v_header['filename'], 0, 1) == '/') {
$v_header['filename'] = $p_path . $v_header['filename'];
} else {
$v_header['filename'] = $p_path . '/' . $v_header['filename'];
}
}
如CVE描述所示,没有对符号链接的目的地进行验证。它可以通过多种方式加以利用,其中包括:
phar://方案包装被阻止,但文件://甚至phar://等其他值都被阻止:这些错误是CVE-2020-28948和CVE-2020-28949,它们都已在归档文件1.4.11中修复;
正在创建目标位于当前目录之外的符号链接。
我们可以创建一个指向提取目录外文件夹的新链接,并向其中写入一个文件,或者创建两个同名条目(此格式允许!),第一个是符号链接,第二个是要写的内容。
我们可以通过将任意内容写入/var/www/html/pearweb/public_html/evil.php来确认该漏洞的可利用性,演示了攻击者在服务器上执行任意代码的能力。这是概念验证视频的第二步。
修补
维护人员于8月4日首次发布了第一个补丁,在该补丁中,他们引入了一种在密码重置功能中生成伪随机字节的安全方法。
由于PHP在引用不存在的变量并将其与默认值NULL关联时不会引发致命错误,因此该代码有一个可利用的细微缺陷。
在[1]中,由16个随机字节组成的字符串被分配给$random_字节,而md5($random_字节)在[2]中被调用:第二个变量不存在($random_字节vs$RANDU字节),此操作将始终导致空字符串(d41d8cd98f00b204e9800998ecf8427e)的md5哈希。
--- a/include/users/passwordmanage.php
+++ b/include/users/passwordmanage.php
@@ -55,7 +55,12 @@ function resetPassword($user, $pass1, $pass2)
{
require_once 'Damblan/Mailer.php';
$errors = array();
- $salt = md5(mt_rand(4,13) . $user . time() . $pass1);
+ // [1]
+ $random_bytes = openssl_random_pseudo_bytes(16, $strong);
+ if ($random_bytes === false || $strong === false) {
+ $errors[] = "Could not generate a safe password token";
+ return $errors;
+ }
+ // [2]
+ $salt = md5($rand_bytes):
PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
$this->_dbh->query('DELETE FROM lostpassword WHERE handle=?', array($user));
$e = $this->_dbh->query('INSERT INTO lostpassword
@@ -91,4 +96,4 @@ function resetPassword($user, $pass1, $pass2)
}
return $errors;
}
我们将这个错误通知了维护人员,之后他们立即修复了它。他们还升级了正在使用的Archive_Tar版本,防止了我们提出的第二个漏洞。