K17 CTF Writeup

Updated: 2025-09-24

Contents: radioactive | smoothie | pass me the salt | dinner | daily re |

This is a writeup of the K17 CTF that I participated in. This was only my 2nd ever CTF! I couldn't solve all the challenges, but the ones I did solve are listed in this writeup.

radioactive

category: OSINT, Beginner

Where is this tower located?

Note: Please answer with coordinates rounded to 3 decimal places and wrap your answer with K17{}. For example, if you think that the tower is the Eiffel Tower, please submit K17{48.858,2.296}.

tower.webp

Solution

If you zoom in at this particular plate, you can see at least two distinct identifiers that can narrow our search down.

radioactive.webp

First is the website link, which is a bit obscured on the last letter, but the text NSA on the bottom hints that the obscured letter is probably an a. This makes our website www.rfnsa.com.au.

On the website, we can search for a telecommunications tower through the site number. One of the digits for this is also hidden, but after trying a few combinations, we get a tower with the number 2154006. If you click on the map point, you can get the coordinates on a popup.

tower-location.webp

smoothie

category: Misc, Beginner

I put the flag in a blender. Oops.

chall.webp

Solution

The challenge is to “unwind” the image and get the flag. If you analyze the metadata on the original image (the uploaded image here might not have it when you download it), there’s something odd that you notice in the comment section.

$ exiftool chall.webp | tail -n 10
Green X                         : 0.3
Green Y                         : 0.6
Blue X                          : 0.15
Blue Y                          : 0.06
Pixels Per Unit X               : 3780
Pixels Per Unit Y               : 3780
Pixel Units                     : meters
Comment                         : Q3JlYXRlZCB3aXRoIEdJTVA=
Image Size                      : 500x500
Megapixels                      : 0.250

It is a Base64 encoded string, which is decoded to Created with GIMP (you can use any online decoder for this). With this hint, you can open the image in GIMP and reverse the previously applied whirl effect (i.e. in the clockwise direction).

rev-whirl.webp

pass me the salt

Category: Crypto, Beginner

Pretty pleaseeeeee 🥺🥺🥺

nc challenge.secso.cc 7002

Solution

The remote server is running a python script acting as an authentication server, where you can perform one of three tasks - create account, login, or change password. Additionally, it has an admin user created beforehand, whose password is the "admin" string encoded as hex (in plaintext).

if __name__ == "__main__":
    # admin account is created beforehand, password is clearly visible
    create_account("admin", "admin".encode().hex())
    
    while True:
                option = input("1. Create Account\n2. Login\n3. Change Password\n(1, 2, 3)> ")
                if option == "1":
                    # ... account creation logic
                elif option == "2":
                    # ... login logic
                elif option == "3":
                    # ... password change logic

First, if we see the create_account logic, we can see there are three major steps to note (described in comments).

def create_account(login, pwd):
    if login in logins.keys():
        return False
    
    # step 1: get a random salt (this can't be replicated)
    **salt = os.urandom(16)**
    
    # step 2: concatenate salt with password encoded as bytes
    **salted_pwd = salt + (pwd).encode()**
    
    # step 3: calculate the hexdigest of the salted password
    # i.e. the password bytes represented in hex format
    **passw = sha1(salted_pwd).hexdigest()**
    
    logins[login] = passw
    salts[login] = salt

    return True

The challenge is to login as the admin user to get the flag. Providing wrong password means we return after check_login function call fails. Seems simple, but there is a catch - the password can not be the string "admin" to view the flag - even though that should be our password in plaintext (or are we wrong in making this assumption?).

elif option == "2":
        # ...
        
        # checks if the login credentials are correct
    if not check_login(login, pwd):
        print("Invalid login or password.")
        continue

    if login == "admin":
            # can't use the correct password to view the flag???
        if pwd != "admin".encode().hex():
            print(f"Congratulations! Here is your flag: {os.getenv('FLAG')}")
        
      # ... rest of the logic

For the check_login function, we can note a few things. First, the pwd must be in hex format for the method bytes.fromhex to work correctly - so password must be provided in hex, which you assume will be "admin".encode().hex() == "61646d696e" .

def check_login(login, pwd):
    # ...
        
    salt = salts[login]
    salted_pwd = salt + **bytes.fromhex(pwd)**

    passw = sha1(salted_pwd).hexdigest()
    return passw == logins[login]
    
    
if __name__ == "__main__":

        # ... login logic
        # the password is passed in plaintext - no hex encoding within the code!
        # so you must pass a hex string as password - is it 61646d696e???
    if not check_login(login, pwd):
        print("Invalid login or password.")
        continue

WRONG! If you notice, the password used in creating the account isn’t the word "admin" it is actually 61646d696e itself!

if __name__ == "__main__":
        # we are encoding into hex while creating the account though!
        # so password isn't admin but 61646d696e!
    create_account("admin", "admin".encode().hex())

Since you can run the script locally, you can modify the code and add a print statement to print out the password during account creation. Here I used a debugger.

passwd.webp

This is also evident when you try to login using the password 61646d696e but the script outputs that the login credentials are incorrect.

1. Create Account
2. Login
3. Change Password
(1, 2, 3)> 2
Login: admin
Password: 61646d696e
Invalid login or password.

The logical step is to then pass 61646d696e further encoded into bytes and hex so that on decoding it is reconstructed.

In [5]: '61646d696e'.encode().hex()
Out[5]: '36313634366436393665' # this is our login password!

In [6]: bytes.fromhex('36313634366436393665')
Out[6]: b'61646d696e'

With 36313634366436393665 as our password, we can successfully login and retrieve the flag (here it is None since the script is run locally, you’ll get the actual flag when you netcat to the remote server nc challenge.secso.cc 7002).

1. Create Account
2. Login
3. Change Password
(1, 2, 3)> 2
Login: admin
Password: 36313634366436393665
Congratulations! Here is your flag: None

dinner

category: OSINT, beginner

foood!!!! fooooood!!!!! what is the name of this venue?!?!!?!? TELL ME

(no need to wrap your answer in the K17 flag format, just enter the name)

location.webp

Solution

As in the previous OSINT exercise, key details from the image that can help narrow our search are,

A quick google search should bring up National Tech Summit 2025 (here it is my first result).

image.webp

If you visit the website, the graphics on the landing page are quite similar to the poster graphics on the picture above (notice the geometric graphics).

tech-council.webp

Now this seems to be a multiple day event at different locations, but we are specifically interested in finding out the venue where the gala dinner took place. I couldn’t find the exact location within the website, so I instead looked through some LinkedIn posts made by the Tech Council of Australia (the event host) around the time of the summit.

gala-dinner.webp

Voila! We found the venue for our Gala Dinner!


daily re

Category: Rev, Web, Beginner

It's too easy to cheat at Wordle, so I fixed that! What are the words for the 72nd, 73rd, and 74th day?

Enter your answer in the format K17{<72nd day word>, <73rd day word>, <74th day word>}. For example if you think the words are 'trace', 'crane', and 'rocks' respectively, enter K17{trace, crane, rocks}.

https://daily-re.k17.secso.cc

Solution

If you inspect the network tab, there isn’t a specific server side request that fetches the word for day 17. Even when you guess the word, there is nothing sent to any external server to validate the check. This hints that the word for the day is stored somewhere on client side. A particular file that stands out to us is the words.wasm file.

wordle-1.webp

Another way this file comes into notice is that we are explicitly calling a webassembly function from our main script. This can be viewed by opening the debugger tab and inspecting the (index) file. To get the wordle script for any day, you need to provide the day number (minus one, probably due to indexing issues) and the day key (a hash value). So simply having the day numbers won’t suffice.

wordle-3.webp

If you now inspect the words.wasm file and scroll down to the bottom, the assembly file describes a constant in the data section.

wordle-2.webp

Copying and opening in a text file, we can see that it seems like a byte-layout with interleaving alphanumeric words (a few are circled for making the point).

wordle-5.webp

From this, we can deduce,

  1. The wordle solution for each day is stored client-side.
  2. The list of words are stored within a data segment in a web assembly file.
  3. To access word for a particular day, we need the day number as well as some hash value.

Now, there can be better ways to proceed to the next part, but I implemented a hack-ish solution to this. Since solution to a single day seems to be encoded as day_number, hash and word_solution, I manually separated the large string into multiple newlines. This might seem tedious, but for a small sample size (we only need about 80 lines to be sure), this is about 5 mins of work (I automated large portions of it using vim scripting, since the character width of each line is constant).

wasm.webp

The line numbers correspond to the wordle values for a particular day. I realize there could be a better way to disassemble/decompile the file for direct access through indexing, but this seems the fastest way to get the flag without having to learn too much about webasm.