CAT CTF 2023 Web Challenges
Last updated
Last updated
Greetings everyone, CAT CTF was organized by CAT Reloaded Team and 0xL4ugh CTF Team as member of both I had the honour to be author and organizer in this CTF. Also I didn't have access to any web challenge except one so I can enjoy solving them, It was totally great experience to be an organizer, author and solver in one CTF😂 let's dive in ....
We didn't have source code access to this challenge, so by heading directly to the link we find:
As we see this we can assume that it is hinting to LFI in mosaa
parameter or even RFI as the challenge name , when we try the basic payload : ../../../../../etc/passwd
we get :
So we are ahead of filter bypass now, trying different encoding techniques did not work so we can think in different approach than directory traversal right ? ... How about php wrappers ?
If we supplied this payload php://
we also get caught by the filter ... however that is not the only wrapper we have ... we have also expect://
, zip://
and eventually data://
.
We find this payload :
Which eventually allowed me to read index.php
as base64 encoded data , by decoding this data we get the flag:
This challenge actually was written by me , I've seen this scenario before and I wanted to include it.
This time we have access to the source code, so let's take a look on it
A simple flask app that takes from us note
parameter in POST request, within this note it replaces every occurrences of {{
, }}
, ..
with white space.
The next thing that it searches within the note parameter for {{ }}
and opens the file name between these curly brackets and converts it's contents to base64 encoded image. but it already omits the curly brackets the step before ... so how can we make it read files ?
If we looked at this line : file_name = os.path.join("notes", re.sub("[{}]", "", include))
we see that it takes files from the notes/
folder which contains 2 notes:
So if we managed to read any note ... we can read the flag! Let's now go to the link:
If we provided our note as follow:
The web app will just print the file name , but will not open it because it deletes {{
and }}
so in order to bypass this filter we can just type : {..{CTF.txt}..}
as it will remove the ..
leaving the note to be : {{CTF.txt}}
The base64 encoded text is the content of CTF.txt, now to read the flag we need to get out the notes folder and read the flag.txt as follow : {..{../flag.txt}..}
but it removes the ..
.
We notice in the same line : file_name = os.path.join("notes", re.sub("[{}]", "", include))
that it uses os.path.join
and if absolute path is passed to it , it will take it .. In the dockerfile we see this line : RUN mv flag.txt /
so we know that flag is in the root directory , passing this /flag.txt
to path.join
it will ignore the previous path and accept the final path...leaving the final payload to be :
Again we have access to source code , let's see it
Flask application, it stores the flag in the environment variables ... it has an endpoint called readfile
that reads files through ?file
parameter.
It uses the same os.path.join
so we can supply absolute paths as follow :
Since we need the flag we can read the /proc/self/environ
file right ? ... well actually no because from this line : blocked=["proc","self"]
these 2 are blocked.
Revealing the hint we see :
hmmmm , so we need another file on the system that is alternative or similar to /proc/self/environ
... I wanted to search through the whole system for any file that matches /proc/so I wrote this line:
The output was :
When going to /dev/fd
we see that it i actually a symlink to /proc/self/fd
:
So if I type /dev/fd/../environ
is like /proc/self/fd/../environ
and we would bypass the filter also .
This was an easy challenge that has been released later , It has source code so let's check it:
Simple curl functionality that take URL in the url
parameter , however it checks if the URL starts with http
or not and if it does contain file
then it will catch us.
The trick here is that the developer forgot to use case sensitive flag in preg_match
so if we used File:// it is actually same as file:// , To find the location of the flag we see this line in the dockerfile : COPY flag.txt /
so the final payload would be : File:///flag.txt
When we go to the link we find normal landing and a page called /contact.php
which allowed us to upload files
That's it, any file we upload we just get this alert even if bypassed the filter we don't know the uploads directory ... this was a dead end until a hint has been released:
Hmmm , so this is part of the challenge ... now we can change our approach to force the application to through errors. This can be done by passing invalid data types , invalid data formats etc ...
When I changed the name from file
to file[]
as a form of invalid data type I saw this error:
It didn't reveal the uploads path , but at least we know what we are dealing with now getimagesize()
function in PHP.
After searching ALOT about this function, I found that it has exceptions which makes it throw errors , these exceptions were:
The third point says that if the filename is impossible to be accessed then it will throw warning ... How to do this ? First I thought of passing invalid URLs with invalid images but none of the worked until I though of passing very long filename as follow:
And the response was:
Nice , we managed to get the directory name which is : Sup3r_S3cret_H1dd3n
we can even confirm the existence of it when we visit it.
Now after we get the directory name we can access our PHP files and get RCE right ? actually no because of the naming convention we can't get the correct name of the file , as you notice at the beginning there is some hex characters. But after revisiting the challenge description it was easier than I thought : once u find it u will find ur gift at Flag.txt
......!
We can access the flag directly under the hidden directory:
This challenge consisted of 2 parts , I included them in one section as I solved both with the same solution. The main idea in the first one is to use the private key the developer forgot to sign the JWT while in the second one they removed the keys. But I didn't use the keys in both so let's start.
We have code access this time so let's check , and again ... The code is the same for the 2 challenges except for the keys.
First it stores the flag and the private key in environment variables, it then creates the DB and insert the user admin in it with the id=1, it defines variable of keypath with the value of /app/secrets/publickey
.
It simply takes from us json parameters which are name
and password
, of course we can't register as an admin.
The login route accepts the same parameters and then checks the database , if it is right it will return us the JWT token , the most important keys in the token are iss
and id
, If not it will return 401
It simply opens the public key file when a request to this route is done
When accessing this route, The function token_required
is called .. If we are admin then it will display the flag else it will return access denied . Sol let's check what does token_required function does:
First it checks whether the token is present in a header called : x-access-tokens
or not
Second thing it gets the header of JWT and then searches specifically for iss
if it does exist then it will check it's value to start with /api/secrets/publickey
and if it is .. it will make a request to the public_key_url and use the response as the public key. So simply the steps are:
Get the headers of JWT
search for iss
and confirm that it starts with /api/public/secretkey
Make a request to the url with the iss appended to it.
Now let's register and login to get our valid token
When accessing the flag route with this token we get:
The app gets the headers of JWT using this line :
So let's try it on our token to see what it gets:
So we don't actually have the iss inside it ... We can test with another token from jwt.io, as the app only searches in the headers without any signing
When using this token we get:
It says invalid issuer , that means that it accepts the token with our issuer but it does not start with the /api/...
Now we know that it uses the following URL to sign the token : http://localhost:5000/api/secrets/publickey
, can we control this URL to request our server and use our own public key ? ... Upon visiting the code again I noticed this endpoint
Logout , it also takes the r
parameter which will redirect us to any URL we need:
That is actually awesome , we can redirect to our server by providing the following url : http://localhost:5000/logout?r=OUR-SERVER
in the iss ... but wait , it should start with /api/secrets/publickey right ?
Here I thought of using directory traversal :
And this will work, to test this we can use ngrok and local server
We succeed to make it points to our local server , which would be easy now to sign the token with our own public key:
I got the public key from jwt.io (You can generate your own)
I saved it on my sever naming it public_key.pem
(same format as the code says)
Add the following payload in the iss : /api/secrets/publickey../../../../logout?r=URL/public_key.pem
Change the id to be equal 1 (to be admin)
Finally access the flag route with the token
And finally ......
That's it .... I hope you've learned something in this CTF ... If you need any further explanation do not hesitate to ask me ... and thank you
Using this resource :