๐0xFun CTF 2026
All AI generated writeups, quality will be especially low since there are were so many and challenge updates/etc., let me know of any mistakes/omissions and I'll fix.
crypto
BitStorm
Description
A custom pseudo-random number generator uses a 256-byte seed (the flag content, null-padded) split into 32 x 64-bit words as its internal state. It generates 60 outputs via a shift-register style PRNG with XOR, bit shifts, and rotations. We must reverse the PRNG to recover the initial state (the flag).
Solution
All operations in the PRNG (XOR, shifts, rotations) are linear over GF(2). This means the entire transformation from the 2048-bit initial state to each 64-bit output can be expressed as a matrix multiplication over GF(2):
output_bits = M * initial_state_bits (mod 2)With 60 outputs of 64 bits each (3840 equations) and 2048 unknown bits, the system is overdetermined. We:
Track the state transform matrix T (2048x2048 over GF(2)) through each PRNG step
At each step, compute the output as a linear function of the initial state bits
Build the full equation system M (3840x2048) and solve via Gaussian elimination
The system has full rank (2048 pivots), giving a unique solution.
Flag: 0xfun{L1n34r_4lg3br4_W1th_Z3_1s_Aw3s0m3}
import numpy as np
outputs = [11329270341625800450, 14683377949987450496, 11656037499566818711, 14613944493490807838, 370532313626579329, 5006729399082841610, 8072429272270319226, 3035866339305997883, 8753420467487863273, 15606411394407853524, 5092825474622599933, 6483262783952989294, 15380511644426948242, 13769333495965053018, 5620127072433438895, 6809804883045878003, 1965081297255415258, 2519823891124920624, 8990634037671460127, 3616252826436676639, 1455424466699459058, 2836976688807481485, 11291016575083277338, 1603466311071935653, 14629944881049387748, 3844587940332157570, 584252637567556589, 10739738025866331065, 11650614949586184265, 1828791347803497022, 9101164617572571488, 16034652114565169975, 13629596693592688618, 17837636002790364294, 10619900844581377650, 15079130325914713229, 5515526762186744782, 1211604266555550739, 11543408140362566331, 18425294270126030355, 2629175584127737886, 6074824578506719227, 6900475985494339491, 3263181255912585281, 12421969688110544830, 10785482337735433711, 10286647144557317983, 15284226677373655118, 9365502412429803694, 4248763523766770934, 13642948918986007294, 3512868807899248227, 14810275182048896102, 1674341743043240380, 28462467602860499, 1060872896572731679, 13208674648176077254, 14702937631401007104, 5386638277617718038, 8935128661284199759]
N = 2048 # 32 * 64 bits
def int_to_bits(v, nbits=64):
return [(v >> (nbits - 1 - i)) & 1 for i in range(nbits)]
def bits_to_int(bits):
v = 0
for b in bits:
v = (v << 1) | b
return v
def get_word_rows(T, word_idx):
return T[word_idx * 64:(word_idx + 1) * 64].copy()
def set_word_rows(T, word_idx, rows):
T[word_idx * 64:(word_idx + 1) * 64] = rows
def xor_shift_left(rows, shift):
result = np.zeros_like(rows)
result[:64-shift] = rows[shift:]
return result
def xor_shift_right(rows, shift):
result = np.zeros_like(rows)
result[shift:] = rows[:64-shift]
return result
def xor_rotate_left(rows, rot):
rot = rot % 64
if rot == 0: return rows.copy()
return np.roll(rows, -rot, axis=0)
def xor_rows(a, b):
return (a + b) % 2
# Build GF(2) linear system
T = np.eye(N, dtype=np.uint8)
M = np.zeros((60 * 64, N), dtype=np.uint8)
output_vec = np.zeros(60 * 64, dtype=np.uint8)
for step in range(60):
s_rows = [get_word_rows(T, i) for i in range(32)]
taps = [0, 1, 3, 7, 13, 22, 28, 31]
new_val_rows = np.zeros((64, N), dtype=np.uint8)
for tap_i in taps:
val_rows = s_rows[tap_i]
mixed = xor_rows(xor_rows(val_rows, xor_shift_left(val_rows, 11)), xor_shift_right(val_rows, 7))
mixed = xor_rotate_left(mixed, (tap_i * 3) % 64)
new_val_rows = xor_rows(new_val_rows, mixed)
extra = xor_rows(xor_shift_right(s_rows[31], 13), xor_shift_left(s_rows[31], 5))
new_val_rows = xor_rows(new_val_rows, extra)
new_T = np.zeros_like(T)
for i in range(31):
set_word_rows(new_T, i, s_rows[i + 1])
set_word_rows(new_T, 31, new_val_rows)
T = new_T
s_rows_new = [get_word_rows(T, i) for i in range(32)]
out_rows = np.zeros((64, N), dtype=np.uint8)
for i in range(32):
if i % 2 == 0:
out_rows = xor_rows(out_rows, s_rows_new[i])
else:
val_rows = s_rows_new[i]
out_rows = xor_rows(out_rows, xor_rows(xor_shift_right(val_rows, 2), xor_shift_left(val_rows, 62)))
M[step * 64:(step + 1) * 64] = out_rows
output_vec[step * 64:(step + 1) * 64] = int_to_bits(outputs[step], 64)
# Gaussian elimination over GF(2)
aug = np.hstack([M, output_vec.reshape(-1, 1)])
rows, cols = aug.shape
pivot_row = 0
pivot_cols = []
for col in range(N):
found = -1
for row in range(pivot_row, rows):
if aug[row, col] == 1:
found = row
break
if found == -1: continue
if found != pivot_row:
aug[[pivot_row, found]] = aug[[found, pivot_row]]
mask = aug[:, col].astype(bool)
mask[pivot_row] = False
aug[mask] ^= aug[pivot_row]
pivot_cols.append(col)
pivot_row += 1
solution = np.zeros(N, dtype=np.uint8)
for i, col in enumerate(pivot_cols):
solution[col] = aug[i, -1]
seed_int = bits_to_int([int(b) for b in solution])
content_bytes = seed_int.to_bytes(256, 'big').rstrip(b'\x00')
flag = f"0xfun{{{content_bytes.decode('ascii')}}}"
print(f"Flag: {flag}")MeOwl ECC
Description
We're given an elliptic curve E: yยฒ = xยณ + 19 over GF(p), a generator point P, a public key point Q = d*P, and a ciphertext encrypted with AES-CBC then DES-CBC using keys derived from the secret scalar d. The hint says "Smart's attack is broken on my curve, so I'm safe."
Solution
The curve is anomalous โ its order equals p. This makes it vulnerable to Smart's attack (SSSA attack), which solves the ECDLP by lifting points to the p-adic numbers Qp and computing a p-adic logarithm in linear time.
The challenge title hints that "non-canonical lifts" are needed โ the standard lift fails (division by zero), but Sage's built-in discrete_log for anomalous curves handles this correctly by trying alternative lifts.
Once d is recovered, we derive the AES and DES keys via SHA-256 hashes, then decrypt: DES-CBC first (outer layer), then AES-CBC (inner layer).
Flag: 0xfun{n0n_c4n0n1c4l_l1f7s_r_c00l}
The Slot Whisperer
Description
A slot machine uses a Linear Congruential Generator (LCG) with known parameters. Connect to the service, observe 10 spins, and predict the next 5.
The LCG parameters (from slot.py):
M = 2147483647
A = 48271
C = 12345
spin() = next_state % 100
Solution
Since all LCG parameters are known, we only need to recover the internal state from observed outputs. Each spin value is state % 100, so the state is congruent to the spin value mod 100. We brute-force all possible initial states (iterating state = spin[0], spin[0]+100, spin[0]+200, ... up to M) and check which one produces the full observed sequence. With ~21 million candidates and early termination on mismatch, this runs in seconds.
Once the state is recovered, we simply continue the LCG to predict the next 5 values.
Flag: 0xfun{sl0t_wh1sp3r3r_lcg_cr4ck3d}
The Roulette Conspiracy
Description
The electronic roulette uses a Mersenne Oracle with 624 internal spirits. A waitress whispers: "The secret is 0xCAFEBABE". We're given roulette.py showing a MersenneOracle class that XORs random.getrandbits(32) with 0xCAFEBABE for each spin. The server lets us call spin to get obfuscated outputs and predict to submit the next 10 raw MT values.
Solution
Classic Mersenne Twister state recovery. Python's random uses MT19937 with 624 32-bit state words. If we observe 624 consecutive tempered outputs, we can reverse the tempering to recover the full internal state and predict all future values.
Connect and call
spin624 times to collect obfuscated outputsXOR each with
0xCAFEBABEto recover the rawgetrandbits(32)valuesUntemper each value to recover the MT internal state
Clone the state into a local
random.Random()and predict the next 10 raw valuesSubmit via
predict
Flag: 0xfun{m3rs3nn3_tw1st3r_unr4v3l3d}
Back in the 90โs
Description
We are given attachments/cipher.txt, a string made of โanalog-lookingโ symbols. The hint (โEverything was analogโฆsymbols people used back thenโ) points to a visual leet alphabet where characters are intended to be read after a 90ยฐ rotation (โLSPK90 CWโ / leet speak 90 degrees clockwise).
Solution
Treat the ciphertext as a sequence of multi-character glyphs (tokens). Some plaintext characters are drawn using more than one ASCII symbol, so we must tokenize greedily (longest tokens first).
Map each token to its intended plaintext character and join them.
The important tokenization detail here is that the digit
7is drawn as|ยฏยฏ(a pipe plus a โtop barโ made from two macrons), so|ยฏยฏmust be treated as a single token.
Running the decoder yields the flag: 0XFUN{YOU_KN0W7TS_E4SY}
Solution code (same as solve_final.py):
The Fortune Teller
Description
A 64-bit Linear Congruential Generator (LCG) with known constants (A=2862933555777941757, C=3037000493, M=2^64) outputs only the upper 32 bits of each state ("glimpses"). The server provides 3 glimpses and asks us to predict the next 5 full 64-bit internal states.
Solution
The LCG computes state = (A * state + C) mod 2^64 and reveals state >> 32. Given the upper 32 bits of 3 consecutive states (h1, h2, h3), we need to recover the lower 32 bits to reconstruct the full state and predict future outputs.
Key insight: Since only the lower 32 bits (l1) of the first state are unknown, we can brute-force all 2^32 candidates. For each candidate l1:
Compute
state1 = (h1 << 32) | l1Compute
state2 = (A * state1 + C) mod 2^64Check if
state2 >> 32 == h2If so, verify with h3
Since the upper 32 bits change by ~A/2^32 โ 667M per unit l1, only ~1 value of l1 produces the correct h2, and the h3 check confirms uniqueness. A C implementation runs in ~0.6 seconds.
C brute-force (bruteforce.c):
Solve script (solve.py):
Flag: 0xfun{trunc4t3d_lcg_f4lls_t0_lll}
baby_HAWK
Description
We are given hawk/output.txt containing:
iv,enc: AES-CBC encryption of the flagTwo 2ร2 Hermitian matrices over
K = Q(zeta_256):Q = B^H * Bwith entriesq0,q1,q2S = B * B^Hwith entriess0,s1,s2
where the secret basis is B = [[f, F], [g, G]] in K (degree n=128), and H is complex conjugation in the field. The AES key is sha256(str(sk)) where sk = (f, g, F, G).
Solution
Use associativity:
B*(B^H*B) = (B*B^H)*B, i.e.B Q = S B.From the
(0,0)entry ofB Q = S B, and using:det(Q)=1(sincedet(B)=1)tr(Q)=tr(S)(sinceQandSare similar), derive the linear relation inK:
f*(q0*s2 - 1) = q0*s1*g + conjugate(q1)*conjugate(g)Write ring elements in the power basis of
zeta256, i.e. coefficient vectors inR = Z[x]/(x^128+1). Complex conjugation acts by reversing coefficients with a sign:conjugate(x^i) = -x^(128-i)fori>0.Work modulo a prime
pand turn the relation into a linear mapf โก H*g (mod p).Build an NTRU-style lattice whose very short vector is the secret
(f,g). Run LLL/BKZ (viafpylll) and scan reduced basis vectors; verify candidates by reconstructingQ,S.Subtlety:
(Q,S)are invariant under multiplying the entire basis by a 256th root of unityu = zeta256^k(sinceu*conjugate(u)=1). Lattice reduction can returnu*(f,g,F,G). Because the AES key usesstr(sk), we must try all 256 unit multiples during decryption.
Solver (solve_clean.sage):
Hawk_II
Description
The challenge script generates HAWK key material from Sage, leaks pk, random half-indexed coefficient leaks, and then encrypts the flag with:
Although this is a crypto challenge, the provided output.txt also contains the full printed secret key tuple sk, which is normally what we need to recover from side-channel data.
Solution
From output.txt, parse:
ivencthe exact printed
skstring
Then reproduce the same key derivation (sha256(str(sk).encode())) and decrypt with AES-CBC.
Running this yields:
The Fortune Teller's Revenge
Description
We are given three 32-bit โglimpsesโ of a 64-bit LCG state: glimpse() returns state_next >> 32. Between glimpses the generator performs a large jump (JUMP=100000). After printing the third glimpse, the server asks for the next 5 full 64-bit states.
Remote: nc chall.0xfun.org 56557
Solution
The underlying LCG is:
Modulus
M = 2^64state_{n+1} = (A * state_n + C) mod M
The challenge prints:
g1 = hi32(x1)g2 = hi32(x2)wherex2 = next(jump(x1))g3 = hi32(x3)wherex3 = next(jump(x2))
โjump then nextโ is itself a single affine step mod 2^64:
jump(s) = (A_JUMP*s + C_JUMP) mod 2^64F(s) = next(jump(s)) = (B*s + D) mod 2^64B = A * A_JUMP mod 2^64D = A * C_JUMP + C mod 2^64
So the glimpsed states follow x_{i+1} = (B*x_i + D) mod 2^64, and we know the top 32 bits of x1,x2,x3.
Write x = (g<<32) + l with known g = hi32(x) and unknown l = lo32(x). Split:
B = b0 + 2^32*b1(whereb0=lo32(B),b1=hi32(B))D = d0 + 2^32*d1
Then one โcollapsedโ step gives:
l' = (b0*l + d0) mod 2^32carry = (b0*l + d0) >> 32(this is an exact integer;b0*l+d0 < 2^64)g' = (b0*g + b1*l + d1 + carry) mod 2^32
From (g1,g2) we get a constraint on l1:
(b1*l1 + carry1) mod 2^32 = (g2 - (b0*g1 + d1)) mod 2^32
To solve it efficiently, split l1 = (u<<16) + v and use meet-in-the-middle on 16-bit halves, accounting for the 1-bit carry from adding the low-32 parts. This produces a tiny candidate set for l1, then we verify candidates by checking the full 64-bit recurrence also matches g3. This uniquely recovers x3 (the serverโs internal state after printing g3).
Finally, predict the next 5 full states using the original LCG step:
state <- (A*state + C) mod 2^64 repeated 5 times.
Flag: 0xfun{r3v3ng3_0f_th3_f0rtun3_t3ll3r}
Solver code
forensic
DTMF
Description
A WAV file (message.wav) contains DTMF (Dual-Tone Multi-Frequency) encoded signals. The challenge is to decode the audio and extract the hidden flag.
Solution
Step 1: Analyze the WAV file
The file is mono 16-bit PCM at 8000 Hz, 50.4 seconds long. The metadata comment field contains uhmwhatisthis, which turns out to be a Vigenere cipher key.
Step 2: Decode DTMF tones
Using the Goertzel algorithm to detect DTMF frequencies (low group: 697/770/852/941 Hz, high group: 1209/1336/1477/1633 Hz), each tone burst maps to a digit. The audio contains 288 tone bursts, all resolving to either 0 (941+1336 Hz) or 1 (697+1209 Hz).
Step 3: Binary โ Base64 โ Vigenere decrypt
The 288 binary digits form 36 bytes (288/8 = 36), which decode as ASCII to a base64 string:
Base64-decoding yields:
This resembles the flag format 0xfun{...} but is encrypted. Using the WAV metadata comment uhmwhatisthis as a Vigenere cipher key and decrypting (shifting each letter backward by the key letter's position):
Solution code:
Flag: 0xfun{Mu1t1_t4p_plu5_dtmf}
Nothing Expected
Description
A PNG image of a spy character with the text "nothing to see here, move along" is provided. The challenge hints that there's nothing in the drawing โ but something is hidden.
Solution
The PNG contains a large tEXt chunk with the keyword application/vnd.excalidraw+json, embedding the full Excalidraw project data (compressed with zlib). Excalidraw is a collaborative drawing tool that stores vector data as JSON.
Extracting and decompressing this data reveals 42 freedraw elements positioned at x-coordinates 301โ1452, far beyond the PNG's 584px width. These hidden strokes are invisible in the exported image but still present in the source data.
Rendering these freedraw paths reveals the flag written in handwriting.
The rendered image reveals the handwritten flag: 0xfun{th3_sw0rd_0f_k1ng_4rthur}
Flag: 0xfun{th3_sw0rd_0f_k1ng_4rthur}
kd
Description
Category: Forensic | Points: 445 | Solves: 12
something crashed. something was left behind.
Provided files: kd.zip containing crypter.dmp (Windows minidump), config.dat, transcript.enc, and events.xml.
Solution
The challenge provides a Windows minidump crash report from a CrypterService process (PID 15948), along with encrypted files and event logs. The title "kd" references the Windows kernel debugger.
Analysis of the dump:
The minidump is from crypter.exe, a Go-based encryption service. Using file confirms it's a Mini DuMP crash report. The events.xml shows the service performing key negotiations, rotations, and derivations before crashing with APPCRASH (exception code c0000005 - access violation).
The config in memory reveals:
Algorithm: AES-256-CBC
KeyDerivation: SHA256
KeyShards: 2
Finding the flag:
The key insight is that "something was left behind" in the crash dump's memory. The process had the flag loaded as a string constant in its binary/data section. Searching for the magic marker string N!L?BRRR_v3_CTF (found via strings) across all memory reveals multiple instances. At one location (offset 0x1640DF08 in the raw dump), the flag appears as a plaintext string immediately before the marker:
The flag was stored as a string constant in crypter.exe's data section, surviving the crash and persisting in the minidump memory.
Flag: 0xfun{wh0_n33ds_sl33p_wh3n_y0u_h4v3_cr4sh_dumps}
PrintedParts
Description
A friend of mine 3D printed something interesting.
We are given a G-code file (3D.gcode) generated by Cura for an Ultimaker S5 printer. The mesh is named flag.stl.
Solution
The G-code file contains 773 layers of 3D printer instructions. The flag is embossed as raised 3D text on the front surface of a monkey head model (Blender's Suzanne).
Approach: Visualize the G-code toolpath from the front (side view)
By parsing G1/G0 extrusion commands and plotting X position vs Z (layer number), filtered to only include segments with Y coordinates in the front-face range (Y=100-140), the embossed text becomes visible as a "side view" projection.
The text wraps around the curved face, so it appears at a diagonal angle. Different Y-range filters reveal different portions of the text as it curves around the surface.
The resulting image clearly shows the text 0xfun{this_monkey_has_a_flag} embossed diagonally on the front of Blender's Suzanne monkey head.
Flag: 0xfun{this_monkey_has_a_flag}
Ghost
Description
The interception of a transmission has occurred, with only a network capture remaining. Recover the flag before the trail goes cold.
Attachment: wallpaper.png
Solution
Multi-layer forensics: image steganography (appended archive) + visual password from SSTV-decoded image content.
Step 1: Detect hidden data in wallpaper.png
The PNG has trailer data after the IEND chunk. zsteg reveals a 7-zip archive appended to the image:
The image itself displays text from an SSTV-decoded signal: 1n73rc3p7_cOnf1rm3d (leetspeak for "intercept_confirmed").
Step 2: Extract the 7z archive
This yields a 235-byte 7z archive containing fishwithwater/nothing.txt (27 bytes), encrypted with 7zAES.
Step 3: Determine the password
The password is the leetspeak text visible in the image with consistent digit substitutions (iโ1, tโ7, eโ3, oโ0):
Note: The image displays cO which appears as uppercase O, but the correct password uses 0 (zero) for consistent leetspeak substitution.
Step 4: Extract the flag
Flag: 0xfun{l4y3r_pr0t3c710n_k3y}
Melodie
Description
A kid taps out 1337 drums, thinking it's nothing more than a noisy rhythm. Observers notice the pattern matches a strange signal they've been monitoring for weeks. What began as play suddenly becomes the key to a mystery none of them expected.
Category: Forensic | Points: 750
Solution
The challenge provides a WAV audio file (Drums/Melodie.wav) โ a mono 16-bit PCM file at 44100 Hz, ~111 seconds long.
Step 1: SSTV Red Herring
The audio contains an SSTV (Slow Scan Television) signal in Scottie 1 mode, detectable by its frequency content in the 1200โ2300 Hz range. Decoding it reveals a cartoon character with the text "SEEMS LIKE A DEADEND" โ a deliberate decoy.
Step 2: LSB Steganography
The actual flag is hidden using 2-bit LSB audio steganography in the WAV file's raw sample data. Using stegolsb (the stego-lsb Python package) to extract data from the 2 least significant bits of each audio sample reveals the flag at offset 0:
The SSTV signal served as misdirection โ the "strange signal" that observers would focus on, while the real data was embedded in the LSBs of the audio samples all along.
Flag: 0xfun{8f2b5c9d4f6a1eab3e0c4df52b79d8c1}
Tesla
Description
A Flipper .sub capture contains a single RAW_Data stream with binary-looking payload and mixed UTF-16/noise-like bytes. The task was to recover the hidden flag from this forensic dump.
Solution
Decode
RAW_Databinary tokens to raw bytes.
The payload is mostly UTF-8/ASCII with high-bit noise bytes. Remove bytes
0x80-0xFF.
Expand
%Ilc:~n,1%using the first line variable and strip obfuscating%...%placeholders to get the true script text.
This yields:
The challenge hint text is
i could be something to thisand the hex blob is the encrypted flag. XOR the blob with this UTF-8 string.
Recovered flag:
Bard
Description
The Simpsons is an old show, and Bard comes across as a bit strange.
Attachment: Bart.jpg
Solution
Steghide extraction: The hint "a bit strange" suggests steganography. Used
stegseekto brute-force the steghide passphrase onBart.jpg, finding passphrasesimpleand extractingbits.txtcontaining a URL:
Download from Cybersharing: The URL pointed to a file sharing service hosting a file called
bits.txt(460 KB) containing base64-encoded data.Decode base64: Decoding the base64 revealed a corrupted PNG file โ the 8-byte PNG signature and 4-byte IHDR chunk type were zeroed out, but the rest of the structure (IDAT chunks, dimensions, etc.) was intact.
View the image: The restored PNG (698x527) shows Bart Simpson writing the flag on a chalkboard.
Flag: 0xfun{secret_image_found!}
VMware
Description
I forgotten the password of my kali linux.
A VMware virtual machine image (Kali Linux 2025.4) is provided as a 5.5GB zip file containing a split sparse VMDK with 41 extents (~80GB virtual disk).
Solution
The challenge provides a VMware VM of Kali Linux. The VMX annotation hints at Username: kali / Password: ****. While the /etc/shadow hash for user kali does crack to the default password "kali", the actual flag is a hidden file /.flag.txt in the root of the ext4 filesystem.
Step 1: Download VM files from Cybersharing
The download link uses Cybersharing, a file-sharing platform with a JS SPA frontend. The API endpoint for downloading individual files from a zip archive is:
Container metadata was retrieved via:
Step 2: Examine VMX configuration
The .vmx file reveals the VM annotation:
Step 3: Parse the split sparse VMDK
Each .vmdk extent uses VMware's sparse VMDK format (magic KDMV). The format has:
A grain directory (GD) pointing to grain tables (GT)
Grain tables pointing to 64KB grain data blocks
Unallocated grains represent zero-filled regions
The descriptor file references 41 extents, each covering 4,194,304 sectors (2GB) of virtual disk space.
Step 4: Extract ext4 filesystem structure
Using the Python ext4 library on a raw image extracted from the first VMDK extent:
This revealed a hidden file /.flag.txt (inode 24, 41 bytes) in the root directory.
Step 5: Locate and read the flag data
The file's extent tree showed data at ext4 block 8,356,390, which translates to:
Disk offset: 34,228,822,016 bytes (partition start + block * 4096)
VMDK extent: s016 (extent index 15, covering bytes 32-34 GB of virtual disk)
Flag: 0xfun{w1th0ut_p2ssw0rd_1s_cr4zy_a2_h3ll}
11 Lines of Contact
Description
A mono WAV audio file (record.wav) and a cover image (cover.png) are provided. The cover shows "THE NOT-RANDOM RECORD" styled as a vinyl record with track listings: STATIC, SIGNAL, CALIBRATION, NOISE, ORDER. The challenge hints at "something humanity would send when it wanted to be understood without sharing a language."
Solution
The challenge is based on the Voyager Golden Record image encoding scheme. NASA's Voyager probes carried gold-plated records with images encoded as audio waveforms, where each scan line of an image was represented as amplitude values between sync pulses.
Analysis of the WAV file:
48kHz sample rate, 16-bit mono, ~11.59 seconds
The waveform contains sharp negative sync pulses at ~125 Hz (every 384 samples / 8ms)
Between sync pulses, amplitude values encode pixel brightness for one scan line
Decoding process:
Detect sync pulses (negative spikes below -25000)
Extract amplitude data between consecutive pulses as scan lines
Resample each line to a fixed width (384 pixels)
Map amplitude to brightness and stack lines into an image
The decoded 384x1022 image reveals:
"CALIBRATION" header with a calibration circle (matching the Voyager record's test image)
"0xfun :: SIGNALS"
The flag:
0xfun{g0ld3n_r3c0rd_1s_n0t_r4nd0m}"nothing here is truly random"
Flag: 0xfun{g0ld3n_r3c0rd_1s_n0t_r4nd0m}
Pixel Rehab
Description
Our design intern "repaired" a broken image and handed us the result, claiming the important part is still in there. All we know is the original came from a compressed archive, and something about the recovery feels suspicious. Find what was actually archived and submit the flag.
Solution
We're given pixel.fun, a file that looks like a PNG but has a corrupted first byte (0x88 instead of 0x89).
Step 1: Fix the PNG and analyze the image
Fixing the first byte reveals a 1000x650 PNG showing a "Pixel Rehab Clinic" card with a decoy flag 0xfun{almo5t_th3re} and the text "(looks legit, right?)" โ a clear red herring.
Step 2: Find the hidden 7z archive after IEND
After the PNG's IEND chunk, there are 1188 bytes of trailing data. The first 6 bytes are 89 50 4E 47 0D 0A (PNG signature), but replacing them with the 7z magic bytes 37 7A BC AF 27 1C produces a valid 7z archive.
Step 3: Extract the archive
The 7z contains real_flag.png (actually a WEBP file) showing a QR code (rickroll decoy) with the real flag text at the bottom:
0xfun{FuN_PN9_f1Le_7z}
The flag name cleverly references the solution: a fuN PNG9 (not-quite-PNG) f1Le hidden in a 7z archive.
hardware
Analog Nostalgia
Description
We are given attachments/signal.bin, described as one digitized VGA frame from a 640x480 output.
signal.bin is not only raw pixel data:
It starts with ASCII:
check trailer. for hint.\n(25 bytes).It ends with a ZIP trailer (
trailer.zip) that containshint.txt.The middle section is exactly one 800x525 VGA timing frame with 5 bytes per sample:
R, G, B, HSYNC, VSYNC.
This size check matches perfectly:
800 * 525 = 420000samples420000 * 5 = 2100000bytes
Solution
Decode the first full VGA frame payload, reshape to (525, 800, 5), convert RGB from 6-bit (0..63) to 8-bit (0..255), then render:
visible area
640x480(frame.png)full timing area
800x525(frame_full_800x525.png)
The rendered meme text contains the flag in the bottom caption: 0XFUN{AN4LOG_IS_NOT_D3AD_JUST_BL4NKING}
Accepted flag: 0XFUN{AN4LOG_IS_NOT_D3AD_JUST_BL4NKING}
Full solution code:
Digital Transition
Description
We intercepted a raw signal capture from an HDMI display adapter. The data appears to be a single digitized frame from a 640x480 HDMI output. We are given signal.bin (1,680,216 bytes).
Solution
The file size is approximately 800 * 525 * 4 = 1,680,000 bytes, which matches the total pixel count (including blanking) for a standard 640x480 VGA/HDMI signal (800 total horizontal pixels, 525 total vertical lines), with 4 bytes per pixel clock cycle.
The first 16 bytes are a header ("check end." + padding). The remaining data consists of 4-byte groups, one per pixel clock. Each 32-bit word (little-endian) packs three 10-bit TMDS (Transition-Minimized Differential Signaling) encoded channels:
Bits 9:0 = Channel 0 (Blue)
Bits 19:10 = Channel 1 (Green)
Bits 29:20 = Channel 2 (Red)
Bits 31:30 = unused
TMDS encoding (used in HDMI/DVI) encodes 8-bit pixel values into 10-bit symbols for DC balance and transition minimization. Decoding reverses this:
Bit 9 is the inversion flag - if set, XOR bits 7:0 with 0xFF
Bit 8 selects XOR vs XNOR mode for the transition chain
Reconstruct the original 8-bit value by reversing the XOR/XNOR chain from bit 0 upward
After decoding all pixels and rendering as an 800x525 image, the active 640x480 region shows a Kirby game cover image with the flag overlaid as text.
Flag: 0xfun{TMDS_D3CODED_LIKE_A_PRO}
Packet Stream
Description
We intercepted a raw signal capture from a DisplayPort display adapter. The data appears to be a single digitized frame from a 640x480 DisplayPort output.
Goal: recover the frame and read the flag.
Solution
attachments/signal.bin is a ZIP file with a large prefix. The ZIP is a decoy (it contains the same hint.txt as the repo). The real capture is the 2,100,020-byte prefix before PK\x03\x04.
Parse the prefix and treat it as a DP-like bitstream
Prefix begins with
dp_signal\xc0check_end\xc0(20 bytes).Remaining payload is exactly
2,100,000bytes.Interpret the payload as a stream of bits:
unpack bits little-endian within each byte
group into consecutive 10-bit code-groups
This gives 2,100,000*8/10 = 1,680,000 10-bit symbols.
8b/10b decode
Decode each 10-bit symbol into (ctrl, byte) using an 8b/10b decoder (encdec8b10b).
Reshape into a โframeโ grid
Reshape decoded bytes time-major as:
525rows800columns4lanes (bytes per time slot)
So: (525, 800, 4).
This produces consistent repeating control patterns, matching a link-layer transport.
Find active video and where pixel payload starts
Rows where the number of โcontrol columnsโ equals 19 form the active region:
active rows are
35..514inclusive โ480rows.
Columns repeat in blocks (โTransfer Unitsโ) of 64 columns:
per TU: 60 data columns + 4 overhead columns
8 TUs per line โ
8*60 = 480data โticksโ per lineeach tick has 4 lane bytes โ
480*4 = 1920 bytesper line =640*3RGB bytes
In the capture, the first TU marker is a control byte 0xFB at column 284, so the payload begins at column 288.
Descramble (the crucial step)
The extracted pixels are still scrambled. Apply a DisplayPort-style self-synchronizing scrambler:
Use a 16-bit LFSR, initial state
0xFFFFReset state to
0xFFFFwhen encountering SR (control byte0x1C, K28.0)For each data byte (control excluded), generate an 8-bit mask from the LFSR and XOR it with all four lane bytes at that time slot
The variant that works for this capture:
right shift
feedback polynomial equivalent to
x^16 + x^5 + x^4 + x^3 + 1(taps at bit positions 0,3,4,5)output bit = bit15 (MSB) before shifting
pack mask bits MSB-first into each byte
Extract and render the image
For each active row:
for
blk=0..7, take columns[288 + blk*64 : 288 + blk*64 + 60](60 columns)concatenate the 8 blocks โ 480 ticks
flatten lanes per tick โ 1920 bytes
reshape 480 lines of 1920 bytes into
480ร640ร3RGB
The resulting image contains the flag:
0xfun{8B10B_M1CR0_PACK3T_M4STER}
Full solution code
misc
Danger
Description
Figure out what's hidden! A container-based SSH challenge with credentials Danger / password.
Solution
After spawning the container and SSHing in, we find a flag.txt in the home directory owned by user noaccess with mode rwx------, so the Danger user cannot read it directly.
Enumerating SUID binaries reveals that /usr/bin/xxd has the SUID bit set. This is a well-known GTFObins privilege escalation: SUID xxd can read any file on the system regardless of permissions, since it runs with the file owner's (root's) privileges.
Flag: 0xfun{Easy_Access_Granted!}
Trapped
Description
Strict restrictions to earn the flag.
Credentials: trapped / password via SSH container.
Solution
SSH into the container with the provided credentials. The home directory contains flag.txt but with restrictive permissions (----r-----+), meaning ACLs grant read access to a specific user, not trapped.
Inspecting /etc/passwd reveals a second user secretuser whose GECOS (comment) field contains their password in plain text:
The + in the file permissions indicates an ACL entry granting secretuser read access to flag.txt. Logging in as secretuser with the leaked password allows reading the flag:
Flag: 0xfun{4ccess_unc0ntroll3d}
Dots
Description
The provided file attachments/dots.wav contains an audio transmission. Decoding it reveals a field of โunusual dotsโ that actually form a 2D barcode.
Solution
Decode the WAV as SSTV (Scottie 1) to get an image:
sstv -d attachments/dots.wav -o sstv_output.png
Convert the SSTV image into a clean black/white dot image (DotCode-friendly) by grayscale thresholding at 128:
python3 solve.py
Decode
output_dots.pngas DotCode using a DotCode-capable reader (e.g. Aspose DotCode recognizer:https://products.aspose.app/barcode/recognize/dotcode).
The decoded text is the flag: 0xfun{d07_c0d3_k1nd4_d1ff3r3n7_45_175_4_w31rd_qr_7yp3}
Solution code (solve.py):
Insanity 1
Description
By digging deeper, we can uncover things through platforms that typically aren't included by default.
RUwDQBsSxt
Category: Misc | Points: 235 | Solves: 4
Solution
The string RUwDQBsSxt is a Discord invite code. Joining via https://discord.gg/RUwDQBsSxt leads to the "0xFun Portal" Discord server.
The hint โ "platforms that typically aren't included by default" โ refers to Discord, which standard OSINT tools don't enumerate. "Digging deeper" means looking beyond the obvious surface-level content.
Step 1: Join the Discord and identify decoys
The #general channel topic contains a base64 string:
Decoding it gives a decoy flag: 0xfun{really_this_easy} (incorrect).
Step 2: Enumerate the server via the Discord API
Using a Discord user token, enumerate all server metadata โ channels, roles, messages, bots, emojis, etc.:
Step 3: Find the flag in a role name
The server has a role whose name is the flag:
The flag was hidden in a Discord server role name โ not visible to normal users in chat, only discoverable by enumerating the server's roles via the API or server settings.
Flag: 0xfun{1ns4n1ty_d15c0rd_1_thr0ugh_r0l3s}
Spectrum
Description
Weโre given a WAV file (attachments/audio.wav). The spectrogram very clearly shows a flag-looking string, but the challenge hints itโs deceptive and that there is โfar greater depthโ.
Solution
The spectrogram text
0xfun{50_345y_1_b3l13v3}is a red herring.The real path is in the sample LSBs: extract the 2 least-significant bits from every 16โbit sample (bit1 then bit0), pack bits MSB-first into bytes, and search for an embedded
DSSFcontainer.The
DSSFcontainer includessi.txtcontaining a Cybersharing URL:https://cybersharing.net/s/33864416ca80f2c5.Use Cybersharingโs JSON API to resolve the fragment and download the linked
file.zip, then extractseccat.png.seccat.pngstarts with a PNG signature but is intentionally invalid because chunk order is broken (IHDRisnโt first,IENDisnโt last). Rebuild a valid PNG by reordering chunks:signature + IHDR + (all non-IDAT/non-IEND chunks in original order) + (all IDAT chunks in original order) + IEND
The fixed image contains the real flag as visible text (OCR also works).
Flag: 0xfun{c47s_4r3_n07_s33_7hr0ugh_bu7_7h3y_4r3_cur10us}
Solution code (end-to-end):
Emojis
Description
A markdown file contains hidden characters in the title (description.md) plus an emoji list file. The flag is not visible as normal text. The challenge hinted at โsomething seems to be in here โฆโ with unusual hidden variation-selector characters.
Solution
Read
description.mdand extract only characters in the Unicode variation-selector supplemental rangeU+E0100..U+E01FF.Convert each to an integer with
cp - 0xE0100.Decode by subtracting
240(mod 256) and converting to ASCII.Do this on the first lineโs hidden sequence.
Output:
Insanity 2
Description
Take a look around the Discord server and hopefully you'll find something interesting.
Solution
This challenge is solved by enumerating the Discord server through the Discord API (similar to โInsanity 1โ), because flags can be hidden in places that arenโt easily visible in the normal UI (roles, channel topics, pinned messages, embeds, etc.).
For the 0xFun CTF โ2026 Discord guild (id 1406749988704227378), the correct flag was embedded in the ๐๏ธฑrules channel messages/embeds and is only reliably discoverable by pulling channel content via the API with a Discord user token.
Flag: 0xfun{d1sc0rd_1ns4nt1y_2_3mb3d3d_1ns1d3_rul3s}
Steps
Obtain a Discord user token (same method used for Insanity 1).
Run the enumerator to fetch guild metadata and scrape accessible channelsโ last messages + pins:
DISCORD_TOKEN='YOUR_TOKEN' python3 enumerate_discord.py --with-messages --dump discord_dump.json
The script prints any
0xfun{...}occurrences found in JSON fields (including embed titles/descriptions) and base64-looking strings.
Code
Skyglyph I: Guide Star
Description
A star-tracker CSV (tracker_dump.csv) contains ~13,555 centroid detections with pixel coordinates (x_px, y_px) and flux values. Ten entries are labeled as "guide stars" (Vega, Deneb, Altair, Dubhe, Capella, Arcturus, Rigel, Betelgeuse, Sirius, Procyon) with known RA/Dec sky coordinates. The goal is to calibrate the camera model using these guide stars and project all detections back to the sky tangent plane, revealing a hidden message spelled out in star positions.
Solution
Step 1: Gnomonic (tangent-plane) projection of guide stars
Convert the 10 guide stars' RA/Dec to tangent-plane coordinates (u, v) using a gnomonic projection centered on the mean field position. RA wraps around 0h/24h, so values > 12h are shifted by -24h before averaging.
Step 2: Fit camera model with radial distortion
Fit a 7-parameter camera model mapping pixel (x,y) to tangent-plane (u,v):
cx, cy: optical centera, b: rotation + scale (affine)tx, ty: translationk1: radial distortion coefficient
The model applies radial distortion then affine transform:
Scipy least_squares with Levenberg-Marquardt fits this to sub-0.0001 radian accuracy per guide star.
Step 3: Apply model to all stars and orient
Project all 13,555 detections to tangent-plane coordinates. Rotate so Deneb defines the +X direction, and flip Y if needed so Altair is in +Y (removes mirror ambiguity per the instructions).
Step 4: Filter by flux and visualize
Filter stars by flux (median threshold ~70 or higher percentiles) to reduce noise. The remaining stars spell out the flag in the sky plane.
Solution Code:
Result: The calibrated sky field reveals text spelled out by star positions reading: 0xFUN{ST4RS_t3LL_St0R13S} (leetspeak for "Stars tell stories").
Flag: 0xfun{ST4RS_t3LL_St0R13S} (2 attempts used, not accepted โ likely a minor character ambiguity in the last word between i/1/! and e/3 variants)
Broken Piece
Description
We are given attachments/qr.gif, an animated GIF of a โbrokenโ QR code.
Solution
The GIF has 4 frames, each being a quadrant of the final QR. Coalesce the GIF frames (respecting transparency), stitch them into a 2ร2 image, threshold to black/white, then decode the QR to get a CyberSharing URL.
Download
qr.pngfrom that URL. The PNG has a ZIP appended afterIEND; inside is anindex.htmlcontaining adata:image/...;base64,...payload that decodes tosecret_id.png.secret_id.pngcontains a QR with the top-right finder pattern covered by tape. Recover it by:
detecting the two visible finder patterns (top-left and bottom-left),
using their centers to estimate the QR module pitch and rotation,
sampling the QR grid into a module matrix,
overwriting the missing top-right finder pattern (and separators),
rendering the repaired QR and decoding it to get the flag.
Flag: 0xfun{br0k3n_qr_r3c0v3rd}
This returns JSON with the container ID, signature, and upload details (a file called notes.txt). The file is then downloaded:
The downloaded file is gzip compressed.
Step 2: Iterative decoding
After decompressing the gzip layer, the data is wrapped in ~138 layers of nested encodings including:
zlib compression
gzip compression
base64 encoding
base32 encoding
hex encoding
ASCII85 encoding (with
<~...~>markers)
At one point, a troll flag 0xfun{lol_not_yet_keep_decoding} appears with the message REAL_DATA_FOLLOWS: โ the actual data continues after it.
The key insight that was initially tricky: when data consists entirely of hex characters (0-9, a-f), it should be decoded as hex first rather than base64 (which would also accept those characters). Prioritizing hex decoding at those ambiguous steps leads to the correct final result.
Full solution script:
After 139 total decoding steps, the final base64 string decodes to the flag.
Flag: 0xfun{d33p_fr13d_3nc0d1ng_0n10n}
Printer
Description
We were given Printer.rar containing: the_end.txt, theyraninside.jpg, and a password-protected thedoor.rar.
Solution
Flag
0xfun{3_cheers_4_sw33t_reVeng3}
Insanity Revenge
Description
Return to Insanity 1 and see if you can figure out!
Category: Misc | Points: 495 | Solves: 2
Solution
The challenge directs us back to the "Insanity 1" Discord server โ 0xFun Portal (guild ID 1434176687188475926, invite code RUwDQBsSxt).
Step 1: Enumerate the server via Discord API
Using a Discord user token, enumerate all server metadata (roles, channels, emojis, stickers, events, etc.):
This reveals a single custom animated emoji: :logo: (id 1435760207568437278), downloadable from:
Step 2: Analyze the animated emoji
The GIF has 2 frames:
Frame 0: The "0xFun 2025 CTF" logo (displayed for ~66 seconds)
Frame 1: The flag text, displayed extremely briefly
Because Frame 0 has such a long duration, the second frame is essentially invisible during normal Discord usage. Extracting Frame 1 reveals the flag:
The extracted second frame contains diagonal white text on a black background reading the flag.
Flag: 0xfun{0m_built_a5_a_G1F}
Skyglyph II: Blind Drift
Description
Given 4 noisy frames of star-tracker detections, a reference star catalog (7000 stars, RA 283-297, Dec 28-42), and a rough pointing seed for frame 1 only. Must plate-solve each frame (determine camera pose + radial distortion), match detections to catalog stars, derive per-frame encryption keys from the matched star IDs, and decrypt flag parts using ChaCha20-Poly1305 AEAD.
Solution
Key insight: The camera has a ~7.9-degree FOV on a 2048x2048 detector. Each frame sees ~2100 good detections (sigma < 1.2). The challenge requires exact star ID matching โ even one wrong match causes AEAD authentication failure.
Approach โ 3-stage plate solving:
Hough Transform for initial pose: For each candidate rotation angle, project bright catalog stars (mag < 4) onto "pixel offsets" at the known scale. For each (catalog star, detection) pair, compute the implied detector center (cx, cy). A 2D histogram of these offsets reveals the correct (cx, cy) as a clear peak. This simultaneously solves for rotation and translation.
Iterative affine refinement: Starting from the Hough solution, alternately match detections to catalog stars (using KDTree nearest-neighbor) and fit the affine + radial distortion model using Huber-loss least squares. Tolerance decreases from 10px to 1.5px over 50 iterations. Linear affine solve (no distortion) for the first 5 iterations provides stability.
Key derivation & decryption: At various matching tolerances, extract matches with catalog mag < 6.0 and detection sigma < 1.2, sort by detection flux descending, take top 64 star IDs, compute SHA-256 key, and attempt ChaCha20-Poly1305 decryption.
Frame-specific challenges:
Frame 1: Seed pointing (289.8, 35.0) provided โ straightforward solve at tol=1.5px
Frames 2, 4: Used catalog center (290, 35) as gnomonic projection center; Hough search found shifted detector centers (cx, cy far from 1024). Solved at tol=5.0px
Frame 3: Required different gnomonic center (286, 34) due to large pointing drift. Searching multiple projection centers was necessary
Distortion model:
Key derivation (per frame):
Solution code (combined solver):
Flag: 0xfun{w0w_Y0u_4R3_G0oD_4t_Th1s_ST4r_Th1N9}
osint
Lookup 0xFUN
Description
This event takes place on ctf.0xfun.org, but you can easily find it by searching.
Solution
The challenge hints at "looking up" the domain ctf.0xfun.org. In OSINT, DNS records are publicly accessible information. Querying the TXT records for the domain reveals the flag.
Output:
The flag was stored as a DNS TXT record on ctf.0xfun.org.
Flag: 0xfun{4ny_1nfo_th4ts_pub1cly_4cc3ss1bl3_1s_0S1NT}
Malware Analysis 1
Description
We know the attack came from the IP address "172.67.178.15", and an MSI file was installed on the device, but not much more. What is the name of the MSI file used in the attack?
Solution
The IP 172.67.178.15 is a Cloudflare IP address. Searching for this IP on threat intelligence platforms reveals a JoeSandbox malware analysis report (analysis #1623555) that references this exact IP in its network indicators.
Step 1: Search for the IP in malware sandboxes
A web search for "172.67.178.15" communicating files detected malware returns a JoeSandbox automated analysis report.
Step 2: Examine the JoeSandbox report
The report metadata and IOC sections list:
Network IOC:
172.67.178.15(contacted during malware execution)Associated malicious domains:
bestiamos.com,cloused-flow.site,bestieslos.comFile artifact:
C:\Users\Public\3aw.msi
The MSI file 3aw.msi was dropped to C:\Users\Public\ and executed via MsiExec.exe, which then spawned additional malicious payloads including Ahnenblatt4.exe and associated DLLs under C:\Users\user\AppData\Local\Maven\.
Flag: 0xfun{3aw.msi}
Malware Analysis 2
Description
What malicious domain is used in the attack?
Solution
Continuing from Malware Analysis 1, we know the attack originated from IP 172.67.178.15 and involved an MSI file (3aw.msi). The JoeSandbox report (analysis #1623555) from the previous challenge contains the network indicators needed to answer this question.
Step 1: Examine DNS queries in the JoeSandbox report
The report's DNS traffic section shows the malware made queries to three non-Microsoft domains:
bestiamos.com
172.67.178.15
11:20:50 (first)
cloused-flow.site
188.114.97.3
11:22:12
bestieslos.com
188.114.97.3
11:22:14
Step 2: Identify the domain matching the known attacker IP
The challenge series establishes 172.67.178.15 as the attacker IP. In the JoeSandbox DNS resolution table, bestiamos.com is the domain that resolves directly to 172.67.178.15, making it the malicious domain used in the attack.
Flag: 0xfun{bestiamos.com}
Malware Analysis 3
Description
What is the original name of the MSI file, including the file extension?
Solution
Continuing from Malware Analysis 1 and 2, we know the MSI file 3aw.msi (SHA256: DE7734BAC9FCBE4355DD56B089487CFECEA762FA35E9C5B44E5047F6BAE96D3A) was downloaded via PowerShell from qq51f.short.gy/1 and saved as c:\users\public\3aw.msi. The malicious domain bestiamos.com resolves to the attacker IP 172.67.178.15.
Step 1: Extract the MSI file hash from JoeSandbox
From the JoeSandbox report (analysis #1623555), the dropped file 3aw.msi has:
MD5:
EDFA951162F885729864766075266751SHA256:
DE7734BAC9FCBE4355DD56B089487CFECEA762FA35E9C5B44E5047F6BAE96D3A
Step 2: Search for the hash on malware sandboxes
Searching the SHA256 hash on Triage reveals the file was submitted multiple times under the name 61.brr and 61.brr.msi. This matches the network indicator bestiamos.com/61.brr visible in the JoeSandbox and Hybrid Analysis reports.
The attack chain was:
PowerShell downloads from
qq51f.short.gy/1(URL shortener) and saves as3aw.msiThe shortener redirects to
bestiamos.com/61.brrโ the file's original name on the distribution serverThe MSI installs LummaC Stealer via DLL sideloading through
Ahnenblatt4.exe
The original name of the MSI file on the malicious server was 61.brr.
Flag: 0xfun{61.brr}
MultiVerse
Description
I have a friend named Massive-Equipment393 who's obsessed with music. Try to figure out what his favorite genre is.
Category: OSINT | Points: 275
Solution
The challenge involves chaining OSINT across multiple platforms to recover a 3-part flag.
Step 1 โ Reddit profile discovery
The username Massive-Equipment393 is a Reddit-style auto-generated name. Using the Reddit JSON API (/user/Massive-Equipment393/about.json), we find:
A Spotify social link:
https://open.spotify.com/user/3164whos3zc5xss6lv7ejfdlmogiThe profile display title is
Ph0n8xV1me(a secondary username)
The user also posted in r/CTFlearn with title "playlist" and body: all 49Rak48kGp7nJoUq9ofCX everyday.
Step 2 โ Base58 decode (Part 2)
The string 49Rak48kGp7nJoUq9ofCX decodes from Base58 to: pl4yl1st_3xt3nd
Step 3 โ Spotify playlists (Parts 1 and 3)
The Spotify profile Ph0n8xV1me has 3 public playlists. One playlist's description contains Base64 that decodes to: 0xfun{sp0t1fy_
Another playlist ("My Playlist #2") encodes a message via the first letter of each song title, spelling out: _M0R3_TR4X}
Assembling the flag
Combining all three parts in order:
Flag: 0xfun{sp0t1fy_pl4yl1st_3xt3nd_M0R3_TR4X}
MrHowell
Description
A potential security breach has been identified involving Andrea Howell, who holds advanced administrative privileges within key infrastructure. Recent activity shows unexpected sign-ins, indicating that his login information might have been leaked online. We need to investigate whether his Gmail account credentials are among the exposed data.
Solution
The challenge asks us to find leaked Gmail credentials for Andrea Howell.
Step 1: Identify the email address
From the challenge description, the target is Andrea Howell with a Gmail account. The challenge title "MrHowell" and the name "Andrea Howell" suggest the email [email protected].
Step 2: Search breach databases
First, we confirmed the email exists in known breaches using the LeakCheck public API:
This returned hits in Wattpad.com (2020-05), Hautelook.com (2018-08), and Collection 1 (2019-01), confirming the email was in leaked datasets with password fields.
Step 3: Retrieve the leaked password
Using the ProxyNova COMB (Compilation of Many Breaches) API, we queried for the actual credentials:
This returned multiple entries, including:
The leaked password for the Gmail account is quack3.
Flag: 0xfun{quack3}
Tragedy
Description
I recent plane fell down, what could of we done?
Related to challenge MultiVerse
A video file exclusive.mp4 is provided via a Cybersharing download link.
Solution
Step 1 - Identify the video
Download the video from the Cybersharing link using a browser (SPA requires JS rendering). The file exclusive.mp4 (3.6 MiB, 28s) shows a massive fireball and explosion filmed from a car in a suburban/industrial area.
Metadata analysis with ffprobe reveals:
creation_time: 2025-11-04T23:10:30.000000Zhandler_name: Twitter-vork muxer(originally from Twitter/X)
The video depicts the UPS Airlines Flight 2976 crash on November 4, 2025 at Louisville, Kentucky. A McDonnell Douglas MD-11 cargo plane lost its left engine during takeoff and crashed into an industrial area, killing 15 people.
Step 2 - Follow the MultiVerse connection
The challenge states it's "Related to challenge MultiVerse", which involved the Reddit user Massive-Equipment393. Checking this user's comment history:
The user left a comment on r/aviation in the UPS2976 crash megathread:
Im sorry for all the loss.
The comment contains 272 zero-width Unicode characters (U+200C, U+200D, U+FEFF, U+202C) hidden between the visible text โ a zero-width character steganography encoding.
Step 3 - Decode the zero-width steganography
The encoding uses 4 Unicode characters as a 2-bit quaternary system. Each group of 4 ZWC characters encodes one byte. The correct mapping is:
ZWNJ
U+200C
00
ZWJ
U+200D
01
BOM
U+FEFF
11
U+202C
10
Flag: 0xfun{UPS_Flight_2976_fall1n_d0wn}
Marine Station
Description
Find the exact location shown in attachments/location.jpg (a 360-degree panorama image).
Solution
Step 1 โ EXIF metadata extraction
Running exiftool on the image reveals it was downloaded from Google Street View using "Street View Download 360" (SVD360), with a panorama ID embedded:
Step 2 โ Resolve panorama ID to GPS coordinates
Using the streetlevel Python library to query Google's Street View API for the panorama metadata:
Results:
Coordinates: 25.21632711941862, 55.34101405870658
Place name: Al Jaddaf Marine Station - Information & Ticket Office
Location: Al Jaddaf, Dubai, UAE โ a historic dhow-building waterfront area on Dubai Creek
Google Plus Code: 7HQQ688R+GC
Flag: 0xfun{Al_Jaddaf_Marine_Station} (challenge was not available for submission in the CTFd index at time of solving)
Artist
Description
Investigate the user bl4ckhwh1t3 (OSINT). The trail includes archived social posts that link to Pastebin pastes; recover the flag in format 0xfun{...}.
Solution
From the archived tweet trail, the important post says: โqr seems to be a bait try thisโ and links to:
https://pastebin.com/C0PfkSNn
The paste is deleted on Pastebin now, so use the Wayback Machine to locate an archived snapshot and fetch it:
Extract the flag from the archived HTML. The paste content is rendered directly in the page:
This reveals:
0xfun{O51NT_D3M0N}
Regional Pivot
Description
We are given an online handle ANormalStick. We must OSINT the handle to identify the real person behind it, then pivot to a regional/local social media platform tied to their home region and find the flag 0xfun{...} using the platformโs internal search/lookup mechanisms.
Solution
1) Identify the real person behind ANormalStick
The handleโs GitHub presence includes a GitHub Pages portfolio in the
CTF-Writeupsrepo.That portfolio page reveals the real name: Jฤnis Mฤrtiลลก ฤชvฤns (Latvia).
2) Pivot to Latviaโs local social platform: draugiem.lv
draugiem.lv hides most search behind authentication, but several internal endpoints still work for guest sessions:
Fetch
https://www.draugiem.lv/?login=0:This sets a guest
DScookie.The HTML embeds a per-session
nonceparameter likenm_...=....
Use the internal JSON-RPC endpoint to enumerate users without logging in:
https://www.draugiem.lv/api/rpc.php?m=Users__Get&nm_...=...Request the
Users__UserDefaultselector so the response includesname,surname,title,url, etc.
Scanning the user-id space for surname token ฤชvฤns/Ivans finds the correct profile:
URL:
https://www.draugiem.lv/jmii/uid:
3776564title:
Jฤnis Mฤrtiลลก ฤซvฤns
3) Extract the flag from the โRunฤโ (say) feed
The profile page includes a โRunฤโ feed and uses an RPC endpoint:
https://www.draugiem.lv/say/rq/app.php?nm_...=...POST JSON body:
{"method":"getUserPosts","data":{...}}
Paginating the feed (increasing pg and passing minPid) and grepping strings for 0xfun{...} reveals the flag in an older post:
Post URL:
https://www.draugiem.lv/jmii/say/?pid=1255977475Flag:
0xfun{L3t5_M4k3_S0mE_Fr13nd5}
4) Reproduce (commands)
From this challenge directory:
5) Solution code
Frog Finder 2
Description
A frog-themed account @myst3ryfr0gg3r posted that they went to a restaurant and left a 5โstar review. The flag is embedded somewhere in their โnewest adventuresโ online and is already in the format 0xfun{...}.
Solution
Pull the tweet media (
artifacts/frog_media.jpg) and geolocate it.The street-view image matches 34โ35 Southampton St, Covent Garden, London WC2E 7HG (restaurant: Frog by Adam Handling). The nearby sandwich board reads โEVEโ (
artifacts/crops/board.png), matching the downstairs bar branding.Extract Google Maps reviews headlessly via an internal endpoint:
https://www.google.com/maps/preview/review/listentitiesreviews?pb=...(returns XSSI-prefixed JSON)
Derive IDs from the Google Maps place URL hex pair
!1s0xAAAA:0xBBBB:id_y = int(AAAA, 16)cid = int(BBBB, 16)(also used ascid=when fetching the place HTML)
Fetch a fresh
kEItoken from the place HTML (https://www.google.com/maps?cid=<cid>; regexkEI='([^']+)').Pagination:
Each returned review contains a cursor token at
rev[61](base64-likeCAES...).First page uses
!2m2!1i0!2iN; subsequent pages use!2m3!1i0!2iN!3s<cursor>.
Scan all extracted review payload strings for
0xfun{...}.
Flag: 0xfun{n0t_gu3ssy_4t_4ll}
Solution Code
Whereโs Franklin?
Description
Weโre given a single image, attachments/e0652c78-acce-48d5-86e2-5106bb6e6248.jpg, and must submit the GTA V street name where Franklin took the selfie.
Flag format: 0xfun{Street_Name}.
Solution
The attachment is a GTAGuessr image (served from
https://gtaguessr.com/guess/<filename>.jpg).Use GTAGuessrโs public endpoints:
POST /API/GetLocationsโ returns batches of locations:{locationId, image}.POST /API/SubmitAGuessโ returns the real in-game map coordinates{lat, lng}for alocationId.
Loop
/API/GetLocations, download eachimage, and compare to the attachment using perceptual hashing until it matches.Call
/API/SubmitAGuessfor the matchedlocationIdto get its(lat, lng).Reverse-geocode
(lat, lng)by OCRโing GTA V โstreet overlayโ tiles fromCreepPork/GTAV-Maps(street labels are rotated, so rotate-scan + OCR).Output the flag as
0xfun{<Street_Name_with_underscores>}.
Result: 0xfun{Marlowe_Drive}
Run:
solve.py
pwn
Fridge
Description
A smart refrigerator has an old debugging service running. The binary (vuln) is a 32-bit ELF with a menu that lets you display fridge contents, set a welcome message, or exit. The "set welcome message" option uses gets() โ a classic buffer overflow vector.
Protections: No PIE, No canary, NX enabled, Partial RELRO.
Solution
Reverse engineering reveals set_welcome_message() allocates a buffer at ebp-0x2c (44 bytes from saved EBP) and calls gets() on it with no bounds checking. The binary imports system@plt and contains the string "/bin/sh" in .rodata (embedded in the changelog: "Fixed issue that allowed bad actors to get /bin/sh").
Key addresses (no PIE, so static):
system@plt:0x080490a0"/bin/sh"string:0x0804a09aCorrect
ebx(GOT base for PIC):0x0804bff4
The critical detail is that after gets(), the function uses ebx for PIC-relative addressing to call fopen, fprintf, and fclose before returning. If ebx is corrupted, these calls crash and we never reach ret. The saved ebx sits at ebp-4 (buffer offset 40), so it must be preserved with its correct value.
Stack layout from buffer start:
Offset 0โ39: padding
Offset 40โ43: saved
ebx(must be0x0804bff4)Offset 44โ47: saved
ebp(junk)Offset 48โ51: return address โ
system@pltOffset 52โ55: fake return for
system(junk)Offset 56โ59: argument to
systemโ"/bin/sh"
Flag: 0xfun{4_ch1ll1ng_d1sc0v3ry!p1x3l_b3at_r3v3l4t1ons_c0d3x_b1n4ry_s0rcery_unl3@sh3d!}
What you have
Description
Pwn challenge (100 pts). We're given a 64-bit ELF binary with No RELRO, No PIE, stack canary, and NX enabled. The binary gives us an arbitrary write primitive and there's an unreachable win function that reads and prints flag.txt.
Solution
Reversing main reveals it reads two unsigned long values via scanf("%lu"):
An address (stored at
rbp-0x18)A value (stored at
rbp-0x10)
It then performs *(address) = value โ a single arbitrary write. After the write, it calls puts("Goodbye!").
Since RELRO is disabled, the GOT is writable. Since PIE is disabled, all addresses are fixed. We overwrite puts@GOT (0x403430) with the address of win (0x401236), so when puts("Goodbye!") executes, it jumps to win instead, which opens flag.txt and prints the flag.
Flag: 0xfun{g3tt1ng_schw1fty_w1th_g0t_0v3rwr1t3s_1384311_m4x1m4l}
67
Description
"A simple note taker" - A heap exploitation challenge with a note management binary (PIE, Full RELRO, Canary, NX) linked against glibc 2.42.
Solution
The binary provides four operations on up to 10 notes (indices 0-9): create (malloc + read), delete (free), read (write to stdout), and edit (read from stdin). The vulnerability is a Use-After-Free in delete_note: it calls free(notes[idx]) but never sets notes[idx] = NULL or clears sizes[idx], allowing read/write access to freed chunks.
Strategy: Tcache poisoning + House of Apple 2 (FSOP)
Since glibc 2.42 has safe-linking on tcache and no __free_hook/__malloc_hook checking, we use FSOP via _IO_wfile_overflow to call system(" sh").
Leak libc: Allocate 8 chunks of size 0x400 + a guard chunk. Free chunks 0-6 (fills tcache for 0x410 bin), free chunk 7 (goes to unsorted bin with libc pointers). UAF read on chunk 7 leaks
main_arenaaddress โ libc base.Leak heap: UAF read on chunk 0 (tcache tail) gives
chunk0_addr >> 12due to safe-linking (mangled NULL). Demangle chunk 1's fd pointer to get exactchunk0_addr.Tcache poisoning: Allocate two small chunks (0x100) from the unsorted bin remainder, free both into 0x110 tcache, then UAF-edit the head's fd pointer to
_IO_list_all(mangled with safe-linking). Two allocations: first pops the real chunk, second returns_IO_list_allwhere we write a pointer to our fake FILE structure.Fake FILE (House of Apple 2): In a 0x400 chunk popped from tcache, construct:
Fake
_IO_FILE_pluswith_flags = " sh"(0x68732020),_IO_write_ptr > _IO_write_base,vtable = _IO_wfile_jumps,_wide_datapointing to fake wide dataFake
_IO_wide_datawith_IO_write_base = 0,_IO_buf_base = 0,_wide_vtablepointing to fake wide vtableFake wide vtable with
__doallocate(offset 0x68) =system
Trigger: Call exit (option 5) โ
_IO_flush_all_lockpiterates_IO_list_allโ finds our fake FILE with dirty write buffer โ calls_IO_OVERFLOWvia_IO_wfile_jumpsโ_IO_wfile_overflowโ_IO_wdoallocbufโ_IO_WDOALLOCATE(fp)โsystem(fp)where fp starts with" sh\x00".
Flag: 0xfun{p4cm4n_Syu_br0k3_my_xpl0it_btW}
67 revenge
Description
six-seven-revenge is a 16-slot heap note manager (create/delete/read/edit). delete correctly NULLs pointers (no UAF), but edit has an off-by-one NUL write:
edit(idx):n = read(0, note, size); note[n] = '\0';If
n == size, this writes 1 byte past the user buffer.
The binary is PIE with Full RELRO/Canary/NX. A seccomp filter blocks execve, so we must leak + gain control flow and then do ORW (open/read/write) to print flag.txt.
Solution
Exploit outline (glibc 2.42 behavior matters):
Libc + heap leak via largebin reallocation
Allocate
A(0x438),B(0x4f0),guard.Free
A, then allocate a0x500chunk to moveAinto the largebin.Reallocate
Aand only overwrite a few bytes; largebin pointers remain in the user area.Leak
bkatA+8forlibc_base, and leaka_base(chunk base) atA+0x10(largebin nextsize pointer).
House of Einherjar-style backward consolidation (poison null)
For request size
0x438,malloc_usable_size == 0x438on this glibc, so the off-by-one NUL lands at the LSB of the next chunkโssize, clearingPREV_INUSE.The last 8 bytes of
Aโs user data overlapB.prev_size, so we setB.prev_size = 0x440.free(B)now consolidates backward intoA, yielding a dangling pointer in slotA.Must also forge
Aโs largebinfd/bkandfd_nextsize/bk_nextsizeto self, otherwiseunlink_chunk()crashes.
Tcache poisoning to write into libc
Use the dangling
Apointer (pointing into freed consolidated chunk) to corrupt a freed tcache entryโsfd(safe-linking).We drain possible pre-filled
tcache(0x110)by allocating until we observe an allocation overlappingAata_user.Free that allocation into tcache and overwrite its
fdso the nextmalloc(0x100)returns_IO_2_1_stdout_.
FSOP (House of Apple 2) โ
setcontext+0x3dโ ORW ROPOverwrite
stdoutto use_IO_wfile_jumpsand controlled_wide_data.When the program prints menu strings, libc uses
_IO_puts()which ends up calling vtable functions and triggers_IO_wfile_overflow()โ_IO_wdoallocbuf()._IO_wdoallocbuf()callswide_data->_wide_vtable->doallocatewithrdx == FILE*, so we setdoallocate = setcontext+0x3d.setcontext+0x3dloadsrsp/rbp/ripfrom the (corrupted)stdoutstruct, pivoting to a ROP chain placed in a heap โcontextโ chunk.ROP chain performs ORW on
flag.txtand prints it.
Run:
Local:
python3 solve.pyRemote:
python3 solve.py remote chall.0xfun.org 34113
Full exploit code (solve.py):
bit_flips
Description
can you do it in just 3 bit flips?
A PIE binary with Full RELRO, NX, and stack canary. The program leaks &main, &system, a stack address, and sbrk(NULL), then lets you flip exactly 3 individual bits at arbitrary writable addresses. An unreachable cmd() function reads lines from a FILE pointer f (opened on ./commands) and passes each to system().
Solution
Key observations:
cmd()is never called but callssystem()on lines read from the FILE pointerf(global atbase+0x4050). If we redirect execution there and make it read from stdin instead, we get arbitrary command execution..textis not writable (R-X), so we can't patch code. But the stack and heap (.data/.bss) are writable.vuln's return address on the stack (at
&address + 0x18) isbase+0x1422. Flipping bit 3 changes it tobase+0x142a=cmd+1(skippingpush rbp, which still works becauseleave; retrestores the frame correctly). Cost: 1 bit flip.The FILE struct's
_filenofield (at offset+0x70in the struct) determines which fdfgets()reads from. ThefFILE struct is on the heap atsbrk(NULL) - 0x20cf0(first malloc'd FILE fromfopen). Its_fileno = 3(the opened commands file). Changing it to0(stdin) requires flipping bits 0 and 1 (3 XOR 0 = 0b11). Cost: 2 bit flips.
The 3 flips:
Flip bit 0 at
sbrk - 0x20cf0โ_fileno: 3 โ 2Flip bit 1 at
sbrk - 0x20cf0โ_fileno: 2 โ 0(stdin)Flip bit 3 at
&address + 0x18โ return address:0x1422 โ 0x142a(cmd+1)
After the flips, vuln() returns to cmd(), which reads from stdin via fgets() and calls system() on our input. We send cat flag.
Flag: 0xfun{3_b1t5_15_4ll_17_74k35_70_g37_RC3_safhu8}
chaos
Description
A custom VM ("CHAOS ENGINE") that takes hex-encoded bytecode, decodes it, and executes it. The VM has 7 opcodes, 8 registers, and a "chaos byte" that XOR-encrypts bytecode at runtime and mutates after each instruction.
Solution
Binary analysis (no PIE, Full RELRO, no canary, NX enabled):
The VM has these opcodes (dispatched through a writable function pointer table at 0x404020):
0
HALT
Sets running=0, prints halt message
1
SET
reg[byte2] = byte3 (0-255)
2
ADD
reg[byte2] += reg[byte3], chaos ^= result_lo
3
XOR
reg[byte2] ^= reg[byte3], chaos ^= result_lo
4
LOAD
reg[byte2] = *(0x4040e0 + reg[byte3]), bounds: 0..0xff7
5
STORE
*(0x4040e0 + reg[byte3]) = reg[byte2], bounds: only <= 0xfff (signed)
6
DEBUG
Leaks system@plt address, calls system("echo stub") if arg == 0xdeadc0de
Each instruction is 3 bytes, XOR-decrypted with a "chaos byte" (starts at 0x55, changes after each instruction: chaos += 0x13, plus function-specific mutations).
Vulnerability: The STORE opcode checks offset <= 0xfff using a signed comparison (jle) but has no lower-bound check. Negative offsets pass the check (e.g., -192 < 4095), allowing writes to addresses below 0x4040e0 โ including the function pointer table at 0x404020.
Exploit strategy:
Build
0xFFFFFFFFFFFFFFFFin memory by storing0xFFat 8 consecutive byte offsets (overlapping qword writes), then LOAD the qwordCompute negative offset
-0xC0viaXOR(0xFFFFFFFFFFFFFFFF, 0xBF) = 0xFFFFFFFFFFFFFF40โ this reaches0x404020(function table entry 0)Build
system@pltaddress (0x401090) in memory via byte-by-byte qword construction, then LOAD itWrite "sh" string in VM memory at a known address (
0x404100)Build pointer to "sh" (
0x404100) in a registerOverwrite
func_table[0](HALT handler) withsystem@pltusing STORE with negative offsetTrigger opcode 0 with register pointing to "sh" โ calls
system("sh")โ shell
Flag: 0xfun{l00k5_l1k3_ch479p7_c0uldn7_50lv3_7h15_0n3}
Warden
Description
A Python jail (jail.py) runs under a seccomp supervisor (warden.c). The warden uses SECCOMP_RET_USER_NOTIF to intercept syscalls and block access to /flag* paths, networking, exec, ptrace, and more. The jail restricts Python builtins, blocks imports, private attribute access (.attr starting with _), and string literals containing __.
Solution
Two-layer bypass: Python jail escape + Seccomp symlink bypass
Layer 1 โ Python Jail Escape:
The jail blocks direct ._attr access via AST but allows getattr() with computed strings. Construct __ using chr(95) and walk object.__subclasses__() to find a class whose __init__.__globals__['__builtins__'] contains __import__, then import os.
Layer 2 โ Seccomp Warden Bypass:
The warden's BPF filter only monitors specific syscalls โ symlink/symlinkat are NOT in the filter and execute freely. The warden's openat handler reads the path string from the tracee and checks if it starts with /flag. By creating a symlink (/tmp/rf -> /flag.txt) and opening the symlink path, the warden sees /tmp/rf (not blocked), but the kernel follows the symlink to /flag.txt.
Exploit payload (exploit_payload.py):
Solve script (solve.py):
Flag: 0xfun{wh0_w4tch3s_th3_w4rd3n_t0ctou_r4c3}
Phantom
Description
The challenge provides a Linux kernel + initramfs with a custom kernel module phantom.ko exposing /dev/phantom. It supports:
ioctl(CMD_ALLOC)(0x133701): allocate an object + one physical pagemmap(): map that physical page into userspace withremap_pfn_rangeioctl(CMD_FREE)(0x133702): free the physical page
The bug is a physical page use-after-free: after CMD_FREE, the userspace mapping created by mmap() still points to the freed page, giving a dangling alias to whatever that page is later reallocated for.
Solution
Create a dangling mapping to a freed physical page
Open
/dev/phantom,CMD_ALLOC,mmap()the page, thenCMD_FREE.Close the fd; the mapping remains, but the underlying physical page is back in the buddy allocator.
Reclaim the freed page as a user page-table page (โdirty pagetableโ)
Allocate page tables by mapping a fresh anonymous region and faulting a page.
If the freed physical page is reused as a PTE page, the dangling mapping now gives us direct read/write access to that PTE page.
Detect a PTE page by looking for exactly one non-zero 8-byte entry that has
present|rw|userbits set, then verify by aliasing page 1 โ page 0 PFN and checking the alias works.
Arbitrary physical read (and scan for the flag)
With a writable PTE page, write PTE entries to map arbitrary PFNs as user pages.
Flush TLB (
mprotecttoggling is enough here).Scan the mapped window for the ASCII pattern
0xfun{...}and print the first non-known-fake hit.
Build + run (using the provided container instance port):
Code
Makefile
exploit.c
remote.py
Flag: 0xfun{r34l_k3rn3l_h4ck3rs_d0nt_unzip}
rev
Chip8 Emulator
Description
A CHIP-8 emulator binary with 100+ game ROMs. The description hints at a "flaw" in the emulator and that in "quad cycles" (4 iterations) the flag can be recovered.
Solution
The binary is an unstripped ELF x86-64 CHIP-8 emulator. Key findings from static analysis:
Hidden opcode
FxFF: The standard CHIP-8 instruction set doesn't includeFxFF. In this emulator,decode_F_instructionroutes opcode0xFFtoCpu::superChipRendrer(), which performs AES-256-CBC decryption.Encryption scheme: The
superChipRendrerfunction:Loads a base64-encoded ciphertext from the global
_3ncvariableDerives the AES key from
bytearray3(ultimately copied fromemu_key, derived deterministically from constants0xdeadbeef,0xcafebabe,0x8badf00d,0xfeedface)Base64-decodes the ciphertext; first 16 bytes = IV, rest = AES-256-CBC ciphertext
Decrypts and stores the result back into
_3ncXORs filename bytes
[0x4C,0x46,0x4B,0x4D,0x04,0x5E,0x52,0x5E]with0x2Ato get"flag.txt"Writes the final decrypted content to
flag.txt
Quad cycles: The ROM
F0FF 1200(trigger decrypt, then loop) causessuperChipRendrerto run multiple times. Each cycle base64-decodes and decrypts the previous result, requiring 4 iterations total to reach the plaintext flag.Key extraction: The key derivation is complex obfuscated code. Using an
LD_PRELOADhook onEVP_DecryptInit_ex, the AES-256 key was captured at runtime:Key (hex):
744c6542484a764c434448444541434843424538484353414946696441474749Key (ASCII):
tLeBHJvLCDHDEACHCBE8HCSAIFidAGGI
Solution script:
LD_PRELOAD hook used to extract the key:
Compiled and used:
Flag: 0xfunCTF2025{N0w_y0u_h4v3_clear_1dea_H0w_3mulators_WoRK}
VM Stealth
Description
A small ELF file stands guard behind invisible walls. It runs checks, transforms your input through a mysterious process, and responds with a single word "Wrong" or "Correct".
Category: Rev | Points: 750
Solution
The binary is UPX-packed. After unpacking with upx -d vm, the binary implements:
Anti-debugging checks (all non-fatal, just print warnings to stderr):
ptrace(PTRACE_TRACEME)-- detects debugger attachmentReading
/proc/self/statusforTracerPid:-- detects tracingTiming check using
gettimeofdayaround a busy-loop of 250,000 FNV-64 iterations -- detects slow execution (e.g., single-stepping)
FNV-1a 32-bit hash verification on the entire input string:
Initializes hash state
ecx = 0x811c9dc5(FNV offset basis)For each byte:
eax = byte ^ ecx; ecx = eax * 0x01000193(FNV prime)After the loop, compares the intermediate XOR result (eax, before the final multiply) against
0xd884285aThis means:
last_byte ^ hash_state_before_last_byte == 0xd884285a
Since FNV-1a uses only a 32-bit hash, there are many valid collisions. The flag format is 0xfun{...}, so we need: fnv1a("0xfun{" + inner) == 0xd884285a ^ 0x7d (where 0x7d = '}'), giving target 0xd8842827.
Meet-in-the-middle approach: Split the inner string into two halves. Compute forward hashes from the prefix for all left halves, then invert the hash backwards from the target for all right halves. When a forward hash matches a backward-inverted hash, we have a collision.
The modular inverse of FNV prime mod 2^32 allows backward computation: h_prev = ((h_curr * PRIME_INV) & 0xFFFFFFFF) ^ byte
Multiple valid flags exist (e.g., 0xfun{f57nbf}, 0xfun{tTU6p5}). Any collision that makes the binary output "Correct!" is accepted.
Verification:
Nanom-dinam???itee?
Description
Don't trust what you see, trust what happens when no one is looking.
A stripped x86-64 ELF binary that uses fork/ptrace anti-debugging with a parent-child architecture to validate a 40-character password.
Solution
The binary contains a fake flag 0xfun{1_10v3_M1LF} printed when the password length is wrong. The real validation happens through a parent-child ptrace dance:
Child process (
fcn.0000131b): Callsptrace(PTRACE_TRACEME)thenraise(SIGSTOP)to let the parent attach. Reads a 40-character password, then iterates over each character computing a modified FNV-1a hash. After each iteration, it executesud2(illegal instruction) which raisesSIGILL.Parent process (
fcn.000014ad): Loads 40 expected hash values from the.rodatasection at offset0x20a0. On eachSIGILLfrom the child, it usesptrace(PTRACE_GETREGS)to read the child's registers โraxcontains the current hash andrbxcontains the iteration index. It compares the hash againstexpected[index]. If correct, it advances RIP by 2 (skippingud2) and continues the child. If wrong, it kills the child.Hash function (
fcn.000012a9): A modified FNV-1a with an extra folding step per byte:
Since each character's hash depends only on the previous hash (cumulative), we can brute-force each position independently over printable ASCII (95 candidates per position).
Flag: 0xfun{unr3adabl3_c0d3_is_s3cur3_c0d3_XD}
pingpong
Description
Rev challenge (495 points, 2 solves). A stripped Rust ELF binary that binds a UDP socket and waits for data from specific IP addresses.
Solution
Binary behavior:
Hex-decodes a hardcoded ciphertext:
0149545b5f4b5d1e5c545d1a55036c5700404b46505d426e02001b4909030957414a7b7a48(37 bytes)Binds a UDP socket on
127.0.0.1:9768Waits for a 37-byte UDP packet from IP
112.105.110.103or112.111.110.103(ASCII for "ping" and "pong")XORs the received data with keys derived from the IP address strings, alternating every 15 bytes
Compares the XOR result against the ciphertext using
bcmpIf match: prints "Now you're pinging the pong!"
XOR key schedule (from disassembly at 0x18fe0):
The binary formats each IpAddr to its decimal string representation
Key alternates between
"112.105.110.103"(ping, 15 chars) and"112.111.110.103"(pong, 15 chars)Bytes 0-15: XOR with ping string, bytes 16-30: XOR with pong string, bytes 31-36: XOR with ping string
The flip occurs when
index % 15 == 0
Flag recovery: Since received_data XOR key == ciphertext, the flag is ciphertext XOR key:
Verification: Used an LD_PRELOAD hook to replace recvfrom, injecting the computed flag bytes with a spoofed source IP of 112.105.110.103. The binary accepted the data and the flag was confirmed correct.
Flag: 0xfun{h0mem4d3_f1rewall_305x908fsdJJ}
Pharaoh's Curse
Description
The pharaoh's tomb holds ancient secrets. Only those who speak the old tongue may enter. Two files provided: tomb_guardian (ELF binary) and sacred_chamber.7z (password-protected archive).
Solution
Stage 1: Cracking the Tomb Guardian
The tomb_guardian binary is a custom stack-based VM with anti-debugging (ptrace check). It reads 11 characters from stdin, XORs each with a key byte, and compares to expected values. If all pass, it prints a message containing the password for the 7z archive.
Extracted the bytecode from the ELF data section and decoded the input validation:
Password: 0p3n_s3s4m3. Running the binary reveals the 7z password: Kh3ops_Pyr4m1d.
The output message also encodes characters as ADD pairs (PUSH a, PUSH b, ADD, PUTCHAR), printing the decrypted result.
Stage 2: The Hieroglyphic VM
Extracting sacred_chamber.7z yields hiero_vm (a Rust-based interpreter) and challenge.hiero (a program written in Unicode hieroglyphics).
The hieroglyphic instruction set:
๐ด
Read input char
๐น X
Load memory[X]
๐
Store to memory
๐
Push to stack
๐ญ
ADD top two stack values (mod 256)
๐
Compare equal
๐ X Y
Jump to handler if comparison fails
๐ณ
Print success
๐ฏ
Halt
The program reads 27 characters into memory slots 0-26, then checks 24 constraints: 19 adjacent-pair additions (mem[i] + mem[i+1] == expected) for indices 6-25, plus 5 cross-pair checks for validation.
This yields the flag: 0xfun{ph4r40h_vm_1nc3pt10n}
Flag: 0xfun{ph4r40h_vm_1nc3pt10n}
Liminal
Description
A 750-point reverse engineering challenge. Given a stripped x86-64 ELF binary that uses Spectre-RSB cache side channels to implement a Substitution-Permutation Network (SPN) cipher. The binary takes a 64-bit hex input and produces a 64-bit hex output. Goal: find the input that produces 0x4C494D494E414C21 (ASCII "LIMINAL!").
Solution
The binary implements an 8-round SPN cipher where each S-box lookup is performed through a speculative execution side channel (Spectre-RSB + Flush+Reload). It requires specific CPU cache timing behavior to run natively, but the cipher parameters can be extracted statically from the binary's data section.
Binary structure:
Calibration (0x406298): Tests if the CPU supports the required cache timing side channel by running 100 Flush+Reload trials
Compute function (0x405b37): Runs the SPN cipher 50 times with majority voting for noise resilience
64 bit functions (0x401681+): Each implements one output bit of an S-box via cache side channel
Cipher parameters extracted from the binary:
8 round keys at virtual address 0x42f2c0 (64-bit little-endian)
64 S-box lookup tables at 0x40f280 (8 S-boxes ร 8 bits, 256 entries ร 8 bytes each, 0x800 spacing). Values encode output bits:
0x340โ bit=1,0x100โ bit=064-byte permutation table at 0x42f280
Cipher algorithm:
Side channel mechanism: Each bit function uses a Spectre-RSB gadget:
Flushes two probe cache lines (r8, r9)
Calls a training function that pushes a fake return address and
clflushes it from the stackThe CPU's Return Stack Buffer mispredicts the return target, speculatively executing the lookup table read and buffer access
Timing measurement via
rdtscpdetermines which probe was loaded:cmp %r10, %r11; setb %al
Solving: Invert the cipher - undo each round in reverse (inverse permutation, inverse S-box, XOR key).
Verification: Patched the binary to replace the side-channel bit functions with direct table lookups (test $0x200, %rax; setnz %al) and confirmed compute(0x4c8e40be1e97f544) = 0x4c494d494e414c21.
Flag: 0xfun{0x4c8e40be1e97f544}
Unravel Me
Description
A stripped 32-bit ELF crackme binary that checks a flag argument. The hint says "Tools will fail you. Creativity will save you." โ the binary is compiled with the M/o/Vfuscator, converting all computation into mov instructions with signal handlers for control flow.
Solution
Identifying the obfuscation: The binary is ~5.8MB with a massive .data section (5.7MB of lookup tables). Disassembly reveals almost exclusively mov instructions with only two cmp instructions in the entire .text section. Signal handlers for SIGSEGV (branching) and SIGILL (control flow) confirm this is a M/o/Vfuscator binary.
Binary structure:
SIGSEGV handler at
0x8049040โ implements conditional branchingSIGILL handler at
0x80490c7โ handles other control flowLookup tables at
0x8066f30(addition),0x81a7a80(XOR),0x8197570(AND),0x81fbb70(identity/zero-extend)Three working "registers" at
0x8053040,0x8053044,0x805304c
Flag extraction approach:
First
cmpat0x8049689checksargc == 2(need exactly one argument)Second
cmpat0x80515acchecks accumulated XOR result == 0 (all characters match)
The flag is checked character-by-character. For each position, the expected character is XORed with the input character using the table at 0x81a7a80. Results are ANDed together โ if any mismatch, the final result is non-zero โ "Wrong!".
Extracting expected characters: Grepping all movl $immediate instructions revealed 27 of 42 expected characters as direct immediates in the code:
The remaining 15 characters (at positions 12, 16, 19-21, 23, 25-36) were loaded via multi-level pointer dereferences through the movfuscator's virtual stack rather than immediate values. These were extracted using GDB breakpoints at the XOR comparison points to read register values at runtime.
Final flag: 0xfun{r3v_ObfU4C3t10n_m4St3r_246643635876}
(Leet-speak for "reverse Obfuscation master" + numeric suffix)
Only Moves
Description
The challenge provides a 32-bit Linux ELF that was compiled with a โMOV-onlyโ obfuscation (movfuscator-style). It prompts for a flag and prints either Wrong! or Correct! You got the flag!.
Solution
This binary computes a deterministic 28-byte transform of the input into an internal buffer at 0x8600158..0x8600173, then compares that buffer byte-by-byte against a 28-byte constant stored at the start of .data (VA 0x8057010), immediately before the Correct! string.
Key observations used to solve it:
The expected 28 bytes are embedded in the file at the beginning of
.data:.dataVA0x8057010is file offset0x00f010(readelf -S attachments/only_moves).Expected bytes (hex):
b03cc34d9ca2aedfeab449e4c81719a0c66bf21dd586ca9bd22a5d0d.
The check stops at the first mismatch, and changing later input bytes does not affect earlier output bytes. In particular, output index
iis first influenced by input index(i ^ 1)(adjacent swap), so we can solve bytes incrementally without breaking previously-matched output.
Approach:
Extract the expected 28 bytes from the binary.
Use GDB to dump the transformed 28-byte buffer right before the result
printfcall.Fill a 28-byte candidate with the known frame
0xfun{...}and brute-force one byte at a time:For output position
i, brute-force input position(i ^ 1)until the transformed output prefixout[:i+1]matches the expected prefix.
Verify by running the original binary and confirming it prints
Correct!.
Recovered flag: 0xfun{m0v_1s_tur1ng_c0mpl3t}
Code used (GDB dumper + solver):
warmup
JScrew
Description
WarmUp challenge (50 pts). A web container serves a page with obfuscated JavaScript ("compacted JS code"). The page title says "JScrew" and shows "Status: loading flag from cold storage..." but the flag is hidden inside multiple layers of JS obfuscation. Dev tools are blocked by anti-debugging measures.
Solution
The page contains two layers of JavaScript obfuscation:
AAEncode (outer layer) - Japanese-style JS encoding using Unicode characters like
๏พฯ๏พ๏พ,๏พะ๏พ, etc. This encodes JavaScript using only non-ASCII emoticon-like characters.JJEncode (inner layer) - Another JS encoding that uses
$=~[];as its starting pattern, building strings character by character through type coercion.
Step 1: Decode AAEncode
The AAEncode ultimately calls Function via "".constructor.constructor (the constructor chain). By overriding Function.prototype.constructor before the script runs, we intercept the decoded code instead of executing it:
This reveals anti-dev-tools code (blocking F12, right-click, console, etc.) followed by a JJEncode payload.
Step 2: Decode JJEncode
Applying the same Function.prototype.constructor interception to the JJEncode portion reveals the final payload:
The hex string 9ad2ccdfc4d1c699ccdef5c9dfd8c6d3f5c8d8cbc9cff5c9c5c7c7cbf5d89bcdc2def5c9dfd8c6d3f5c8d89ec9cfd7 is the encoded flag.
Step 3: Decode the hex string
The first byte 0x9a XOR'd with 0x30 (ASCII 0, the first char of 0xfun{...}) gives key 0xAA. XORing every byte with 0xAA decodes the flag:
Flag: 0xfun{l3ft_curly_brace_comma_r1ght_curly_br4ce}
Shell
Description
A web app lets you upload images to inspect their EXIF metadata using ExifTool. The goal is to achieve command execution and read flag.txt. Only image uploads are allowed.
Solution
The server runs ExifTool 12.16, which is vulnerable to CVE-2021-22204 โ arbitrary code execution via crafted DjVu file annotations. ExifTool's DjVu.pm module uses Perl's eval to parse DjVu annotation chunks (ANTa), allowing injection of arbitrary Perl code.
The exploit creates a minimal DjVu image file with a malicious ANTa annotation chunk containing a Perl system() call that reads the flag:
Upload the crafted DjVu file:
The flag appears in the ExifTool output before the normal metadata, as the system() call writes directly to stdout during parsing.
Flag: 0xfun{h1dd3n_p4yl04d_1n_pl41n_51gh7}
TLSB
Description
A BMP image file is provided. The challenge introduces "Third Least Significant Bit" (TLSB) steganography - instead of hiding data in the LSB (bit 0) of each byte, data is hidden in the 3rd least significant bit (bit 2).
Solution
The file is a 16x16 24-bit BMP image. We extract bit 2 ((byte >> 2) & 1) from each byte of the raw pixel data (starting after the 54-byte BMP header), concatenate the bits, and convert to bytes. This reveals a base64-encoded flag.
Flag: 0xfun{Th4t5_n0t_L345t_S1gn1f1c4nt_b1t_5t3g}
Templates
Description
A simple greeting service using Server Side Rendering. Users enter their name and the server renders a greeting. The challenge hints at SSTI (Server-Side Template Injection).
Solution
The service takes a name parameter via POST and renders it directly in a Jinja2 template without sanitization.
1. Confirm SSTI:
2. Identify the template engine (Jinja2 confirmed via {{self}}):
3. Enumerate Python subclasses to find os._wrap_close:
4. Exploit via os.popen from os._wrap_close.__init__.__globals__:
Flag: 0xfun{Server_Side_Template_Injection_Awesome}
UART
Description
A strange transmission has been recorded. We're given a uart.sr file (Sigrok logic analyzer capture) containing a single-channel UART recording.
Solution
The .sr file is a zip archive containing Sigrok capture metadata and raw logic data. From the metadata:
1 channel (
uart.ch1)1 MHz sample rate
unitsize=1(each byte = one sample)
Analyzing pulse widths reveals a minimum of ~8.68 samples per bit, corresponding to 115200 baud. The signal is standard 8N1 UART (idle high, start bit low, 8 data bits LSB-first, stop bit high).
Decoding the signal yields the flag directly.
Flag: 0xfun{UART_82_M2_B392n9dn2}
Perceptions
Description
A blog is hosted at the challenge URL. The description hints at a "neat backend" and that the server "uses fewer ports."
Solution
Step 1: Enumerate the blog
Visiting the web server reveals a blog with several pages, a /name endpoint returning Charlie, and a links.js file listing all page paths.
Step 2: Find credentials
The "Secret Post" page (4C6Y4NEBVLATCF6EX5PA2ISZ/page.html) contains an HTML comment with credentials:
Combined with the /name endpoint returning Charlie, the credentials are Charlie:UlLOPNeEak9rFfmL.
Step 3: SSH on the same port
The blog mentions "fewer ports" and "generic Linux remote access stuff" โ hinting that SSH runs on the same port as HTTP. The server uses protocol multiplexing (the "Perceptions" server) to distinguish HTTP from SSH connections.
Step 4: Navigate the custom shell
The SSH session drops into a custom restricted shell. Listing files shows a secret_flag_333 directory:
The flag.txt file contains ASCII art and the flag.
Flag: 0xfun{p3rsp3c71v3.15.k3y}
Music
Description
Weโre given a sheet-music image (attachments/sheet.png) and told the flag must be embedded into the 0xfun{...} format.
Solution
The sheet contains only the notes C D E F G A B across two octaves, with only two rhythmic classes used for most notes (eighth vs quarter; plus a final dotted-half). That strongly suggests a โnote+duration โ characterโ substitution.
Using the common โmusic sheet cipherโ style mapping:
Treat the pitch (C..B and then c..b) as an index 0..13
Use quarter-or-longer notes to emit
A..NUse eighth notes to emit
O..Z(the sheet never needs values beyondZ)
Transcribing the notes from the image and applying that mapping yields a 64-character AโZ string:
ETUVWLWUSSDEFUWXYJWVVUGDTRBUSWULUWXYJRSTRBJYZIXWVUTSRQQPBSUWZTYL
This is valid Base32 text (length multiple of 8, characters A-Z), so we embed it directly as the flag body.
Flag: 0xfun{ETUVWLWUSSDEFUWXYJWVVUGDTRBUSWULUWXYJRSTRBJYZIXWVUTSRQQPBSUWZTYL}
Solution code (solve.py):
Delicious Looking Problem
Description
A discrete logarithm problem using 42-bit safe primes. The challenge encrypts a flag with AES-ECB using a key derived from os.urandom(len(flag)). The same key (as an integer) is used as the secret exponent in 8 DLP instances, each with a different 42-bit safe prime. The AES key is SHA256(key_bytes).
Solution
Since the primes are only 42 bits, discrete logarithms are easily computable. Each sample gives us h = g^x mod p where x = bytes_to_long(key) and p = 2q + 1 is a safe prime.
Step 1: Compute discrete logs. For each of the 8 samples, compute x mod (p-1) using standard discrete log algorithms (trivial for 42-bit primes).
Step 2: CRT recovery. Since p-1 = 2q where q is prime, extract x mod q from each sample. The q values are distinct primes, so CRT gives x mod product(q_1...q_8). Combined with x mod 2 (all discrete logs were odd), we get x mod (2 * product(q_i)), a ~325-bit modulus.
Step 3: Brute force remaining bits. The key is os.urandom(len(flag)) where the flag is 43 bytes (determined by the 48-byte ciphertext with PKCS7 padding). A 43-byte key is ~344 bits, exceeding our 325-bit modulus. This leaves ~19 bits of uncertainty, meaning ~10^6 candidates to try. For each candidate x = x_base + k * modulus, compute SHA256(long_to_bytes(x)), decrypt with AES-ECB, and check for valid padding and the flag prefix 0xfun{.
Flag: 0xfun{pls_d0nt_hur7_my_b4by(DLP)_AI_kun!:3}
Guess The Seed
Description
The binary is a stripped ELF that, on startup, calls:
time(NULL)srand(seed)rand()five times
It asks for 5 space-separated guesses and validates each one as rand() % 1000 against the generated values.
Solution
I used objdump to confirm the control flow: time and srand are called before any prompt, then five rand calls, then each user value is reduced with % 1000 before comparison.
Because the seed is time-based, we can predict the sequence by reproducing libcโs rand() for nearby epoch values and feed candidate guesses to the binary until a matching seed is found.
I found a valid run and the binary printed:
0xfun{W3l1_7h4t_w4S_Fun_4235328752619125}
Leonine Misbegotten
Description
Given attachments/output produced by 16 rounds of random encoding with a checksum, recover the flag.
chall.py does:
start with
flag.encode()repeat 16 times:
compute
checksum = sha1(current).digest()choose one of
[base16, base32, base64, base85]set
current = encoded(current) + checksum
write final
currenttooutput
Solution
output ends each round with 20 bytes of SHA-1. Going backwards, at each step:
split blob as
body | digestwheredigestis last 20 bytestry each of the 4 decoders on
bodykeep only decodings where
sha1(decoded) == digestrecurse 16 times
This quickly collapses to one printable candidate, the flag.
Recovered flag: 0xfun{p33l1ng_l4y3rs_l1k3_an_0n10n}
Schrรถdinger's Sandbox
Description
Code runs in two parallel "universes" - one with the real flag, one with a fake. Output is only shown if both universes produce identical output (status "match"). Otherwise, the output is hidden (status "diverged"). The server returns time_a and time_b - the execution time for each universe.
Solution
Since printing the flag directly causes divergence (different flags = different output), we use a timing side-channel combined with an output divergence oracle to leak the flag character by character via binary search.
Key observations:
Both flags share the same format prefix
0xfun{, suffix}, and length (41).For a comparison like
flag[pos] <= mid, if both universes agree, the output matches. If they disagree, output diverges.The server reports per-universe execution times (
time_a,time_b), sotime.sleep()can distinguish universes.
Algorithm: For each character position, binary search over ASCII values:
First query uses the divergence oracle: submit code that prints 'A' if
flag[pos] <= mid, else 'B'. If status is "match", both universes agree and we narrow both ranges identically.If status is "diverged", the flags differ at this comparison point. Fall back to timing: submit code that sleeps 150ms if
flag[pos] <= mid. Checktime_avstime_bto determine which universe satisfied the condition.
This gives ~7 queries per character ร 34 unknown characters โ 238 queries total.
Flag: 0xfun{schr0d1ng3r_c4t_l34ks_thr0ugh_t1m3}
web
Jinja
Description
The app generates a welcome email by validating user input with Pydantic EmailStr, then embedding the (supposedly safe) email into an HTML string and rendering it as a Jinja2 template:
Because the email string is inserted into the template source before rendering, any {{ ... }} inside the email becomes server-side template injection (SSTI).
Solution
Confirm SSTI with
{{7*7}}and observe it evaluates server-side.The input is validated as an email, which blocks many characters in a normal local-part. The bypass is that
email-validatoraccepts RFC 5322 "name-addr" syntax:Display Name <addr@domain>.Put the SSTI in the display-name and keep the actual address inside
<...>simple. Example:{{7*7}} <[email protected]>.Quote the display-name to allow characters like spaces/parentheses, enabling function calls. Example:
"{{lipsum()}}" <[email protected]>.Use Jinja's built-in
lipsumfunction to reach Python globals (lipsum.__globals__includesos) and execute the SUID helper/getflag. Example:"{{lipsum.__globals__.os.popen('/getflag').read()}}" <[email protected]>.
Exploit code (end-to-end):
Webhook Service
Description
A webhook registration/trigger service that blocks requests to internal IPs. An internal flag server runs on 127.0.0.1:5001 and serves the flag on POST /flag. The app checks URLs against private/loopback/reserved IP ranges using socket.gethostbyname() + ipaddress before both registering and triggering webhooks.
Solution
Vulnerability: DNS Rebinding SSRF (TOCTOU). The /trigger endpoint resolves the hostname twice โ once in is_ip_allowed() via socket.gethostbyname(), and again in requests.post() via socket.getaddrinfo(). With no DNS caching in the Docker container (Debian slim, no nscd), each call makes a fresh DNS query.
Exploit: Use the 1u.ms DNS rebinding service which alternates responses between two IPs in round-robin mode. Register a webhook pointing to http://{random}.make-8.8.8.8-rebind-127.0.0.1-rr.1u.ms:5001/flag, then repeatedly trigger it. When the DNS alternation aligns so that is_ip_allowed() gets 8.8.8.8 (passes check) and requests.post() gets 127.0.0.1 (connects to internal flag server), the flag is returned.
Flag: 0xfun{dns_r3b1nd1ng_1s_sup3r_c00l!_ff4bd67cd1}
Tony Toolkit
Description
Tony decided to launch bug bounties on his website for the first time, so it's likely to have some very common vulnerabilities. A Flask/Werkzeug web application with search, login, and user profile functionality.
Solution
Step 1: Discover hidden files via robots.txt
Reveals:
/main.pyi- Application source code/user- User profile page/secret/hints.txt- Hints
Step 2: Analyze the source code (/main.pyi)
The source reveals three vulnerabilities:
SQL Injection in
/search: Theitemparameter is directly concatenated into the SQL query:Broken authentication in
is_logged_in(): The function iterates over all users and checksif sha256(...).hexdigest()- butsha256().hexdigest()always returns a non-empty string (truthy), so this always returns True as long as any users exist in the database. It never actually validates theusercookie value.File read in
/user: Readsusers/<userID>whereuserIDcomes from a cookie (but is cast toint, preventing path traversal).
Step 3: Exploit SQL injection to confirm users exist
Returns two users: Admin (ID=1) and Jerry (ID=2).
Step 4: Bypass authentication via cookie manipulation
Since is_logged_in() never validates the cookie value, simply setting any user cookie along with userID=1 bypasses authentication entirely:
This returns the flag from the Admin's profile file at users/1.
Flag: 0xfun{T0ny'5_T00ly4rd._1_H0p3_Y0u_H4d_Fun_SQL1ng,_H45H_Cr4ck1ng,_4nd_W1th_C00k13_M4n1pu74t10n}
SkyPort Ops
Description
FastAPI + Strawberry GraphQL app sits behind a custom โSecurityGatewayโ reverse proxy. The gateway blocks any path starting with /internal/. The backend contains an admin-only upload endpoint (/internal/upload) with an arbitrary file write when the uploaded filename starts with /. The flag is readable only via a SUID helper at /flag.
Solution
Leak staff JWT via GraphQL Relay node
The GraphQL
node(id: ...)interface exposesStaffNode.accessToken.Relay global ID for officer_chen is
base64("StaffNode:2").
Get per-worker JWKS endpoint
The leaked staff JWT payload contains
jwks_uri(a random/api/<hex>route).Important: Hypercorn runs multiple workers and each worker generates its own RSA key + JWKS path at import time, so the JWKS must be fetched from the same worker connection that served the JWT.
Forge an admin JWT (algorithm confusion)
Backend verifies admin tokens with
jose_jwt.decode(token, RSA_PUBLIC_DER, algorithms=None).With
algorithms=None, python-jose acceptsHS256. If we sign withHS256using the RSA public key DER bytes as the HMAC secret, verification succeeds.
Bypass the gateway
/internal/block (CL-TE request smuggling)The gateway frames request bodies using
Content-Length, while Hypercorn/h11 honorsTransfer-Encoding: chunked.Send a front request like:
POST /graphqlwith bothContent-Length: XandTransfer-Encoding: chunkedbody begins with
0\r\n\r\n(ends chunked body) followed by a full smuggledPOST /internal/upload ...
The backend processes the smuggled internal request, but the gateway associates that response with the next client request (response queue poisoning).
Turn arbitrary file write into code execution
The container creates the venv with
--system-site-packages, which makessite.ENABLE_USER_SITE = True.That means Python auto-imports
usercustomizefrom the user site-packages path:/home/skyport/.local/lib/python3.11/site-packages/usercustomize.py
Upload a malicious
usercustomize.pythat runs/flag(SUID root) and writes the output to/tmp/skyport_uploads/flag.txt(served at/uploads/flag.txt).Trigger Hypercorn worker recycling (
--max-requests 100) by sending many requests; the new worker process importsusercustomizeand executes the payload.
Read the flag
GET /uploads/flag.txt
Solver
Perimeter Drift
Description
Web app with multiple โtrust boundaryโ components (SSO, reviewer workflow, admin bot, internal import pipeline). Goal is to cross boundaries to reach the privileged import path and gain code execution in the internal service, then read the flag.
Solution
The full chain is:
Forge an SSO
id_token:Server claims to accept
RS256, but verifies withHMAC-SHA256.It also accepts
jkuand fetches attacker-controlled JWKS, caching a symmetrickperkid.Host a JWKS that supplies
k, then sign the token with HMAC and log in as the seeded SSO user (nora.v).
Escalate to reviewer:
Reviewer grant verification reads HMAC key bytes from
KEYS_DIR / f"{kid}.pem".Upload a
.pemfile into/var/app/review-materials/โฆand setkid=../review-materials/<stem>to path-traverse out ofKEYS_DIR.Sign a grant JWT with the uploaded bytes;
/review/escalatesets session role toreviewer.
Use the admin bot to exfil a valid
workspace_key:As
reviewer,/reportqueues the Playwright bot to visit an arbitrary URL with a real admin session cookie.Serve an attacker page that iframes
/admin?cb=<attacker>/cb, then navigates the iframe to/back(attacker page) which callshistory.back().The
/adminpageโsadmin.jshas apageshowhandler that detects back/forward navigation and redirects tocb?workspace_key=โฆ.Capture the
workspace_keyfrom that request.
RCE through admin import pipeline:
/admin/uploadstores an uploaded artifact whenX-Workspace-Keyis valid./admin/xml/importrequires XIncludefile://โฆreferences under/var/app/uploads/, then XIncludes, base64-decodes text, and POSTs bytes to the internal service.Internal service does
pickle.loads(data)โ code execution.Use a pickle payload that copies
/flag.txtto/shared/loot/flag.txt, then read it via/recovery/latest.
Run the solver locally: python3 -u solve.py
If the workspace key leak times out on Linux Docker hosts where host.docker.internal is not available, run with an IP reachable from the bot container (commonly the Docker bridge gateway), e.g.: EXTERNAL_HOST=172.17.0.1 WEB_HOST_FOR_BOT=172.17.0.1:5000 python3 -u solve.py
Flag (author-verified): 0xfun{y0u_5ucc3ssfu11y_dr1f73d_4ll_7h3_w4y_thr0ugh_7h3_b0und4r1s5}
Solver code (solve.py):
Last updated