PHP Supply Chain Attack on PEAR

by thomas chauchefoin|

PHP Supply Chain Attack on PEAR

Introduction 

After we released our research that allowed us to take over any package hosted on Packagist, the main repository used by Composer, we decided to review its counterpart named PEAR. Its use slowly decreased in favor of Composer, but it is still an integral part of the PHP ecosystem used by many companies. 

For the second time in a year, we identified critical code vulnerabilities in a central component of the PHP supply chain. We believe these vulnerabilities could have been easily identified and exploited by threat actors with only minimal technical expertise, causing important disruption and security breaches across the world.

We already discussed the SolarWinds case, but numerous non-targeted attacks have made the news since. A recent report by the European Union Agency For CyberSecurity (ENISA) studied 24 attacks reported between January 2021 and early July 2021 and highlighted that 50% of these attacks came from known threat actors and predicted a four-fold increase in 2021 as ransomware groups are joining the trend.

The impact of such attacks on developer tools such as PEAR is even more significant as they are likely to run it on their computers before deploying it on production servers, creating an opportunity for attackers to pivot into companies’ internal networks. 

It is estimated that around 285 million packages have ever been downloaded from pear.php.net, the most popular ones being the PEAR client itself, Console_Getopt, Archive_Tar, and Mail. While Composer has a larger market share, these PEAR packages still get several thousand downloads per month. 

In this article we present two bugs, both exploitable for more than 15 years. An attacker exploiting the first one could take over any developer account and publish malicious releases, while the second bug would allow the attacker to gain persistent access to the central PEAR server. 

Before diving into the technical details, check out our video showing the various stages leading to arbitrary code execution on our local PEAR instance:

Technical Details

In this section, we will cover the technical specificities of these two bugs, describe their root cause and how they can be exploited in a real-world scenario. We performed all our tests on a local virtual machine to avoid disrupting the official PEAR instance and used the official Git repository at commit f3333c2.

The source code behind pear.php.net can be found on GitHub, in a project named pearweb. Our findings affect all pearweb instances before 1.32, version in which the maintainers fixed the vulnerabilities we discovered. 

The role of this software is to provide a bridge between the name of a package (e.g., Console_Getopt) and the absolute URL where to download it from (e.g., http://download.pear.php.net/package/Console_Getopt-1.4.3.tgz). Its compromise would allow changing this association and force package managers to download packages from unintended sources under the attacker’s control.

Initial Foothold: Weak Entropy during Password Reset

pearweb instances do not allow self-registration: accounts are reserved to developers willing to propose packages for inclusion in the official PEAR repository. Requesting accounts can be done with the Request Account form, where the requester has to provide information about their identity and the project they want to distribute. Requests are then manually validated by PEAR administrators. 

This is an interesting choice to reduce abuse and to minimize the attack surface of the service: excluding the bug tracker, the only “interesting” features available without an account are this account request form, the authentication and the password reset functionality. 

After scanning this project on SonarCloud, our engine identified a Security Hotspot in a method named resetPassword()

SonarCloud reporting the use of an unsafe number generator in resetPassword()

This code generates a random value, hash it with MD5 and then inserts it in the database along with other details required for the password reset. The use of MD5 is not a problem here, as long the hashed value is strong enough and unique.

The problem is explained in great detail in the SonarCloud rule description: mt_rand() should not be used for security-sensitive reasons. Let’s review the values concatenated together and then hashed with md5():

  • mt_rand(4,13): an integer between 4 and 13 (inclusive bounds);
  • $user: the username of the account to reset, known and controlled by the attacker;
  • time(): the current timestamp;
  • $pass1: the new password to use, known and controlled by the attacker.

From the attacker’s point of view, the final value is only based on two unknowns, which are the output of mt_rand() and time(): the first one cannot yield many values (10), and the second one can easily be approximated by the attacker. In addition, the HTTP server of pear.php.net adds a Date header to its responses, narrowing it down to only a few values (< 5). 

We could conclude that attackers can discover a valid password reset token in less than 50 tries, and we developed a script to exploit this weakness and confirm its impact: this is the first step of the introduction video.

For the anecdote, this bug was introduced in March 2007 when first implementing this feature

By using this exploit against existing developer or administrator accounts, attackers could publish new releases of existing packages after including malicious code in them. It would then be automatically downloaded and executed every time somebody fetches these packages from PEAR. 

Gaining Persistence: CVE-2020-36193 in Archive_Tar

After finding a way to access the features reserved to approved developers, threat actors are likely to look to gain remote code execution on the server. Such discovery would grant them considerably more operational capabilities: even if the previously mentioned bug ends up being fixed, a backdoor will allow keeping persistent access to the server and to continue to alter packages releases. It could also help them to hide their tracks by modifying access logs.

Identification

The initial access obtained with this first bug expands the attack surface to new features that were not reachable without an account and also likely to be less secure. 

When deploying pearweb on our test virtual machine, we noticed that it pulled the dependency Archive_Tar in an old version (1.4.7, while the last one is 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

Looking at the changelog entries of this package, we can notice that until Archive_Tar 1.4.12, creating a symbolic link pointing to an absolute path outside of the extraction directory was possible; this bug is tracked as CVE-2020-36193. 

That bug class is very powerful, as it could allow writing a PHP file in a directory served by the HTTP server, ultimately leading to arbitrary code execution.

This library is used to extract package contents in a temporary directory to process them with phpDocumentor and later publish the resulting files:

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);

This code is triggered at regular intervals using cron and new records are added to the table filename every time a new release of a package is published: this call to Archive_Tar::extract() is then reachable by attackers thanks to the initial access they obtained with the first bug we presented.

Exploitation

To understand the technical details behind this vulnerability, some background knowledge about Tar archives is necessary. Archived files are stored sequentially, each entry prefixed with a 512 bytes header and their contents aligned to 512 bytes. The end of an entry is signaled with two empty records of 512 bytes. Fields like the file mode, the owner and group numeric identifier, and the file size are stored as octal numbers using ASCII digits. 

Structure of a TAR archive.

This archive format supports writing multiple kinds of “objects” to the disk, and among them are symbolic links: based on the CVE description, we can make the assumption that the bug lies in Archive_Tar’s implementation of the extraction of such entries. It is easy to locate its implementation in the source code: at [1] we match any entry whose type is “Symbolic link”, remove the destination (header entry filename) at [2], and then finally create the link at [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;
}

Unlike $v_header['link']$v_header['filename'] is validated beforehand using _maliciousFilename() to ensure the absence of directory traversal characters and dangerous scheme wrappers

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;
}

It should also be mentioned that the extraction of absolute paths is made safe by always prefixing with the destination folder ($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'];
    }
}

As suggested by the CVE description, there is no validation performed on the destination of symbolic links. It could be exploited in several ways, among which:

  • The phar:// scheme wrapper is blocked, but not other values like file:// or even PHAR://: these bugs are CVE-2020-28948 and CVE-2020-28949, both fixed in Archive_Tar 1.4.11;
  • Creating a symbolic link whose target is outside of the current directory. 

We can either create a new link pointing to a folder out of the extraction directory and write a file to it, or create two entries with the same name (it is allowed by this format!), the first being a symbolic link and the second the contents to write. 

We were able to confirm the exploitability of this bug by writing arbitrary content to /var/www/html/pearweb/public_html/evil.php, demonstrating the ability for an attacker to execute arbitrary code on the server. This is the second step of the proof-of-concept video. 

Patch

The maintainers first released a first patch on August 4th, in which they introduced a safe method to generate pseudo-random bytes in the password reset functionality.

This code had a subtle flaw exploitable due to PHP not raising fatal errors when referencing non-existent variables and associating them with a default value, NULL

At [1], a string made of 16 random bytes is assigned to $random_bytes, while md5($rand_bytes) is called at [2]: this second variable does not exist ($random_bytes vs $rand_bytes) and this operation will always result in the MD5 hash of an empty string (d41d8cd98f00b204e9800998ecf8427e). 

--- 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;
     }

We notified the maintainers of this typo, after which they promptly fixed it. They also upgraded the version of Archive_Tar in use, preventing the second vulnerability we presented. 

Timeline

DateAction
2021-07-30We report all issues to active maintainers of PEAR.
2021-08-03A maintainer confirms the issues and starts working on patches; patches are released on GitHub a few days after.
2021-09 - 2022-03We regularly ask for updates, to make sure the patches are deployed on the production instance.
2022-03-13The patches are deployed in production.
2022-03-25The vulnerabilities of this article are publicly presented at Insomni’hack.

Summary

In this article, we presented two code vulnerabilities that could have been exploited to perform a supply chain attack against the PEAR ecosystem and compromise both developers and companies who rely on it. These vulnerabilities have been present for more than a decade and were trivial to identify and exploit, raising questions about the lack of security contributions from companies relying on them. 

We also recommend reviewing your use of PEAR and consider migrating to Composer, where the contributors community is more active and the same packages are available.

We would like to thank Ken Guest, Mark Wiesemann and Chuck Burgess of the PEAR team for handling our security advisory and deploying the patches. You can support The PHP Foundation on OpenCollective.

Related Blog Posts