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}.
Solution
If you zoom in at this particular plate, you can see at least two distinct identifiers that can narrow our search down.
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.
smoothie
category: Misc, Beginner
I put the flag in a blender. Oops.
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).
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.
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)
Solution
As in the previous OSINT exercise, key details from the image that can help narrow our search are,
- Date: 15-17 September (year unknown)
- Location: Sydney
- Keywords: Gala Dinner, Tech Central
A quick google search should bring up National Tech Summit 2025 (here it is my first result).
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).
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.
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}.
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.
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.
If you now inspect the words.wasm file and scroll down
to the bottom, the assembly file describes a constant in the data
section.
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).
From this, we can deduce,
- The wordle solution for each day is stored client-side.
- The list of words are stored within a data segment in a web assembly file.
- 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).
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.