OTW Natas25

Natas25

Username: natas25
Password: GHF6X7YwACaYYssHVY05cFq83hRktl4c
URL:      http://natas25.natas.labs.overthewire.org
Natas25 info
<?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);
    }
?>
Natas25 source

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:

  1. Look for a way to bypass the strstr() check for the natas_password file
  2. See if the $_SERVER['HTTP_USER_AGENT'] can arbitrarily read from a path and the session_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
Makefile

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 '-,'
PHP Warning

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!

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.

Privilege of webpass files

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.

Requesting the privileged symlink locally

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

Created symlink in /tmp/

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
Input crafting steps

Sending it worked first try, :feelsgood:

Natas25 success

PHSC138
The World Wide Web