Overthewire Natas Level 20 and 21
Table of Contents
Overthewire - Natas
This series on the overthewire webpage challenges you to think outside the box and more about communication between client and server in order to find the hidden flag on the website for the next level. The structure of the challenge is that for each level you require a user name and password to authenticate to the next level challenge website. The username is natas[X] with X being the current level (e.g. natas5 for level 5), the corresponding URL is “http: //natas[X].natas.labs.overthewire.org/", X again being the level number e.g. “http://natas5.natas.labs.overthewire.org/" for the fifth level, and the password consists of 32 alphanumeric characters.
And now without further ado, let’s get to it:
natas20
We have yet another webpage, where we can login “as admin” to retrieve the credentials for natas21. And we have the source code at our disposal! So… Let’s review it:
<body>
<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>
The print_credentials()
prints our desired credentials, if $_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1
so if there is a $_SESSION variable with a key “admin” and that key has the value 1.
On w3, we learn that session variables are set with the PHP global variable $_SESSION PHP $_SESSION and “Session variables hold information about one single user, and are available to all pages in one application.”
Overall a session is started via session_start()
. So let’s have a look at where each of the parameters are set in the code. To help us out, if a parameter “debug” is set within the $_GET request, we get some debug output from the functions myread
and mywrite
and the if-clause that looks up, whether a session with that name already exists. Functions myopen, myclose, mydestroy, mygarbage
all return True and we can probably neglect them here.
All these functions are used in the SessionHandler class to overwrite some default handler callbacks via session_set_save_handler()
.
After these new callbacks are set, the session is started thus the global PHP variable $_SESSION created via session_start().
Before going deeper in the code let’s use the debug function and the name admin to access the webpage via a $_GET request: http://natas20.natas.labs.overthewire.org/index.php?name=admin&&debug This returns:
We see MYREAD output, that the session file did not exist before, that the name is set to admin and the output of MYWRITE and where that session is stored.
If we reload that same page (F5), we get a different output:
So we see, that this time, the server knows this session and can read the stored data from the server-side stored session “3p5eeb8jha8gccedjsvar568s7”.
In general the different my-functions that are overwriting the default callbacks are executed with session open, read, write, close, (destroy, garbage) [Source]. So let’s dig deeper into the MYREAD and MYWRITE functions: First it is checked whether the session ID (sid) consists of only alphanumeric characters and is of the expected length. Then a filename is constructed via session_save_path()/sid. We can see this in the debug output: “/var/lib/php5/sessions//mysess_3p5eeb8jha8gccedjsvar568s7” Then the contents of that file is read and stored into the data variable. A $_SESSION global variable is created as an array.
Debug output tells us for each line of data its content. An important PHP function here is the explode function. It splits a string by a string and returns an array of strings, each being a substring of string “formed by splitting it on boundaries formed by the string delimiter.”
Here we have explode(" ", $line, 2)
, thus " " is the delimiter, the string is $line and the limit of substrings is 2.
Now comes the interesting part: A $parts = explode(" ", $line, 2)
variable is created and the explode function is used to fill the array. Finally if the first entry is not empty, $_SESSION[$parts[0]] = $parts[1]
the key of $_SESSION is filled with the first entry of the parts variable and the value is set as the second value in parts.
So here is the first idea to break natas20: If we could set the contents of $parts to [“admin”, 1] we have won! How do we write to that file?
The mywrite function opens the same file as mentioned above (i.e. session_save_path()/sid, e.g. “/var/lib/php5/sessions//mysess_3p5eeb8jha8gccedjsvar568s7”). Then whatever is is stored in $_SESSION is ksorted. E.g. $fruits = array("d"=>"lemon", "a"=>"orange", "b"=>"banana", "c"=>"apple"); ksort($fruits);
Returns a=orange, b=banana, c=apple, d=lemon etc..
Then an array of key value pairs is created from each entry in the $_SESSION global variable.
This is finally stored (put) into the file.
With all of this information now put together, let’s use burp to manipulate our input:
Nothing happens.
Let’s reload that page again with our crafted input:
Again nothing happens, so let’s check the debug:
As we can see, we have successfully passed on the “name” and the “admin” parameter into the $_SESSION global variable.
As we used a " " between our admin and 1 entry, this served as separator in the myread function.
Why did we need to send our session input twice?
In the first round the special session file is created. Then in the second round the admin 1 entry is being read from the file and stored into the $_SESSION variable.
And now we need to refresh that page with the same input as before to trigger the same session launch. Now we get access to the page as the parameters stored in the $_SESSION variable are now set to “admin” => 1.
Spoiler natas21:
Username: natas21Password: IFekPyrQXftziDEsUr3x21sYuahypdgJ
natas21
This time we are also tasked to login as admin. Ok what about the sourcecode this time?
<body>
<h1>natas21</h1>
<div id="content">
<p>
<b>Note: this website is colocated with <a href="http://natas21-experimenter.natas.labs.overthewire.org">http://natas21-experimenter.natas.labs.overthewire.org</a></b>
</p>
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: natas22\n";
print "Password: <censored></pre>";
} else {
print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas22.";
}
}
/* }}} */
session_start();
print_credentials();
So again, if we get the $_SESSION variable key to admin and its value to 1 we have won.
How do we do that this time?
First we go to the “colocated site” http://natas21-experimenter.natas.labs.overthewire.org and log in with our natas21 credentials.
We see this page:
Here the PHP code looks as follows:
session_start();
// if update was submitted, store it
if(array_key_exists("submit", $_REQUEST)) {
foreach($_REQUEST as $key => $val) {
$_SESSION[$key] = $val;
}
}
if(array_key_exists("debug", $_GET)) {
print "[DEBUG] Session contents:<br>";
print_r($_SESSION);
}
// only allow these keys
$validkeys = array("align" => "center", "fontsize" => "100%", "bgcolor" => "yellow");
$form = "";
$form .= '<form action="index.php" method="POST">';
foreach($validkeys as $key => $defval) {
$val = $defval;
if(array_key_exists($key, $_SESSION)) {
$val = $_SESSION[$key];
} else {
$_SESSION[$key] = $val;
}
$form .= "$key: <input name='$key' value='$val' /><br>";
}
$form .= '<input type="submit" name="submit" value="Update" />';
$form .= '</form>';
$style = "background-color: ".$_SESSION["bgcolor"]."; text-align: ".$_SESSION["align"]."; font-size: ".$_SESSION["fontsize"].";";
$example = "<div style='$style'>Hello world!</div>";
So if we use this page to create the known “admin 1” key-value pair and reload the main natas21 page, we should have cracked it, right? Oh, we get a little help again: GET with debug prints out the stored content of $_SESSION! If we do that, we get:
Alright, now we only need to do three things: We clear all cookies on the CSS experimental page, intercept the post request with burp and add a admin=1 to it, then we copy that cookie value to the main natas21 page and refresh.
Now we copy and replace that PHPSESSID cookie content in our POST request to the original natas21 site:
And we get:
Spoiler natas22:
Username: natas22 Password: chG9fbe1Tq2eWVMgjYYD1MsfIvN461kJWrap Up
We have seen how we can manipulate input parameters, taht are later on stored into a SESSION variable to login with higher priviledges than intended. Especially level natas 21 was interesting in that regard, as we have seen, what happens when different website in the same domain run on the same backend. Then if one website proves vulnerable, more of them are!
See you soon. :)