OverTheWire Natas Level 20 Walkthrough
The next level in Natas continues along with the PHPSESSID
theme. This time, the app is storing session data outside of our each, but is implementing its own way of processing session data.
This walkthrough includes source code analysis and using Burp Suite to get the flag.
What is Natas?
Natas is an online hacking game meant to help you learn and practice security concepts.
OverTheWire is a website with a number of “war games”, which are online hacking games that allow you to practice security concepts. If you are looking for a beginner introduction to web security (albeit an older tech stack), then Natas is a great place to start.
Natas is hosted on different subdomains following the pattern of http://natas<level#>.natas.labs.overthewire.org
. As you progress through the levels, you’ll need to increment the level number in the URL in order to view the correct level.
Each level requires the levels below it to be solved, so you will need the level 20 flag found in level 19 to begin this walkthrough. As before, make sure you keep notes and write down the passwords as you find them!
Level 20 ➔ 21
Visiting http://natas20.natas.labs.overthewire.org/
with credentials natas20
and eofm3Wsshxc5bwtVnEuGIlr7ivb9KABF
gets us this view:
If we check the PHPSESSID
cookie in Dev Tools, it ends up being a random string of characters. We’re given the source code, and as before, we can append ?debug
to the URL (e.g. http://natas20.natas.labs.overthewire.org/index.php?debug
) and see debug output:
Our goal is to “login as an admin” in order to get natas21 credentials. In the source code, that happens here:
In order to view the credentials, we need the $_SESSION
variable to exist, for it to contain a key called admin
and for the value of that key to be 1
.
Source code analysis
From the debug view shown above, how did we get a session value? Looking at the end of the provided source code, we see these lines of code:
First, session_set_save_handler() is called. This is a session-related function that sets user-level session storage functions. In other words, it lets the author of the code specify exactly what they want to happen when a session is read from, written to, opened, closed, and destroyed.
Then, session_start() is called, which starts or resumes an existing session. Then we call the print_credentials() function that would have shown us the flag if we had the admin values set. Then it checks for “name” in the session value and, if present, sets that for the form.
Of the functions specified in the session_set_save_handler()
call, all but two are empty functions. That means we can effectively ignore myopen()
, myclose()
, mydestroy()
, and mygarbage()
. The remaining two are myread()
and mywrite()
.
Session writing: mywrite()
If we’re to submit a name in the input provided to us, mywrite()
is called. If we have ?debug
appended to our requested URL, we’ll get debug output printing the $sid
(randomly assigned session ID) and $data
.
But we also see there are comments saying that the program uses something that’s “better” than the encoded $data
value.
The $sid
is checked to make sure it only includes alphanumeric characters using strspn(). This prevents us from doing some kind of path traversal attack using this line:
$filename = session_save_path() . "/" . "mysess_" . $sid;
Then, things get a bit weird. The $data
variable (which contains the serialized version of $_SESSION
) is cleared, and the $_SESSION
variable is ksort()‘d, which sorts the keys in ascending order.
Any time that developers go out of their way to avoid using the normal behavior of a function or library is an opportunity to find a bug. Not to say that open source developers are infallible, but the intended usage is usually there for good reason.
Finally, each key and value pair in $_SESSION
are read into the (recently cleared) $data
variable, and saved in the file.
Session reading: myread()
Next up is the myread()
function, which takes a $sid
value (the random PHPSESSID
) and makes sure it is not doing any path traversal weirdness.
Then it looks for the existence of the file that was saved previously in the mywrite()
call. It gets the contents from that file, reads it line by line, and then writes that into the $_SESSION
variable.
The last few lines are a bit confusing but if each line from the file is separated out (using the newline character) and read into a $line variable, then an example where set set name
to equal test
would look like:
- File contents “name test” are read out of file
$parts
reads “name test” using explode(), which separates “name test” using a space character, and turns it into an array of$parts = ["name", "test"];
- If the
$parts[0]
(which should be “name” in this example) is not empty,$_SESSION[$parts[0]] = $parts[1];
In our example, this means$_SESSION["name"] = "test";
Pinpointing the vulnerability
I got hung up on this level for quite a while, making the vulnerability more complicated than it needed to be. If you use the debug variable, you see output like:
The name|s:4:"test"
part looks like a classic PHP object encoding vulnerability. But, I missed earlier that while that info gets printed up in the debug statement as $data
, the value of $data
gets overwritten for this homebrew $_SESSION
storage scheme.
Another thing I missed was that myread()
has a foreach
loop to read lines out of the file. Why would there be multiple lines in a file where there’s only one key (“name”) that we can set?
Similar to that, why is there a ksort()
to sort multiple keys if there’s only one key (again, “name”) that we can input?
The last clue that I originally missed was the newline character. The program does add a new line in for us in the mywrite()
function, which partially explains why the myread()
function reads lines in using the new line character as the end of the line. However, if there’s only one intended line, it seems unnecessary.
Injecting newlines
In addition to being unnecessary, it’s also exploitable, since no filtering is happening on our name input. If we’re able to input whatever we want for our name, which then gets formatted into:
name <our input>
Let’s try adding a \n
and then more input such that the mywrite()
function breaks it up by newline and writes it to the file as:
name test
secondkey whatever_we_want_here
Of course, since our goal is $_SESSION["admin"] = 1
, our string will look like:
test\nadmin 1
We can’t inject this value using the browser form directly, so let’s open up Burp Suite and use it to capture us submitting “test” to the form input. Then, right-click the request and select Send to Repeater
.
Natas Level 20 Solution
Now that we’ve got the POST request sent to the repeater, we can make our modifications to get the flag. Here’s our original request:
We need to change test
to be test\nadmin 1
but we need to URL encode this. A newline character is URL-encoded as %0A
and a space is URL-encoded as %20
:
Add ?debug
to the end of your request and hit Send. The result should include the flag:
You can also click on the Render tab to see it more easily:
As you can see, it interpreted our input (with “name test” and “admin 1” on two separate lines) as valid data, (k)sorted them in alphabetic order, and then set the first one (“admin”) as a key/value pair in the $_SESSION variable, granting us admin access.
Takeaway: look for ways that developers have tried to modify or bypass intended functionality, as there may be logic errors or the opportunity for injected data. And if you’re a dev, don’t do this. 🙂