Natas25
Username: natas25
Password: GHF6X7YwACaYYssHVY05cFq83hRktl4c
URL: http://natas25.natas.labs.overthewire.org
<?php
// cheers and <3 to malvina
// - morla
function setLanguage(){
/* language setup */
if(array_key_exists("lang",$_REQUEST))
if(safeinclude("language/" . $_REQUEST["lang"] ))
return 1;
safeinclude("language/en");
}
function safeinclude($filename){
// check for directory traversal
if(strstr($filename,"../")){
logRequest("Directory traversal attempt! fixing request.");
$filename=str_replace("../","",$filename);
}
// dont let ppl steal our passwords
if(strstr($filename,"natas_webpass")){
logRequest("Illegal file access detected! Aborting!");
exit(-1);
}
// add more checks...
if (file_exists($filename)) {
include($filename);
return 1;
}
return 0;
}
function listFiles($path){
$listoffiles=array();
if ($handle = opendir($path))
while (false !== ($file = readdir($handle)))
if ($file != "." && $file != "..")
$listoffiles[]=$file;
closedir($handle);
return $listoffiles;
}
function logRequest($message){
$log="[". date("d.m.Y H::i:s",time()) ."]";
$log=$log . " " . $_SERVER['HTTP_USER_AGENT'];
$log=$log . " \"" . $message ."\"\n";
$fd=fopen("/var/www/natas/natas25/logs/natas25_" . session_id() .".log","a");
fwrite($fd,$log);
fclose($fd);
}
?>
Looking through this I see there are some checks in the safeinclude()
function which strip ../
out of the $filename
string. This string is passed into the function call and has the value "language/"
. The other parameter comes from _REQUEST["lang"]
, which is a variable we can control by passing values into the lang query string.
The second check where it checks for the "natas_webpass" string is going to be hard, if not impossible to bypass. I could try doing something with arrays again, but I'm not sure what will happen.
Another source of user input is in the logRequest()
function when the server logs the $_SERVER['HTTP_USER_AGENT']
and stores it in a file postfixed with session_id()
.
At first glance it would be cool to control the path for the log of the session_id, but I don't see how I could use it for anything.
My 2 options to look into are:
- Look for a way to bypass the
strstr()
check for the natas_password file - See if the
$_SERVER['HTTP_USER_AGENT']
can arbitrarily read from a path and thesession_id()
can return controllable data.
For this, I didn't want to use my installed PHP 8.0.20
, I wanted to use the version of PHP that the server was using (PHP 5.6.33
)
php5.6:
docker run --rm -it --name php5.6 -u www-data:www-data -p 8000:8000 -v `pwd`:/var/www/html --entrypoint php php:5.6.33-fpm-alpine -S 0.0.0.0:8000
I found the right version from the PHP container library and made this beautiful Makefile to store my very long docker command that I didn't want to keep searching my shell history for.
This docker run command creates an image named php5.6 from the php:5.6.33-fpm-alpine container and will link the current directory with the /var/www/html path of the container (so I can make updates to the natas25.php file) Then it'll run the php -S 0.0.0.0:8000 command which looks a little weird because I changed the entrypoint and the arguments come after the image name.
This is going to come in handy for locally testing challs when they get more complex and there isn't a way to see the serverside logs.
Directory traversal and read with session_id
The session_start()
didn't like when the PHPSESSID cookie got changed to anything with ../
in it, so it looks like I can't do a directory traversal there.
Warning: Unknown: The session id is too long or contains illegal characters, valid characters are a-z, A-Z, 0-9 and '-,'
Bypassing the strstr check
At first I tried to change the filename string in a way that the strstr
call wouldn't return anything, but the file_exists
call would.
Examples included natas\_webpass
, natas_*
, nat.s_webpass
, etc... They were able to bypass the check, but the file_exists
call wouldn't accept the file. Looking at the documentation for the file_exists function, I found a note about symlinks: "This function will return false
for symlinks pointing to non-existing files". This means that it will follow symlinks!
Testing symlink privileges
Before I go off creating symlinks to everything, let me first look at if an unprivileged user can create a symlink for a privileged file.

Here I've created 2 natas_webpass files locally with symlinks pointing to each file. I am able to create symlinks to privileged files, but the permissions of the file still apply. Ex: can't write to the root
symlink even though I created it because the linked file's permissions don't allow that.
This means that I can use a symlink on the Natas server, but I've done a couple more local tests with the PHP source first.

Here we can see a few debug messages I have printing out in the source + some warnings that I don't care about at all. First we have that the strstr
call is returning nothing, which means we've bypassed that check, and second we see that the directory traversal is successful and includes the file with the contents being displayed on the final line.
Solving it Remotely
Time to go back to the gift that keeps on giving: natas9!
Let's create a symlink for the natas26 password in /tmp/
with ; ln -s /tmp/nolook /etc/natas_webpass/natas26; echo

Then, I just have to request /tmp/nolook
. To do this I have to first figure out where the natas25.php file is stored to get the current path. I'm making an educated guess that it's at /var/www/natas/natas25/
since we have a path to the logs from this line: $fd=fopen("/var/www/natas/natas25/logs/natas25_" . session_id() .".log","a");
so the path we have to traverse is: /var/www/natas/natas25/language/
.
I also had to take into account that the safeinclude()
call would remove ../
's from the input. We can craft a string like ....//
such that after the call that removes the ../
characters, the string will look like ../
.
# Path of $filename
/var/www/natas/natas25/language/
# Directory traversal to get /tmp/nolook
../../../../../tmp/nolook
# Directory traversal with "input sanitization"
....//....//....//....//....//tmp/nolook
Sending it worked first try, :feelsgood:
