Cybersecurity Contributions
  • CyberSec
  • Zixem Challenges
  • TryHackMe write-ups
  • TryHackMe SQL Injection Lab
  • SQLi Collected Cheat Sheets & write-ups
  • Portswigger - SQLi Labs
  • Riddler CTF Challenges
  • Cyber Apocalypse CTF 2022 Web Challenges
  • CyberStarters CTF Challenges
  • SQLi Filter Bypass 101
  • Order By SQL Injection
  • Black Hat CTF Web Challenges (2022)
  • TJCTF 2023 writeup (Code Review)
  • CAT CTF 2023 Web Challenges
  • Arab Regional CTF 2023 (Cyber Talents)
  • BugHunting
    • Google dorking to SQL injection
Powered by GitBook
On this page
  • Remotely
  • Bypassme
  • Read
  • Curly
  • 500
  • Legacy Developer 1 & 2

CAT CTF 2023 Web Challenges

PreviousTJCTF 2023 writeup (Code Review)NextArab Regional CTF 2023 (Cyber Talents)

Last updated 1 year ago

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 ....

Remotely

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 :

data://text/plain,<?php echo base64_encode(file_get_contents("index.php")); ?>

Which eventually allowed me to read index.php as base64 encoded data , by decoding this data we get the flag:

Bypassme

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 :

Read

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:

for d in $(ls /); do echo "Trying $d" ;ls -las /$d |  grep -i "/proc/" 2>/dev/null ; done

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 .

Curly

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

500

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:

Legacy Developer 1 & 2

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 .

Register Route

It simply takes from us json parameters which are name and password , of course we can't register as an admin.

Login Route

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

Public Key Route

It simply opens the public key file when a request to this route is done

Flag Route

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 :

unverified_header = jwt.get_unverified_header(token)

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 :

URL/logout?r == URL/api/secrets/publickey../../../../logout?r

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 :

https://book.hacktricks.xyz/pentesting-web/file-inclusion#lfi-rfi-using-php-wrappers-and-protocols
And here we get the flag
Request
Response
dummy token with iss specified
When supplying /api/.. as the value
redirection to google
The token
Connection received
How the token looks
We got the flag
The flag from the another challenge using the same approach