On 11th April Giuseppe Trotta
and myself organized a CTF (Capture The Flag) competition for Cyber Secure Pakistan
(A conference that combines all the stakeholders). The challenge was hosted on hack.me and contained 9 different challenges, some challenges itself contained sub-challenges. Overall, we received great feedback from vast majority of participants. No one was able to solve all the challenges within the given time frame, however a day or two we noticed that a team of "Sajjad
" and "MakMan
" was able to solve the challenge, and they were kind enough to do the writeup for the challenge, so over to Sajjad for the writeup.
I teamed-up with a very talented friend of mine to solve the challenges. He goes by the nickname "MakMan"
and possesses extra-ordinary problem solving and penetration testing skills. We solved almost all challenges together. I want to keep this write-up precise, so I would not ramble on much about what I tried and failed. I will rather explain what steps I went through in each of the challenges to exploit the vulnerability and capture its flag. I am going to explain them in descending order where order is the time I had to spend to solve the challenge.Note:
You might notice that I didn't use threading in any of my scripts to make them work faster. The reason is that I was afraid of getting my I.P address banned by the server for sending requests too fast; I was also afraid of overloading the server by sending too many requests at a time. And in some scenarios, it was not feasible to use threading.
9. Authentication Bypass (via cookie manipulation)
This was the very first challenge I attempted in this CTF and upon solving it, I found it to be the easiest. A simple sign-in page was provided on main page (index.php) which can be seen in the screenshot below.
At first, I expected it to be simple SQL injection (x' or 'x'='x) bypass; but it didn't take long for me to figure out that that was not the case, so I started with analyzing the headers and cookies and found an interesting cookie named "admin" with its value set as '0'. At this point, I became sure that it was a cookie manipulation vulnerability, so I simply modified the cookie value and set it to '1'. After that, all I had to do was submit the form with any username and password values. Below screenshot shows the result of the form submission after cookie manipulation.
The MD5 hash of this key was the flag for this challenge.
8. PHP Object Injection (via "unserialize" function)
This challenge had 300 points which in my opinion, it did not deserve. It was a simple PHP Object injection which I believe (according to my limited knowledge) can only be exploited if the some information of the source code is known. At first, the source code was not provided; even the class name (which had exploitable magic method) was not mentioned. Later, the source code was provided which made it a very easily solvable challenge.
Anyway coming back to the agenda... The main page (unserialize.php) of this challenge had a form which can be seen in the screenshot below.
And here is the given source code:
From the source code, it can clearly been seen that the direct user input is being passed to the unserialize function which is a security risk and results in a code execution. It can also be seen that a vulnerable magic method "__destruct" exists in the class named "CreateFile" which exists in the same context (or file) as the unserialize function.
Here is the vulnerable line of code:
readfile(dirname(__FILE__) . '/' . $this->tmpfile);
Anybody having basic understanding of the objection injection vulnerability would know that he can set the values of the class properties according to his will by exploiting the "unserialize" function via input. Thus, we can set the value of "tmpfile" which would be the member of the current class instance.
So here is the input payload I submitted which resulted in Local File Disclosure.
For anyone (not familar with serialization), here is the code I wrote to generate this payload.
A "KEY" was defined in the source code of unserialize.php.
The MD5 hash of that key was the flag for this challenge.
7. Cross-site Request Forgery (CSRF)
This scenario of this challenge seemed unrealistic to me for many reasons. But who cares, right? The goal was to solve the challenge which I did.
So the below hint was provided in the challenge details.
Hint: It's awesome when other people do things for you, isn't it?;)
The simulation of this hint, so the biggest part of this challenge is at "fake_user.php" file
And the credentials of a user account were also given.
The URL provided for this challenge had a small web application for bank management. There was a login page which can be seen in the screenshot below.
So I logged in using the provided credentials and noticed that a donation page existed which could be used to donate money to other members in the bank.
The goal of this challenge was to become the richest in the bank. The user whose credentials were provided had $10 in account. And it was mentioned that the current richest person in the bank has $1131.
there is a donation page? At this point I knew that it was the donation system which would be vulnerable. So I donated $5 to a random person (who shouldn't be tankful to me) to learn how it worked.
It was discovered that that a request was generated to a page "sendmoney.php" with GET parameters that included sender, receiver, amount, etc. The original parameter names can be seen in below link:
I tried to tamper the "frommemberID" parameter but the result was not a success. It seemed like the donation page was checking whether the "frommemberID" value matches with that of currently logged in user. The resulting page had this message:
So you want to steal money?? This is not the correct way!
Then I remembered the hint i.e. fake_user.php. That page was a user simulator. It also had other (a little too much) information. Here is the information which was mentioned there:
Welcome CSRF Master!
As you already know, the donation form is vulnerable to CSRF.
You also know, that to become the richest in the bank and win the gorgeous prize you must steal some money from all the members in the bank.
Hey, not too much...max $5 (the donation budget)!
We know, and you too, that to do a CSRF attack we need a victim that follows our links.
Well, to simplify we have made the following form.
This form simulate a user logged in his members area that clicks on a given link.
Below this text, was a form which had a field to input the URL which victim would visit. To cut to the chase, the donation URL was meant to be submitted in the form on "fake_user.php" page. So I tested it by pasting the same URL that I had tried directly earlier and here was the result:
Then I checked the balance and it now had $5 more than the previous balance i.e. $10 which means in total it had $15. Now I had to steal another $1132 in order to be the richest in the bank. And $1132 divided by $5 (max donation amount allowed) equals 228 (users) but I already had $15 so 228 minus 15 = 212; this was the amount to users I had to steal from. It would have taken a long time submitting each URL manually (to steal $5 from each of the 212 users). So I coded a little PHP script which automated the task.
Here is the code of that script:
So after executing the script, I checked the balance page and this was the result:
The MD5 checksum of the image displayed on the page was the flag for this challenge.
6. Authentication Bypass (SQLi)
This challenge seemed more interesting than the previous bypass challenge (#9 in this list). Just like that challenge, a sign-in form could be seen on the front page (index.php).
An ajax call to 'login.php' with POST parameters ('username' and 'password') was generated upon submitting the form. At first I expected it to be an easily discoverable but tricky SQL injection vulnerability but it turned out to be the opposite.
It didn't take much time for me to figure out that the inputs (sent via POST method) were being filtered, restricting them to only alpha-numeric characters and spaces. At this point, it started to become interesting. After testing cookies and headers for possible SQL injection bugs, I stopped thinking like a pentester and thought like a programmer. After brain-storming for about 10 minutes, it occurred to me what if the programmer has made a mistake while filtering the input? With the hypothesis in mind that the programmer might be applying filter on only $_POST requests and using $_REQUEST in the query, I took the POST parameters and send them as GET parameters with an apostrophe and voila, an error appears on the page.
Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'g00n' LIMIT 0,1' at line 5
First I thought of extracting the admin username and password from the database; but I figured that was unnecessary if the authentication page could be bypassed with good old "x' or 'x'='x
/login.php?username=x' or 'x'='x&password=x' or 'x'='x
So upon sending GET request to login.php with the injection payload, a message was appeared on the page which said "Correct loginCiro". And the SESSID cookie was set in the browser.
Then I opened the main page which redirected to another page named private.php and that page had an image slider on it with five images.
The MD5 checksums of four out of the total five images were the flags for this challenge.
5. Supposedly Analytical Test (bypassed via automation)
Okay, this was one weird challenge, lol. It was supposed to break my brains which it failed to achieve. Let me explain how.
On the challenge page, it could be seen that we had to choose a minimum or maximum number (randomly displayed on page) from a list of numbers which is also displayed on the page. Initial score was zero.
The session expires after 120 seconds which makes the score reset to zero. One wrong answer and the score becomes zero. Choosing a minimum or maximum between two numbers? Should be easy, huh?
But wait, number of numbers increase as the score increases which means when score becomes one, there are three numbers to choose from and when the score becomes two, four numbers are displayed to choose from. Still seems doable, right? Okay, let's mix the negative and positive numbers and then let the game begin.
When score reaches 10, there are 13 numbers both positive and negative and we had to choose a minimum or maximum from them. And still we did not how much score was required to win the game, so an ideal solution was automation.
I coded a PHP script to auto determine and submit the answers and stop when the key is found.
Here is the code of that PHP script:
The result that this script generated can be seen in the screenshot below.
It turned out that the key is displayed when the score becomes more than 50.
This challenge was interesting. It was like a riddle with a little twist.
A hint was provided in the description of the flag which said:
This challenge has more than 150 solutions, but are you able to find the one that starts with «#? If yes, give me the MD5 of that value!
From this line, it was understood that the username was "admin.
The next line in the JS code told that the password length should be 7.
Now comes the interesting part. It can be seen in the JS code that an "if" statement is used to verify the entered password which has six conditions combined with logical AND (&&) operator. So I analyzed the first condition this way.
document.getElementById("ptb").value.charCodeAt(0) + document.getElementById("ptb").value.charCodeAt(1)==206
This code tells that the sum of the Unicode values of the first and second characters of our password should be equal to 206. Let's have a look at the second condition.
document.getElementById("ptb").value.charCodeAt(1) + document.getElementById("ptb").value.charCodeAt(2) == 201
This condition says that the Unicode values of the first and second characters of our password should be equal to 201.
Rest of the four conditions follow the same pattern. The password could be easily calculated via simple plus, minus. But we had to calculate the password that started with «# as first two characters of the password.
the Unicode of « is 171
the Unicode of # is 35
171 + 35 = 206
Following this pattern, let's try to find out the third character using the second condition which says that the Unicode values of the first and second characters of our password should be equal to 201.
We know the second character, don't we? The Unicode of second character is 35.
201 - 35 = 166
This tells us that the character at the Unicode 166 in the third character of required password which is "|".
So far the first three characters of our password are « # and |
Following this pattern, we could manually calculate the password but some of the characters were unprintable so it was better to calculate using small code.
So after calculations, I got the following Unicode values of the password:
Here is the little piece of JS code I wrote:
Here is the output:
So this MD5 hash was the flag for this challenge.
3. Multi-level (LFI, SQLi, Decoding, etc)
This challenge was fun to solve. Specially the last stage where the password hash needs to be decoded into plain-text. I will be very precise about explaining it because it's kind of lengthy.
It had different stages and a flag was provided at each stage.
At first stage, I had to perform simple LFI. The application was designed in such a way that the name of child pages were mentioned in the URL and those pages were included into the parent page at run-time.
Here is the URL that I noticed at first:
After some attempts, I discovered that a config file (config.php) existed in a directory named "config" which was located in the parent directory of current location. So I simply used the following URL to read the contents of config file:
The output said: "Ops, where is the salt?! Yes it is here...."
A salt was actually mentioned in an HTML comment which could be seen by viewing the source code of the page.
The MD5 hash of this salt of the flag for this stage. I had to enter this salt in a page to unlock the second stage.
This stage was a simple SQL injection. Another page (beer.php) was unlocked after entering the salt which was discovered in first stage and that page had some link. The "id" parameter of the first link was vulnerable to SQLi.
On performing the SQLi (just putting a ' in the URL), another page was unlocked with the name "Kelly Green". An MD5 hash was required entered on that page. It was kind of obvious that I had to enter the password hash to the user named "Kelly Green" there. I extracted users information from the database using the below link.
/beer.php?id=1' union select 1,CONCAT_WS(CHAR(32,58,32),id,name,surname,username,password),3 from users where name="kelly" and surname="green" and 'x'='x
Here is the output:
22 : Kelly : Green : S0ZH8BR22J5 : UzA5T05UWTRkZnNhNzgwZnNkNmI3OGY2YmRzNmFmdDg3NmFzZDY1OGE=
So after entering this hash, the final stage was unlocked.
In second stage, we extracted the password hash of Kelly Green. So in this last stage, the hash needed to be cracked/decoded and I was required to enter the plain-text password of Kelly Green.
This stage would have been fun if the hint was not provided right above the input field. The hint said:
As you can see, the dbms is MySQL. In this dbms the comments on tables are stored into the INFORMATION_SCHEMA db, inside the table TABLE.
This hint made pretty obvious that the encoding or encryption algorithm was stored in the table comments. So I used the following query to extract the contents of the comment of table "users".
/beer.php?id=1' union select 1,table_comment,3 from information_schema.tables where table_schema=database() and table_name="users" and 'x'='x
Here is the output:
password = base64(base64(password) + salt)
So I had the salt which I extracted in first stage and I had the password hash which I extracted in the second stage. And I had this algorithm. It took only a few seconds to decode the hash and get its plain-text.
The plain-text password (i,e. "KON568" was the third flag for this challenge.
Upon submitting the password, a GIF image was displayed on the page.
The MD5 checksum of that image was the final flag for this challenge.
2. Blind XPath Injection
This challenge was easy but it was time-taking. Anyone familiar with blind exploitation of such injections will understand why.
The goal was defined in the challenge description as follows:
Your goal: find the secret hidden among the information sent by a special customer.
This challenge had a small PHP application for a restaurant website. Two of the total three pages had nothing of a pen-tester's interest but the third page had a form for seat reservation. It has had a link to another page to check the status of a reservation by providing email and phone.
First thing I did was reserve a seat for myself using that reservation form and while doing so, I tested if any parameter was vulnerable which was not the case. But at least I reserved a seat for myself in "one of the most important restaurants in the world," as described in the challenge information.
Anyway after doing so, I opened the page to to check my reservation. The page had a form which looked like this:
The email field in this form was vulnerable to blind XPath injection which I deducted after testing the negative and positive results by executing the form as follows:
POST data: email@example.com' and '1'='1
URL: /checkreservation.phpPOST data: firstname.lastname@example.org' and '1'='2
Later I realized I could send the same parameters in GET request as well (not important but makes things a bit easier). Anyway, I started with exploiting it manually by using the xpath's "substring" function but soon I realized that it was going take an eternity this way. So I tried to use a python tool named "xcat". It worked but it was so slow that it took about 30 minutes to extract the information of first user in the document but the secret was not stored in the information of first user. What a disappointed that was! And it reached the 4th user, the session was expired; but the secret was not yet found. But it did help because now I knew the element names and I figured which element contained the required secret message. This was the structure of the XML document:
So I wrote a little PHP script to extract only the text of "info" element which I believed contained the secret. So first I manually discovered the length of the text in "info" element and then used the script to extract the contents. I started from second user as I already knew that the info of first user didn't have the required secret and found the secret in the info of 7th user.
Here is the little script I wrote:
Here is the output it generated:
Before I show the final output, I must admit that I had a mini heart-attack when it reached at this point after about 20 minutes.
Because I had already spent about an hour while extracting the information of users in hope to find the secret.
Here is the final output:
MD5 hash of this secret (Giuseppe did a great job!) was the flag for this challenge.
1. Guess the Password (Side Channeling)
This challenge was misleading. The title was misleading plus it had only 100 points, so I thought how difficult could it be? Should be easy, so I tried to solve this at second number (after finishing the 50 points challenge) but gave up after spending about 30 minutes on it and moved to the next challenge (didn't want to waste time and solve as many challenges as possible in less time).
After solving all other challenges, I came back to this. The main page had a form with one input field which can be seen in the screenshot below:
I started with analyzing headers, cookies and HTML source expecting the password to be hidden somewhere but at first attempt, I couldn't find anything of interest. Then I thought maybe it's a simple brute-force challenge; maybe a common weak password is used. Having this thought in mind, I coded a little script and started brute-forcing using a list of most common 1000 passwords. Result? Disappointed.
Here is how the form request was submitted:
POST data: password=test
And here is the resulting page:
I am not afraid or ashamed to admit that I had never heard of "side channeling" attacks before and I had never used one and that's the reason that even after spending 2 hours on this challenge, I could not figure it out. I must admit that I was annoyed.
Anyway, I checked the HTML source once again and found this interest comment there.
"?debug for DEBUGGING" - I started wondering what it could possibly mean. I included "debug" parameter in the URL and analyzed the source but it was the same with and without the parameter.
After trying many things using this hint, I tampered the password submitting request and sent the request as below:
POST data: password=test
At first the result looked exactly the same. But after checking the source, I discovered that that was some addition content in the result; it was an HTML comment which can be seen in the screenshot below.
Now that was something interesting. I started testing the process time with different inputs in hope of find the length of the password or the charset used; for few inputs it remained the same. After few inputs, I submitted password=test and this was the result:
I noticed that the process time was changed. I submitted the same request and noticed that the time fluctuated between 0.09 and 0.10. So I increased the length by adding more characters after the word "test" but the result remained same. Then I started changed the letters of "test" one by one and analyzed the change in time, starting from the end and removed the letter if it didn't effect the time. So I left with only this:
When I changed the letter 't' to something else, the time again became zero. I tried all characters on the keyboard and the time remained zero. After putting back 't' the time was again 0.1. At this point, I figured that the first letter of the password might be 't' because it's getting passed through checks and there must be some processing going on on the valid characters which takes some time hence the increment in time. So after that, I added one letter after 't' and tried password=ta but the time remained same i.e. 0.1. So once again I replaced 'a' with every character on my keyboard one after one and when finished upper and lower-case alphabets, I inputted password=t1 and this was the result:
Yes, an addition of 0.1 in the time. This made me believe that I was going in the right direction and this was indeed the right way to "GUESS" the password. But just to be sure, I checked the third character manually and now I had first three valid characters of the password i.e. "t1c".
Obviously I didn't know the length of the password. It could have been 100 even 1000 characters long, so I decided code a PHP script to automate the task and "GUESS" the password for me.
Here is the code of that PHP script:
It was a little fun coding this one. Here is the output it generated:
"t1ckt0ck_b00m" was the valid password and the MD5 hash of this password was the flag for this challenge.
So that was it. It was really fun solving some of these challenges and it was indeed a good learning experience.
About The Author
Sajjad is a penetration tester and security enthusaists. He is a co-founder of CATRAX and REFLUXES where he offers security testing services.