Natas teaches the basics of serverside web-security.
Each level of natas consists of its own website located at **http://natasX.natas.labs.overthewire.org**, where X is the level number. There is no SSH login. To access a level, enter the username for that level (e.g. natas0 for level 0) and its password.
Each level has access to the password of the next level. Your job is to somehow obtain that next password and level up. All passwords are also stored in /etc/natas_webpass/. E.g. the password for natas5 is stored in the file /etc/natas_webpass/natas5 and only readable by natas4 and natas5.
Start here:
Username: natas0
Password: natas0
URL: http://natas0.natas.labs.overthewire.org
Level 0
Look in the source for the following comment:
1
|
|
Level 1
You can still view the page source from the URL:
1
|
|
Again, the password is in a comment:
1
|
|
Level 2
In the source you will see a directory path that you can navigate to:
1
|
|
Go to http://natas2.natas.labs.overthewire.org/files/ and you will see a directory listing. Chech the users.txt file:
1 2 3 4 5 6 7 |
|
Level 3
There is a comment in the source again:
1
|
|
Well, since they mentioned Google, let’s look for a robots.txt file..If you go to http://natas3.natas.labs.overthewire.org/robots.txt , you will see the following line: Disallow: /s3cr3t/
. Navigate to http://natas3.natas.labs.overthewire.org/s3cr3t/ and there is another users.txt file: natas4:Z9tkRkWmpt9Qr7XrR5jWRkgOU901swEZ
Level 4
If our access is permitted based on the Referer header, all we have to do is change it. I used Live HTTP Headers for the task. Changed the Referer, refreshed the page and: Access granted. The password for natas5 is iX6IOfmpN7AYOQGPwtn3fXpbaJVJcHfq
Level 5
So how do they determine if I’m logged in? A cookie maybe..I used Firebug to look at cookies, and indeed there is a loggedin cookie with the value of 0. Changed it to 1 and Access granted. The password for natas6 is aGoY4q2Dc6MgDq4oL4YtoKtyAg9PeHa1
Level 6
This time we are also given the backend source code:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
That include directive stands out. If you go to http://natas6.natas.labs.overthewire.org/includes/secret.inc you get a blank page. But the source is not so blank:
1 2 3 |
|
Enter it in the form and Access granted. The password for natas7 is 7z3hEENjQtflzgnT29q7wAvMNfZdh0i9
Level 7
Inside the source there’s a comment:
1
|
|
Going to the Home and About pages, nothing interesting jumps out. However, combining the hint with how the URL looks like, I thought about local file inclusion. The normal URL is http://natas7.natas.labs.overthewire.org/index.php?page=home and I tried to read the password file by changing it to http://natas7.natas.labs.overthewire.org/index.php?page=../../../../../../etc/natas_webpass/natas8 . And it worked! The password is DBfUBfqQG69KvJvJ1iAbMoIpwSNQ9bWe
Level 8
We have to look at PHP source code again:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
So it’s looking for a string that matches the end result of all these conversions. Instead, we can reverse the process and decrypt the encoded secret to its original value.
1 2 3 4 5 6 7 8 9 10 11 |
|
Input oubWYf2kBq
in the form and you will get Access granted. The password for natas9 is W0mMhUcRRnG8dcghE4qvk3JA9lGt8nDl
Level 9
If you enter something, the backend greps for that word in a dictionary file:
1 2 3 4 5 6 7 8 9 10 11 |
|
So I thought to terminate the first command and chain another one, that would read the password: ; cat /etc/natas_webpass/natas10
. And the password is output, along with the entire file: nOpp1igQAkUzaI1GUUjzn1bFVj7xCNzu
Level 10
This level is the same as the last, except now there is some filtering in place:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
This filtering doesn’t exclude all characters that could be useful. If you read the grep manpage, you will come across this section:
Anchoring The caret ^ and the dollar sign $ are meta-characters that respectively match the empty string at the beginning and end of a line.
So I went ahead and tried ^ cat /etc/natas_webpass/natas11
, and the password was output, along with the rest of the file. This worked because grep returned every line containing the string that matches the beginning of the line (or end if you use $). I just added the password file for grep to read
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Level 11
The backend code is more complicated:
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 50 51 52 53 |
|
Well, looking at the page, we see a data cookie that’s base64 encoded, but decoding it gives rubbish because it’s XOR encrypted. The PHP code operates on it. We can also set the background color by giving it a valid value.
Now for the code! Breaking it down:
The default data is an array comprised of the values showpassword set to no and bgcolor set to #ffffff
The xor_encrypt function performs XOR encryption on the given input
The loadData function loads the data from the cookie, or keeps the default values if the data is invalid.
The saveData function sets the cookie’s value by the process of
JSON encode –> XOR encrypt –> base64 encode
At the end, we can see that if showpassword is set to yes, the password for the next level will be displayed. To achieve this, we have to mirror the cookie creation process, and change that value accordingly. But we don’t have the key used for the XOR encryption. However, we know that in XOR encryption, original xor key = encrypted
, and the following also applies: original xor encrypted = key
. Because we have both the original data and the encrypted version, we can recover the key!
I kept the original code since it does all the work, and only made some modifications to the variables:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Ran this through the PHP sandbox at http://sandbox.onlinephpfunctions.com/ and the result was the string qw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jq
. The string qw8J gets repeated, this is the key! Now we can reuse the code to create a cookie encrypted with this key, and with showpassword set to yes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Running this code gives a new cookie value: ClVLIh4ASCsCBE8lAxMacFMOXTlTWxooFhRXJh4FGnBTVF4sFxFeLFMK
. Replace the cookie value in the page and you will get the next password: The password for natas12 is EDXp0pS26wLKHZy1rDBPUZk0RKfLGIR3
Level 12
For this mission it seems we can upload a file to the server.
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 |
|
The code tests if the file satisfies the constraints and uploads it with a new name that’s randomly generated. Then it gives you the link where you can find it:
So I tried uploading a PHP file that would read the password for the next level:
1 2 3 4 |
|
But the extension is changed to a jpg, so the code doesn’t get executed. Further in the HTML there is this line:
1
|
|
I used Firebug to change the jpg extension to a php one and re-uploaded the file and this time it worked: The file upload/g72k7zidu8.php has been uploaded
. Next I followed the link and inside was the password: jmLTY0qiPZBbaKc9341cqPQZBJv7MQbY
Level 13
Ok, this time they made a modification so that only jpg files can be uploaded..or so they claim. The code is the same as the last challenge, except for a new check:
1 2 |
|
exif_imagetype() reads the first bytes of an image and checks its signature. If the signature is invalid, it returns False.
This type of check can be fooled by providing the specific magic number for the file in question. The signature for jpg files is the hex value 0xFFD8FFE0
1 2 |
|
The upload process is the same (don’t forget to modify the extension with Firebug or other tools). Then I went to the link and the password is Lg96M10TdfaPyVBkJdjymbllQ5L6qdl1
. If you notice the weird looking characters ÿØÿà before it, it’s because the text representation of the jpg magic number is also echoed back. The password starts after that
Level 14
Looking at the code hints at what type of vulnerability can be exploited:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
No input sanitization = SQL injection! Moreover, we can get additional information by setting debug to True in the URL. For that, I also included the username and password fields in the URL: http://natas14.natas.labs.overthewire.org/index.php?debug=True&username=test&password=pass
And now there was a message showing the query that was run on the backend:
1 2 |
|
After seeing how the query looks like, I used the following injection string to fool the database:
username = can be anything
password = pass" or 1=1—
To see why this works, look at the query now:
1
|
|
By fixing the quotes we forced the database to evaluate an always true condition (1=1) and bypass the credentials check. The —
comments out the rest of the query which would otherwise break our injection. If you inject in the URL, don’t forget that you need to URL encode the space (%20)
After the SQL injection, you will see this: Successful login! The password for natas15 is AwWj0w5cvxrZiONgZ9J5stNVkmxdk39J
Level 15
This time you can check if a username exists or not. Let’s look at the 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 |
|
We can again see the query that is being run on the backend by manipulating the URL: http://natas15.natas.labs.overthewire.org/index.php?debug=True&username=natas16
1 2 |
|
So, this time the SQL code checks for the existence of a user and reports whether that username exists or not. We can’t inject in a way that would directly give us the password like previously, but we know the query will be run against the users table, which contains both usernames and passwords. There is a way to bruteforce the natas16 password by forcing the database to check it one character at a time and report True of False (user exists or not). The statement to inject will look like this: username=natas16" AND password LIKE BINARY “a%”—
. Testing it in the URL (don’t forget to encode the space after comments), you can check one character a time until the database respons with the user exists message. Then you know the password begins with the respective character and you can move on to the next. But the password is 32 characters long, so we will do it in an automated way!
Some explanation about the SQL keywords:
The AND operator displays a record if both the first condition AND the second condition are true.
The LIKE operator is used in a WHERE clause to search for a specified pattern in a column.
The BINARY operator casts the string following it to a binary string. This is an easy way to force a column comparison to be done byte by byte rather than character by character. This causes the comparison to be case sensitive even if the column is not defined as BINARY or BLOB. BINARY also causes trailing spaces to be significant.
% A substitute for zero or more characters
If you run this query with the debug parameter set, you will see how it looks like:
1
|
|
When the entire statement is evaluated, the query will return True of False, and we will use that information to build the password. Here’s a Python script to do the job:
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 |
|
The passwod will be slowly built like this:
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 |
|
And now we have the password for natas16: WaIHEacj63wnNIBROHeqi3p9t0m5nhmh
Level 16
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Right, this is similar to level 9. This time, however, there is character filtering in place, so we can’t use any of these: ;|&`\‘“
. So there is no way to inject or chain commands..at the first glance! There is one useful character that is not filtered! The dollar sign! This is used in the bash shell in the same way as the backticks: for command substitution
Basically, you can use it to run a command and store its output in a variable or display it with the echo command. It looks like this:
1 2 |
|
So we want to bruteforce the password in the way we did before. Whatever we run with the $() command will be placed inside the $key variable, which is passed to grep against the dictionary file. If there is a match, the words containing it are displayed, else nothing is displayed. This is the behavior we will exploit for True and False values with our injection
Let’s test it first. In the form field, I injected $(echo matrix)
, and that return all the matches for that word:
1 2 3 4 5 |
|
The code executed by the server ends up being grep -i matrix dictionary.txt
. Now, if I inject a non-existent word, there is no output. So to check for the password, we will use a nested grep inside the main grep, that will look like this: $(grep -E ^a.* /etc/natas_webpass/natas17)matrix
. This checks if the password starts with a, and we will then iterate over all characters. Let’s imagine what happens if a is the first character of the password:
the nested grep that we injected returns a, which is appended to the word we passed after, matrix in this case, so the server-side grep looks for the word amatrix in the dictionary file, and since that doesn’t exist, nothing is returned. So we know that if nothing is returned, we had a match
there is no match for the nested grep, so the matrix word remains unchanged, and the server returns all the matrix words, which means there was no match for the character we tried in the password
To automate the injection process, I wrote a Python script again:
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 |
|
And the output:
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 |
|
Cool, we have the password for the next level: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw
Level 17
Again, a level similar to a previous one. This will be another case of SQL injection:
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 |
|
We know the database is vulnerable, but nothing is displayed to the screen, because the echo statements are commented out. So we’re going in blind! To determine if the database returns True or False to our query, we can use time-based SQL injection, by making the database load longer if our query is true, and normal if not. I tested it with this injection string: natas18" AND SLEEP(5)—
. As expected, since the user natas18 exists, the page took 5 seconds to load. When the username didn’t exist, it loaded instantly. So the sleep function is executed if the previous part of the query was true, but not if it’s false. With this in mind, I modified the Python script I used before:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
This took long because I had to use higher values for sleep() and timeout..the script kept stopping early with shorter times. Anyway, skipping the build-up output, the passwod is xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP
Level 18
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
|
This is a lot of code, but first let’s see its behavior. When you enter something in the form, a random PHPSESSID between 1 and 640 is created. Then you see the message that you are logged in as a regular user. If you turn debug on and try tampering with the cookie, you will see the message that the session was old and the admin flag was set. The objective appears to be to log in with an admin session ID, and then the credentials for the next level will be printed to the screen. The first time I looked over the code and noticed the fact that the $maxid can be predicted and bruteforced, I thought that’s the way to go, but first to understand the code:
the $maxid holds the maximum value of a PHPSESSID –> 640
isValidAdminLogin() just returns 0, so whenever it’s called it will set the admin session ID to 0 (not what we want)
isValidID($id) returns True if the ID is a valid number or numeric string, False otherwise
createID($user) this is the function that creates the PHPSESSID, with a random value between 1 and 640 (predictable and not long to bruteforce, not what we want in a session ID)
debug($msg) this just prints messages such as session started, etc.
my_session_start() this starts a session if there is a valid PHPSESSID cookie, and sets the admin session ID to 0 if it doesn’t exist in the $_SESSION array
print_credentials() prints the password we’re after if there is an admin session ID that’s set to 1 in the $_SESSION array. Otherwise it just prints a regular message
Well, the main vulnerabilities are the predictable session ID and the fact that the session starts based on the existence and validity of a cookie, which we can freely control. Since we need to be admin for the next level, we have to bruteforce the session cookies until we hit upon the one with the admin flag set to 1. Python to the rescue again:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
I ran it and it discovered the admin session ID was 46. Password for the next level is 4IwIrekcuZlA9OsjOkoUtwU6lhokCPYs
Level 19
We don’t have source code this time and apparently the session IDs aren’t sequential anymore..Let’s see. I logged in with some dummy values and noticed the PHPSESSID cookie is hex encoded now. Decoding it..surprise! It looked like this: 512-admin
. admin was what I put in the username field. I tried more bogus values for username and password and noticed that the session ID cookie is always constructed like this: random number-username
. So again, brute forcing to the rescue! Since I didn’t know how much of the code from the previous challenge has changed, I assumed the max session ID value remained the same:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
And after a while I hit the jackpot with a sessiod ID of 381-admin. The password for the next level is eofm3Wsshxc5bwtVnEuGIlr7ivb9KABF
Level 20
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
|
This is similar to the previous challenges, we still need the $_SESSION array to contain a key named admin with the value of 1. The code writes the session data to a file and that is where it will read the session ID from (the name of the file is the session ID). First, let’s look at the debug output when we change our name: http://natas20.natas.labs.overthewire.org/index.php?name=admin&debug
1 2 3 4 5 6 7 8 9 |
|
I placed the corresponding PHP code to the same line with the output for convenience. Now to analyze the relevant code:
function mywrite($sid, $data) – after checking that the session ID contains alphanumeric characters only, it sets the path where the session data will be used. The file looks like mysess_SID, see in the output above. Then it sorts the $_SESSION array by its keys and iterates over the array as key => value. In my example, you can see from the output
name => admin
that name is the key and admin is the value. Then the key and value are written to the file as follows:$data .= “$key $value\n”;
. So the data will look like this: name admin followed by a newline.function myread($sid) – this function reads the data from the file and breaks the string into an array, split by the delimiter, which in this case is the newline. Then the key and value are separated by a space. Basically, this reads what was written earlier in the file
We want to focus on the mywrite function because that’s the actual code that writes the data that we passed to the server. And the code that needs our attention is this:
1 2 3 |
|
We know that to get the password for the next level, the $_SESSION array has to contain a key / value pair of admin => 1. And the mywrite function does the writing of this data for us..so all we need is to find a way to inject it. But if you look at how data is written to the file, you will notice the newline delimiter…what if we can inject another key / value pair after our initial input? We currently have this: name => admin by entering admin in the form. But if we add a newline character we can then insert a new key / value pair that matches the expectations of the server in order to give us the password. So what we want to inject is admin\nadmin 1
. And then the session data would look like this:
1 2 |
|
Since we need to URL encode the carriage return and space, the injection looks like this: admin%0dadmin%201
. So I passed it to the URL like this: natas20.natas.labs.overthewire.org/index.php?debug&name=admin%0Aadmin%201 and here’s the output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
And we successfully acquired the next password: IFekPyrQXftziDEsUr3x21sYuahypdgJ
Level 21
We need to satisfy the same requirements as before to get next password:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
This page allows you to play with some CSS values. Also the session ID for this page is different than the other one.
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 |
|
If you turn on debug, you can see the contents of the $_SESSION array:
1 2 |
|
Again we want to insert the pair admin => 1 in the array, but the code only allows those 3 keys, so we can’t POST what we want. But if we look at this code:
1 2 3 4 5 6 |
|
As long as the key submit exists in the $REQUEST array, it will take the key / value pairs in the $REQUEST array and set them in the $_SESSION array. This is exactly what we want! But we can’t POST our values because of the validity checks. Reading through the PHP manual I saw this:
$REQUEST — An associative array that by default contains the contents of $GET, $POST and $COOKIE.
The variables in $_REQUEST are provided to the script via the GET, POST, and COOKIE input mechanisms and therefore could be modified by the remote user and cannot be trusted.
Well, we have control of what gets passed to $_REQUEST, and the code inserts whatever we give it as long as the key submit exists. Instead of POST’ing, I modified the HTML using Firebug to:
1 2 |
|
On the CSS page a new session ID was issued: 4nhuf71ckmm80osqvn1s8s8bd6
. I pasted it in the session ID of the page that should give us credentials and refreshed:
1 2 3 4 |
|
Level 22
Pretty blank, eh? Let’s look at the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Well, it looks like all you have to do is pass a GET parameter named revelio and receive the password. But if you’re not an admin, you will just be redirected to the same page via a Location header. I couldn’t think of a way to fool the page that I’m admin, but I tried messing with the headers,URL and session ID, with no success. However, when I just decided to look at the response to my request in Burp, the answer was in the HTML:
1 2 |
|
After receiving this response the browser made another request..but at this point it didn’t matter :D
Level 23
Here we have to input a password to login. Let’s see the code:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
We will get the credentials if we enter a password that contains the string iloveyou and that is larger than 10. But how can a string be compared to an integer? PHP manual to the rescue! According to the Comparison Operators section:
If you compare a number with a string or the comparison involves numerical strings, then each string is converted to a number and the comparison performed numerically.
So how is the string converted to a number?
If the string does not contain any of the characters ‘.’, ‘e’, or ‘E’ and the numeric value fits into integer type limits (as defined by PHP_INT_MAX), the string will be evaluated as an integer. In all other cases it will be evaluated as a float.
The value is given by the initial portion of the string. If the string starts with valid numeric data, this will be the value used. Otherwise, the value will be 0 (zero).
So all we have to do is enter a password that starts with a number greater than 50, followed by the iloveyou string, something like 50iloveyou:
1 2 3 |
|
// (I thought at the beginning that the comment was related to the challenge, but it turns out that’s the handle of the creator of the challenge).
Level 24
1 2 3 4 5 6 7 8 9 10 11 12 |
|
This level is centered around exploiting the strcmp function. This function takes 2 strings as arguments and performs a case sensitive, binary safe string comparison:
1 2 |
|
When reading the user contributed notes in the manual, I noticed the mention of the necessity for both parameters to be strings, otherwise the return values would be unexpected, especially if given something like an array. Then I searched for some more information about the subject, check Chaotic Security blog and the OWASP PHP security cheatsheet. If you pass an array to the function, it will return NULL, and PHP will treat it as a 0, hence fooling the code that you provided the correct password. So I did it like this: http://natas24.natas.labs.overthewire.org/?passwd[]=pwn
1 2 3 4 5 |
|
Level 25
Here we have a page with a quote that we can choose to view in English or German.
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 50 51 52 |
|
At first it would seem that we have to find a way to traverse to /etc/natas_webpass
and read the password from there, however there is a check in the code to prevent us from going there. So I next looked at bypassing the LFI filter and played a bit in a PHP sandbox to see which injection would work against the filter. Finally, I was able to read the log file with this injection: lang=….//….//….//….//….//tmp/natas25_6n8g6cuqkbuthmp8usvql1vej2.log
1 2 3 4 5 6 |
|
Excellent, now we’re getting somewhere! The next technique we’ll use to get the password is a log poisoning attack. Read more here
If you look at the logRequest function you will see that it appends various information to a log file. Part of this information is under our control (the User Agent). By using the log poisoning attack, we can change the User Agent to some PHP code of our choosing, that will then get written to the log file when we do an action which should be logged. And when the server reads the log file, it will happily execute the code contained within. Let’s see this in practice:
I changed my user agent to
<?php readfile(‘/etc/natas_webpass/natas26’); ?>
Then I refreshed the page where I was looking at the log file and among all the logged information was also the password:
1
|
|
The password is oGgWAJ7zcGT28vYazGo4rkhOPDhBu34T
Level 26
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
|
Code looks complicated so I’m breaking it down in little pieces:
We have a Logger class that writes some messages to a log file
the showImage() function sets the image tag source to the given filename, if that file exists
the drawImage() function creates an image and outputs it to the browser
drawFromUserdata() uses the user-supplied coordinates to draw lines across the image
storeData() populates an array with the 4 $_GET parameters and sets a cookie named drawing to contain the serialized and base64 encoded value of the previously created array
So far, out of ideas, but when reading about unserialize() in the PHP manual, there was a security warning:
Warning
Do not pass untrusted user input to unserialize(). Unserialization can result in code being loaded and executed due to object instantiation and autoloading, and a malicious user may be able to exploit this. Use a safe, standard data interchange format such as JSON (via json_decode() and json_encode()) if you need to pass serialized data to the user.
Next I proceeded to read more about exploiting PHP unserialization, and there were quite a few resources available, so I must be on the right track :D And this also explained the existence of the Logger class, which isn’t instantiated anywhere in the program. But first, we must understand what serialization is all about.
string serialize ( mixed $value )
Generates a storable representation of a value. This is useful for storing or passing PHP values around without losing their type and structure. Returns a binary string containing a byte-stream representation of value that can be stored anywhere.
Serialization is the conversion of a PHP data structure to a string that can be passed to external applications, such as databases, or stored in files etc.
Unserialization converts the string back to a PHP value
Now let’s look at what OWASP says about the PHP object injection attack:
The vulnerability occurs when user-supplied input is not properly sanitized before being passed to the unserialize() PHP function. Since PHP allows object serialization, attackers could pass ad-hoc serialized strings to a vulnerable unserialize() call, resulting in an arbitrary PHP object(s) injection into the application scope.
In order to successfully exploit a PHP Object Injection vulnerability two conditions must be met:
The application must have a class which implements a PHP magic method (such as wakeup or destruct) that can be used to carry out malicious attacks, or to start a “POP chain”.
All of the classes used during the attack must be declared when the vulnerable unserialize() is being called, otherwise object autoloading must be supported for such classes.
Well, we can exploit this because both conditions apply to our case! Remember that we have the Logger class, and it contains a __construct() and __destruct() magic method. So the class wasn’t just lying around for nothing in the code, hehehe!
Before continuing, I want to show an example of serialization, so you can have an idea of what it looks like with an easier to understand example than deciphering the drawing cookie:
1 2 3 4 |
|
And the output is a:3:{i:0;s:4:“Math”;i:1;s:8:“Language”;i:2;s:7:“Science”;}
. Ugh, looks complicated! But here it is:
a = array, 3 = the number of elements in the array
i = integer, 0 = index in the array, s = string, 4 = length of the string, Math is the element value, and this continues for the other elements as well
Now, to exploit this. We have:
a way to inject our own code into the application (by changing the drawing cookie that will get unserialized)
a way to write to a file (leverage the Logger class)
a way to read a file (we can browse to where images are stored inside img/)
First, I made my own malicious Logger class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
This code I wrote and tested on my local machine, first with local files, to see that it behaves as I want it to. When that was done, I used PHP to serialize and base64 encode it, so I can paste it in the cookie, and this is how it looks like:
- serialized:
1
|
|
- base64 encoded:
1
|
|
In my Logger class I just removed what wasn’t necessary from the original code, and made the modifications so that the script will create a PHP file inside the img/ directory, with this code inside it:
1
|
|
And after changing the cookie and navigating to pass.php, the code gets executed and spits the password: 55TBjpPZUUJgVP5b3BnbG6ON9uDPVzCJ
Because I used readfile(), I actually saw the password followed by a space and 33 (the length of read data). I looked in the PHP manual and noticed saw that file_get_contents() is a better choice for reading a file into a string, but I was too lazy to change it!
file_get_contents() is the preferred way to read the contents of a file into a string.
Helpful resources:
Level 27
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
|
Before digging in the code, I just tested the functionality of the login system..you can create a user and then view its username and password values. After logging in, you will see something like this:
1 2 3 |
|
I then tried to create a natas28 user to see what would happen…and surprise!
1
|
|
This tells us that there is indeed such a user in the database and that our random password doesn’t match the one stored in the database..so that’s what we want to get! I’ve tried some SQLi, but got nothing. So back to reading PHP code it is! (ugh)
checkCredentials() checks if the provided username and password (which are both escaped) exist in the table, returning True if they are
validUser() checks if the username is already in the table
dumpData() prints the data about the array containing the username and password as seen above in the log in message
createUser() inserts a new username-password pair in the table
The important part of the rest of the code is that it looks up the username in the table, creating it if it doesn’t exist, and proceeding with the credentials check and data printing if it already exists. After reading about the functions in the PHP manual I still had no idea how to continue. At this point, noticing the flow of the code was helpful:
1) when giving a username that already exists, it continues to the credentials checking part
2) if credential check is successful, the welcome message and credentials data are printed (without any other action from the user)
Judging from the above lines of reasoning, I thought that the interesting function that I might need to check again is the dumpData() one (because it returns data from the database, so it’s possible to find out about the natas18 user from it). Still no idea how to do that though, but another thing I noticed is how important the username is for the code: all the checks and actions revolve around it, and it was also possible to determine the existence of the natas18 user because of that. So, at this point, I thought the next part should be to convince the code to dump the data for natas18.
I next thought about creating a username of natas18 followed by many spaces, exceeding the 64 character limit. The code still returned wrong password, so all the spaces must be trimmed. I made a string in Python to check what really happens:
1 2 3 |
|
And I stopped inputting a password, because the code created users irrespective if they had passwords, and I could log in as an existing user with a blank password, as can be seen from this test dummy:
1 2 3 |
|
Now I tried to create a user with that long string and yeah, the space is removed:
1
|
|
However, when next I tried to log in just as natas28 with no password, here is what awaited me!
1 2 3 |
|
Why was this possible? Remember the flow of the code when you try to log in:
1 2 3 4 5 |
|
To confirm it, I used sqlfiddle to generate a database and queries that mimic the PHP code.
First, table creation:
1 2 3 4 |
|
Then, inserting the natas28 user with the password (I used a dummy one but assume it’s the one we’re after):
1
|
|
Next, the querying for the username as it happens in the validUser() function:
1
|
|
And the result:
When trying to insert the long string next I received a data truncation error because it was larger than the allowed 64 characters, so I manually adjusted it to natas28 + 57 spaces:
1
|
|
Then I added it to the table:
1
|
|
And when querying the database both are returned (with the first being the original natas28 user):
To summarize:
1 2 3 4 5 6 7 8 9 10 |
|
Password is JWwR438wkgTsNKBbcJoowyysdM82YjeF
Level 28
And it’s finished for now! Awesome challenge!
1 2 3 4 5 6 7 8 |
|