๐Ÿ‚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:

  1. Track the state transform matrix T (2048x2048 over GF(2)) through each PRNG step

  2. At each step, compute the output as a linear function of the initial state bits

  3. 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.

  1. Connect and call spin 624 times to collect obfuscated outputs

  2. XOR each with 0xCAFEBABE to recover the raw getrandbits(32) values

  3. Untemper each value to recover the MT internal state

  4. Clone the state into a local random.Random() and predict the next 10 raw values

  5. Submit 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

  1. 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).

  2. Map each token to its intended plaintext character and join them.

  3. The important tokenization detail here is that the digit 7 is 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:

  1. Compute state1 = (h1 << 32) | l1

  2. Compute state2 = (A * state1 + C) mod 2^64

  3. Check if state2 >> 32 == h2

  4. If 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 flag

  • Two 2ร—2 Hermitian matrices over K = Q(zeta_256):

    • Q = B^H * B with entries q0,q1,q2

    • S = B * B^H with entries s0,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

  1. Use associativity: B*(B^H*B) = (B*B^H)*B, i.e. B Q = S B.

  2. From the (0,0) entry of B Q = S B, and using:

    • det(Q)=1 (since det(B)=1)

    • tr(Q)=tr(S) (since Q and S are similar), derive the linear relation in K:

    f*(q0*s2 - 1) = q0*s1*g + conjugate(q1)*conjugate(g)

  3. Write ring elements in the power basis of zeta256, i.e. coefficient vectors in R = Z[x]/(x^128+1). Complex conjugation acts by reversing coefficients with a sign: conjugate(x^i) = -x^(128-i) for i>0.

  4. Work modulo a prime p and turn the relation into a linear map f โ‰ก H*g (mod p).

  5. Build an NTRU-style lattice whose very short vector is the secret (f,g). Run LLL/BKZ (via fpylll) and scan reduced basis vectors; verify candidates by reconstructing Q,S.

  6. Subtlety: (Q,S) are invariant under multiplying the entire basis by a 256th root of unity u = zeta256^k (since u*conjugate(u)=1). Lattice reduction can return u*(f,g,F,G). Because the AES key uses str(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:

  • iv

  • enc

  • the exact printed sk string

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^64

  • state_{n+1} = (A * state_n + C) mod M

The challenge prints:

  • g1 = hi32(x1)

  • g2 = hi32(x2) where x2 = next(jump(x1))

  • g3 = hi32(x3) where x3 = next(jump(x2))

โ€œjump then nextโ€ is itself a single affine step mod 2^64:

  • jump(s) = (A_JUMP*s + C_JUMP) mod 2^64

  • F(s) = next(jump(s)) = (B*s + D) mod 2^64

  • B = A * A_JUMP mod 2^64

  • D = 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 (where b0=lo32(B), b1=hi32(B))

  • D = d0 + 2^32*d1

Then one โ€œcollapsedโ€ step gives:

  • l' = (b0*l + d0) mod 2^32

  • carry = (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

  1. Decode RAW_Data binary tokens to raw bytes.

  1. The payload is mostly UTF-8/ASCII with high-bit noise bytes. Remove bytes 0x80-0xFF.

  1. Expand %Ilc:~n,1% using the first line variable and strip obfuscating %...% placeholders to get the true script text.

This yields:

  1. The challenge hint text is i could be something to this and 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

  1. Steghide extraction: The hint "a bit strange" suggests steganography. Used stegseek to brute-force the steghide passphrase on Bart.jpg, finding passphrase simple and extracting bits.txt containing a URL:

  1. Download from Cybersharing: The URL pointed to a file sharing service hosting a file called bits.txt (460 KB) containing base64-encoded data.

  2. 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.

  1. 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:

  1. Detect sync pulses (negative spikes below -25000)

  2. Extract amplitude data between consecutive pulses as scan lines

  3. Resample each line to a fixed width (384 pixels)

  4. 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 contains hint.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 = 420000 samples

  • 420000 * 5 = 2100000 bytes

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:

  1. Bit 9 is the inversion flag - if set, XOR bits 7:0 with 0xFF

  2. Bit 8 selects XOR vs XNOR mode for the transition chain

  3. 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.

  1. 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,000 bytes.

  • 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.

  1. 8b/10b decode

Decode each 10-bit symbol into (ctrl, byte) using an 8b/10b decoder (encdec8b10b).

  1. Reshape into a โ€œframeโ€ grid

Reshape decoded bytes time-major as:

  • 525 rows

  • 800 columns

  • 4 lanes (bytes per time slot)

So: (525, 800, 4).

This produces consistent repeating control patterns, matching a link-layer transport.

  1. 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..514 inclusive โ†’ 480 rows.

Columns repeat in blocks (โ€œTransfer Unitsโ€) of 64 columns:

  • per TU: 60 data columns + 4 overhead columns

  • 8 TUs per line โ†’ 8*60 = 480 data โ€œticksโ€ per line

  • each tick has 4 lane bytes โ†’ 480*4 = 1920 bytes per line = 640*3 RGB bytes

In the capture, the first TU marker is a control byte 0xFB at column 284, so the payload begins at column 288.

  1. Descramble (the crucial step)

The extracted pixels are still scrambled. Apply a DisplayPort-style self-synchronizing scrambler:

  • Use a 16-bit LFSR, initial state 0xFFFF

  • Reset state to 0xFFFF when encountering SR (control byte 0x1C, 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

  1. 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ร—3 RGB

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

  1. Decode the WAV as SSTV (Scottie 1) to get an image:

    • sstv -d attachments/dots.wav -o sstv_output.png

  2. Convert the SSTV image into a clean black/white dot image (DotCode-friendly) by grayscale thresholding at 128:

    • python3 solve.py

  3. Decode output_dots.png as 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

  1. The spectrogram text 0xfun{50_345y_1_b3l13v3} is a red herring.

  2. 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 DSSF container.

  3. The DSSF container includes si.txt containing a Cybersharing URL: https://cybersharing.net/s/33864416ca80f2c5.

  4. Use Cybersharingโ€™s JSON API to resolve the fragment and download the linked file.zip, then extract seccat.png.

  5. seccat.png starts with a PNG signature but is intentionally invalid because chunk order is broken (IHDR isnโ€™t first, IEND isnโ€™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

  6. 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

  1. Read description.md and extract only characters in the Unicode variation-selector supplemental range U+E0100..U+E01FF.

  2. Convert each to an integer with cp - 0xE0100.

  3. Decode by subtracting 240 (mod 256) and converting to ASCII.

  4. 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

  1. Obtain a Discord user token (same method used for Insanity 1).

  2. 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

  3. 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 center

  • a, b: rotation + scale (affine)

  • tx, ty: translation

  • k1: 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

  1. 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.

  2. Download qr.png from that URL. The PNG has a ZIP appended after IEND; inside is an index.html containing a data:image/...;base64,... payload that decodes to secret_id.png.

  3. secret_id.png contains 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:

  1. 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.

  2. 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.

  3. 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 #1623555arrow-up-right) 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.com

  • File 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 #1623555arrow-up-right) 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:

Domain
Resolved IP
Timing

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 #1623555arrow-up-right), the dropped file 3aw.msi has:

  • MD5: EDFA951162F885729864766075266751

  • SHA256: DE7734BAC9FCBE4355DD56B089487CFECEA762FA35E9C5B44E5047F6BAE96D3A

Step 2: Search for the hash on malware sandboxes

Searching the SHA256 hash on Triagearrow-up-right 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:

  1. PowerShell downloads from qq51f.short.gy/1 (URL shortener) and saves as 3aw.msi

  2. The shortener redirects to bestiamos.com/61.brr โ€” the file's original name on the distribution server

  3. The 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/3164whos3zc5xss6lv7ejfdlmogi

  • The 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.000000Z

  • handler_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:

Character
Code Point
Bits

ZWNJ

U+200C

00

ZWJ

U+200D

01

BOM

U+FEFF

11

PDF

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

  1. From the archived tweet trail, the important post says: โ€œqr seems to be a bait try thisโ€ and links to:

  • https://pastebin.com/C0PfkSNn

  1. The paste is deleted on Pastebin now, so use the Wayback Machine to locate an archived snapshot and fetch it:

  1. 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-Writeups repo.

  • 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 DS cookie.

    • The HTML embeds a per-session nonce parameter like nm_...=....

  • 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__UserDefault selector so the response includes name, 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: 3776564

  • title: 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=1255977475

  • Flag: 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

  1. Pull the tweet media (artifacts/frog_media.jpg) and geolocate it.

  2. 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.

  3. Extract Google Maps reviews headlessly via an internal endpoint:

    • https://www.google.com/maps/preview/review/listentitiesreviews?pb=... (returns XSSI-prefixed JSON)

  4. Derive IDs from the Google Maps place URL hex pair !1s0xAAAA:0xBBBB:

    • id_y = int(AAAA, 16)

    • cid = int(BBBB, 16) (also used as cid= when fetching the place HTML)

  5. Fetch a fresh kEI token from the place HTML (https://www.google.com/maps?cid=<cid>; regex kEI='([^']+)').

  6. Pagination:

    • Each returned review contains a cursor token at rev[61] (base64-like CAES...).

    • First page uses !2m2!1i0!2iN; subsequent pages use !2m3!1i0!2iN!3s<cursor>.

  7. 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

  1. The attachment is a GTAGuessr image (served from https://gtaguessr.com/guess/<filename>.jpg).

  2. 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 a locationId.

  3. Loop /API/GetLocations, download each image, and compare to the attachment using perceptual hashing until it matches.

  4. Call /API/SubmitAGuess for the matched locationId to get its (lat, lng).

  5. Reverse-geocode (lat, lng) by OCRโ€™ing GTA V โ€œstreet overlayโ€ tiles from CreepPork/GTAV-Maps (street labels are rotated, so rotate-scan + OCR).

  6. 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: 0x0804a09a

  • Correct 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 be 0x0804bff4)

  • Offset 44โ€“47: saved ebp (junk)

  • Offset 48โ€“51: return address โ†’ system@plt

  • Offset 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"):

  1. An address (stored at rbp-0x18)

  2. 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").

  1. 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_arena address โ†’ libc base.

  2. Leak heap: UAF read on chunk 0 (tcache tail) gives chunk0_addr >> 12 due to safe-linking (mangled NULL). Demangle chunk 1's fd pointer to get exact chunk0_addr.

  3. 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_all where we write a pointer to our fake FILE structure.

  4. Fake FILE (House of Apple 2): In a 0x400 chunk popped from tcache, construct:

    • Fake _IO_FILE_plus with _flags = " sh" (0x68732020), _IO_write_ptr > _IO_write_base, vtable = _IO_wfile_jumps, _wide_data pointing to fake wide data

    • Fake _IO_wide_data with _IO_write_base = 0, _IO_buf_base = 0, _wide_vtable pointing to fake wide vtable

    • Fake wide vtable with __doallocate (offset 0x68) = system

  5. Trigger: Call exit (option 5) โ†’ _IO_flush_all_lockp iterates _IO_list_all โ†’ finds our fake FILE with dirty write buffer โ†’ calls _IO_OVERFLOW via _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):

  1. Libc + heap leak via largebin reallocation

    • Allocate A(0x438), B(0x4f0), guard.

    • Free A, then allocate a 0x500 chunk to move A into the largebin.

    • Reallocate A and only overwrite a few bytes; largebin pointers remain in the user area.

    • Leak bk at A+8 for libc_base, and leak a_base (chunk base) at A+0x10 (largebin nextsize pointer).

  2. House of Einherjar-style backward consolidation (poison null)

    • For request size 0x438, malloc_usable_size == 0x438 on this glibc, so the off-by-one NUL lands at the LSB of the next chunkโ€™s size, clearing PREV_INUSE.

    • The last 8 bytes of Aโ€™s user data overlap B.prev_size, so we set B.prev_size = 0x440.

    • free(B) now consolidates backward into A, yielding a dangling pointer in slot A.

    • Must also forge Aโ€™s largebin fd/bk and fd_nextsize/bk_nextsize to self, otherwise unlink_chunk() crashes.

  3. Tcache poisoning to write into libc

    • Use the dangling A pointer (pointing into freed consolidated chunk) to corrupt a freed tcache entryโ€™s fd (safe-linking).

    • We drain possible pre-filled tcache(0x110) by allocating until we observe an allocation overlapping A at a_user.

    • Free that allocation into tcache and overwrite its fd so the next malloc(0x100) returns _IO_2_1_stdout_.

  4. FSOP (House of Apple 2) โ†’ setcontext+0x3d โ†’ ORW ROP

    • Overwrite stdout to use _IO_wfile_jumps and 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() calls wide_data->_wide_vtable->doallocate with rdx == FILE*, so we set doallocate = setcontext+0x3d.

    • setcontext+0x3d loads rsp/rbp/rip from the (corrupted) stdout struct, pivoting to a ROP chain placed in a heap โ€œcontextโ€ chunk.

    • ROP chain performs ORW on flag.txt and prints it.

Run:

  • Local: python3 solve.py

  • Remote: 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:

  1. cmd() is never called but calls system() on lines read from the FILE pointer f (global at base+0x4050). If we redirect execution there and make it read from stdin instead, we get arbitrary command execution.

  2. .text is not writable (R-X), so we can't patch code. But the stack and heap (.data/.bss) are writable.

  3. vuln's return address on the stack (at &address + 0x18) is base+0x1422. Flipping bit 3 changes it to base+0x142a = cmd+1 (skipping push rbp, which still works because leave; ret restores the frame correctly). Cost: 1 bit flip.

  4. The FILE struct's _fileno field (at offset +0x70 in the struct) determines which fd fgets() reads from. The f FILE struct is on the heap at sbrk(NULL) - 0x20cf0 (first malloc'd FILE from fopen). Its _fileno = 3 (the opened commands file). Changing it to 0 (stdin) requires flipping bits 0 and 1 (3 XOR 0 = 0b11). Cost: 2 bit flips.

The 3 flips:

  1. Flip bit 0 at sbrk - 0x20cf0 โ†’ _fileno: 3 โ†’ 2

  2. Flip bit 1 at sbrk - 0x20cf0 โ†’ _fileno: 2 โ†’ 0 (stdin)

  3. 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):

Opcode
Function
Description

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:

  1. Build 0xFFFFFFFFFFFFFFFF in memory by storing 0xFF at 8 consecutive byte offsets (overlapping qword writes), then LOAD the qword

  2. Compute negative offset -0xC0 via XOR(0xFFFFFFFFFFFFFFFF, 0xBF) = 0xFFFFFFFFFFFFFF40 โ€” this reaches 0x404020 (function table entry 0)

  3. Build system@plt address (0x401090) in memory via byte-by-byte qword construction, then LOAD it

  4. Write "sh" string in VM memory at a known address (0x404100)

  5. Build pointer to "sh" (0x404100) in a register

  6. Overwrite func_table[0] (HALT handler) with system@plt using STORE with negative offset

  7. Trigger 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 page

  • mmap(): map that physical page into userspace with remap_pfn_range

  • ioctl(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

  1. Create a dangling mapping to a freed physical page

    • Open /dev/phantom, CMD_ALLOC, mmap() the page, then CMD_FREE.

    • Close the fd; the mapping remains, but the underlying physical page is back in the buddy allocator.

  2. 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|user bits set, then verify by aliasing page 1 โ†’ page 0 PFN and checking the alias works.

  3. 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 (mprotect toggling 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:

  1. Hidden opcode FxFF: The standard CHIP-8 instruction set doesn't include FxFF. In this emulator, decode_F_instruction routes opcode 0xFF to Cpu::superChipRendrer(), which performs AES-256-CBC decryption.

  2. Encryption scheme: The superChipRendrer function:

    • Loads a base64-encoded ciphertext from the global _3nc variable

    • Derives the AES key from bytearray3 (ultimately copied from emu_key, derived deterministically from constants 0xdeadbeef, 0xcafebabe, 0x8badf00d, 0xfeedface)

    • Base64-decodes the ciphertext; first 16 bytes = IV, rest = AES-256-CBC ciphertext

    • Decrypts and stores the result back into _3nc

    • XORs filename bytes [0x4C,0x46,0x4B,0x4D,0x04,0x5E,0x52,0x5E] with 0x2A to get "flag.txt"

    • Writes the final decrypted content to flag.txt

  3. Quad cycles: The ROM F0FF 1200 (trigger decrypt, then loop) causes superChipRendrer to run multiple times. Each cycle base64-decodes and decrypts the previous result, requiring 4 iterations total to reach the plaintext flag.

  4. Key extraction: The key derivation is complex obfuscated code. Using an LD_PRELOAD hook on EVP_DecryptInit_ex, the AES-256 key was captured at runtime:

    • Key (hex): 744c6542484a764c434448444541434843424538484353414946696441474749

    • Key (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:

  1. Anti-debugging checks (all non-fatal, just print warnings to stderr):

    • ptrace(PTRACE_TRACEME) -- detects debugger attachment

    • Reading /proc/self/status for TracerPid: -- detects tracing

    • Timing check using gettimeofday around a busy-loop of 250,000 FNV-64 iterations -- detects slow execution (e.g., single-stepping)

  2. 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 0xd884285a

    • This 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:

  1. Child process (fcn.0000131b): Calls ptrace(PTRACE_TRACEME) then raise(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 executes ud2 (illegal instruction) which raises SIGILL.

  2. Parent process (fcn.000014ad): Loads 40 expected hash values from the .rodata section at offset 0x20a0. On each SIGILL from the child, it uses ptrace(PTRACE_GETREGS) to read the child's registers โ€” rax contains the current hash and rbx contains the iteration index. It compares the hash against expected[index]. If correct, it advances RIP by 2 (skipping ud2) and continues the child. If wrong, it kills the child.

  3. 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:

  1. Hex-decodes a hardcoded ciphertext: 0149545b5f4b5d1e5c545d1a55036c5700404b46505d426e02001b4909030957414a7b7a48 (37 bytes)

  2. Binds a UDP socket on 127.0.0.1:9768

  3. Waits for a 37-byte UDP packet from IP 112.105.110.103 or 112.111.110.103 (ASCII for "ping" and "pong")

  4. XORs the received data with keys derived from the IP address strings, alternating every 15 bytes

  5. Compares the XOR result against the ciphertext using bcmp

  6. If 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:

Glyph
Operation

๐“‹ด

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:

  1. Calibration (0x406298): Tests if the CPU supports the required cache timing side channel by running 100 Flush+Reload trials

  2. Compute function (0x405b37): Runs the SPN cipher 50 times with majority voting for noise resilience

  3. 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=0

  • 64-byte permutation table at 0x42f280

Cipher algorithm:

Side channel mechanism: Each bit function uses a Spectre-RSB gadget:

  1. Flushes two probe cache lines (r8, r9)

  2. Calls a training function that pushes a fake return address and clflushes it from the stack

  3. The CPU's Return Stack Buffer mispredicts the return target, speculatively executing the lookup table read and buffer access

  4. Timing measurement via rdtscp determines 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 branching

  • SIGILL handler at 0x80490c7 โ€” handles other control flow

  • Lookup tables at 0x8066f30 (addition), 0x81a7a80 (XOR), 0x8197570 (AND), 0x81fbb70 (identity/zero-extend)

  • Three working "registers" at 0x8053040, 0x8053044, 0x805304c

Flag extraction approach:

  1. First cmp at 0x8049689 checks argc == 2 (need exactly one argument)

  2. Second cmp at 0x80515ac checks 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:

    • .data VA 0x8057010 is file offset 0x00f010 (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 i is first influenced by input index (i ^ 1) (adjacent swap), so we can solve bytes incrementally without breaking previously-matched output.

Approach:

  1. Extract the expected 28 bytes from the binary.

  2. Use GDB to dump the transformed 28-byte buffer right before the result printf call.

  3. 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 prefix out[:i+1] matches the expected prefix.

  4. 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:

  1. AAEncode (outer layer) - Japanese-style JS encoding using Unicode characters like ๏พŸฯ‰๏พŸ๏พ‰, ๏พŸะ”๏พŸ, etc. This encodes JavaScript using only non-ASCII emoticon-like characters.

  2. 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..N

  • Use eighth notes to emit O..Z (the sheet never needs values beyond Z)

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 current to output

Solution

output ends each round with 20 bytes of SHA-1. Going backwards, at each step:

  1. split blob as body | digest where digest is last 20 bytes

  2. try each of the 4 decoders on body

  3. keep only decodings where sha1(decoded) == digest

  4. recurse 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:

  1. Both flags share the same format prefix 0xfun{, suffix }, and length (41).

  2. For a comparison like flag[pos] <= mid, if both universes agree, the output matches. If they disagree, output diverges.

  3. The server reports per-universe execution times (time_a, time_b), so time.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. Check time_a vs time_b to 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

  1. Confirm SSTI with {{7*7}} and observe it evaluates server-side.

  2. The input is validated as an email, which blocks many characters in a normal local-part. The bypass is that email-validator accepts RFC 5322 "name-addr" syntax: Display Name <addr@domain>.

  3. Put the SSTI in the display-name and keep the actual address inside <...> simple. Example: {{7*7}} <[email protected]>.

  4. Quote the display-name to allow characters like spaces/parentheses, enabling function calls. Example: "{{lipsum()}}" <[email protected]>.

  5. Use Jinja's built-in lipsum function to reach Python globals (lipsum.__globals__ includes os) 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:

  1. SQL Injection in /search: The item parameter is directly concatenated into the SQL query:

  2. Broken authentication in is_logged_in(): The function iterates over all users and checks if sha256(...).hexdigest() - but sha256().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 the user cookie value.

  3. File read in /user: Reads users/<userID> where userID comes from a cookie (but is cast to int, 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

  1. Leak staff JWT via GraphQL Relay node

    • The GraphQL node(id: ...) interface exposes StaffNode.accessToken.

    • Relay global ID for officer_chen is base64("StaffNode:2").

  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.

  3. 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 accepts HS256. If we sign with HS256 using the RSA public key DER bytes as the HMAC secret, verification succeeds.

  4. Bypass the gateway /internal/ block (CL-TE request smuggling)

    • The gateway frames request bodies using Content-Length, while Hypercorn/h11 honors Transfer-Encoding: chunked.

    • Send a front request like:

      • POST /graphql with both Content-Length: X and Transfer-Encoding: chunked

      • body begins with 0\r\n\r\n (ends chunked body) followed by a full smuggled POST /internal/upload ...

    • The backend processes the smuggled internal request, but the gateway associates that response with the next client request (response queue poisoning).

  5. Turn arbitrary file write into code execution

    • The container creates the venv with --system-site-packages, which makes site.ENABLE_USER_SITE = True.

    • That means Python auto-imports usercustomize from the user site-packages path:

      • /home/skyport/.local/lib/python3.11/site-packages/usercustomize.py

    • Upload a malicious usercustomize.py that 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 imports usercustomize and executes the payload.

  6. 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:

  1. Forge an SSO id_token:

    • Server claims to accept RS256, but verifies with HMAC-SHA256.

    • It also accepts jku and fetches attacker-controlled JWKS, caching a symmetric k per kid.

    • Host a JWKS that supplies k, then sign the token with HMAC and log in as the seeded SSO user (nora.v).

  2. Escalate to reviewer:

    • Reviewer grant verification reads HMAC key bytes from KEYS_DIR / f"{kid}.pem".

    • Upload a .pem file into /var/app/review-materials/โ€ฆ and set kid=../review-materials/<stem> to path-traverse out of KEYS_DIR.

    • Sign a grant JWT with the uploaded bytes; /review/escalate sets session role to reviewer.

  3. Use the admin bot to exfil a valid workspace_key:

    • As reviewer, /report queues 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 calls history.back().

    • The /admin pageโ€™s admin.js has a pageshow handler that detects back/forward navigation and redirects to cb?workspace_key=โ€ฆ.

    • Capture the workspace_key from that request.

  4. RCE through admin import pipeline:

    • /admin/upload stores an uploaded artifact when X-Workspace-Key is valid.

    • /admin/xml/import requires XInclude file://โ€ฆ 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.txt to /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