I recently participated in Gryphons CTF 2025 with Mengxiang, Ernest and Xizhen.
We finished 3rd place overall, tying for 1st in points. I also got my first web FC so I figured I'd do a chall walkthrough.
Kid named Jason
The webpage we are provided with gives us some instructions.
Visiting the /token endpoint does indeed give us a sample token.
When we try visiting the /verify endpoint with our token we get this error.
{"error":"The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.","file_leak":"-----BEGIN PUBLIC KEY-----\nFAKEPUBLICKEY\n-----END PUBLIC KEY-----\n","ok":false}The first thing we may notice is that the error message has a file_leak parameter, and the contents of a public key file seem to be outputted.
Decoding the JWT token from Base64 shows a kid parameter in the token header containing the path to a public key.
This points towards an LFI vulnerability. When we change kid to a known file like /etc/passwd, the server does indeed return the file's contents.
This means that if we are able to locate the flag file, we can get the server to output it and print out the flag.
After some guesses, I found it in /flag.txt.
Flag: GCTF25{t0ken_of_4ppreciation}
Pantone
We are given a webpage where we can mix colors.
In the source code, the flag is stored as a global variable.
We can also notice that there's a prototype pollution vulnerability in one of the functions.
In the /colors endpoint, there's an eval() call which we can potentially hijack to get RCE.
We can easily craft a payload that will cause the vulnerable function to recursively traverse to the global scope, where we can then overwrite _EXEC_CMD to flag.
Sending the payload to the /colors endpoint will then cause the server to output the flag.
Flag: GCTF25{COL0Rfu1_C!a55_polLU71ON}
treasure hunt
We are given a webpage where we can submit our name for a greeting message.
The greeting message hints that there is an SSTI vulnerability within the webpage.
Running a simple Python SSTI payload shown below gave an error, which revealed that the server used Node.js Nunjucks for templating rather than Jinja.
{{ self.__init__.__globals__.__builtins__['__import__']('os') }}
With this knowledge, we can craft a simple payload that gives us RCE on the webpage.
{{ range.constructor('return process')().mainModule.require('child_process').execSync('ls').toString() }}Running ls then reveals the entire directory structure.
To spare you the details, we have to read flags/part1.txt, flags/secret.bat, part2.txt and server.js to retrieve and reassemble all parts of the flag.
Flag: GCTF25{5STI_p47H_7Rav3R5A1_M45teR}
TypeFinder!
We are given a webpage with some additional functionalities.
In the forgor.php page, we can confirm that an admin account does indeed exist on the server.
We also have a file viewer page, which hints that there is a list of accounts on the server, but there's no instructions on how to use it. However, judging from the fact that forgor.php uses q for arguments, a reasonable guess would be that view.php uses f for file arguments.
That indeed works, and we are able to view the source code of view.php. From this, we learn that there is a whitelist of readable files, and users.json is indeed among them.
Viewing the source code for login.php also reveals the exact location of users.json.
Since users.json and the .php files are in different directories, we have to use path traversal to access it.
http://chal1.gryphons.sg:8004/view.php?f=../../private/users.json
Recalling the source code for login.php, the login page actually checks the MD5 hash of our entered password against the password stored on the server, so we can't just login with 0e462097431906509019562988736854 directly.
However, we can simply get the preimage of the MD5 hash using md5decrypt.
Logging in with admin and 240610708 does indeed output the flag.
Flag: GCTF25{TypE_jugg13_th3$E_nuT$}
Gryphons Site
We are given a webpage containing information about the Gryphons team members.
There is also an admin login page, but all attempts at SQLi fail, so the login can't be that straightforward.
Going back to the team page, I found out that member information is fetched using /members?id=1.
Looking at the HTML source of one of the member pages, I found a suspiciously empty div.
I also found out that it is possible to leak errors when tampering with the id parameter in /members. Below was the message I got when I set it to 0.
Replacing id with an SQLi payload like '-- gave a different error, which proved that the endpoint was vulnerable to SQLi.
Since this is essentially a blind SQLi, the most logical thing to do would be to leak the database structure and table information.
Through trial and error, I was able to produce a working payload that leaked the structure of the first table in the database.
0 union select sql,null,null,null,null,null from sqlite_master where type="table"I wrote a Python script to bruteforce all possible tables by incrementing the OFFSET in the union attack.
One of the tables leaked was creds, which could potentially contain all login credentials.
By tweaking the payload a bit, I was able to leak all the accounts from creds.
My teammate was able to crack weiyan's password hash using hashcat, revealing that her password was maple.
Using the credentials, I was finally able to login to the admin dashboard.
The flag wasn't in any of the pages on the dashboard, but I did manage to find a potential LFI vulnerability.
After some guesses, I found a file called flag.html, which displayed the Base64-encoded flag.
Flag: GCTF25{welcome_to_gryphons}