OverTheWire: Natas

Level 20 > Level 21

natas20-01

Once again I’ve cleaned up their code a little.

<h1>natas20</h1>
<div id="content">
<?

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

?>

<form action="index.php" method="POST">
Your name: <input name="name" value="<?=$name?>"><br>
<input type="submit" value="Change name" />
</form>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html> 

This time we have a bunch of my* functions; open, close, read, write, etc. These are registered with session_set_save_handler which, per PHP documentation, “sets user-level session storage functions.” After a quick read of the code it appears sessions are being managed manually in files rather than automagically by PHP.

The meat of mywrite is the following for loop. For each key/value pair in $_SESSION it appends “<key> <value>” plus a line ending to a buffer. The end result of mywrite is a file of space-delimited session variables, one per line.

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

The meat of myread is the following for loop which takes a file and, expecting space-delimited session variables one per line, explodes it by line, and then explodes each line into two pieces by a space. That is a takes a file full of “<key> <value>” lines and populates $_SESSION[<key>] = <value> variables.

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

The ultimate goal is to make the function print_credentials dump the password by getting a session variable named admin equal to one and satisfying the following condition.

if($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1)

We can’t just POST something like name=foo&admin=1 because only the name variable is sucked into the session variables in the line $_SESSION["name"] = $_REQUEST["name"] when the page runs.

The vulnerability lies in mywrite. What if our name was foo \n admin 1? Nothing in the code appears to prevent it. The newline would be written to disk and admin 1 interpreted as a second line when read back in.

Giving it a try (in Burp repeater to avoid URL encoding)…

natas20-02

Cool.

IFekPyrQXftziDEsUr3x21sYuahypdgJ