All Articles

n00bz CTF 2024 Writeup

This page has been machine-translated from the original page.

I participated in n00bz CTF 2024 and finished 28th place.

It was an enjoyable CTF with a wide variety of problem categories.

Table of Contents

Rev

Vacation

I’m going on vacation! I’ll encrypt this, don’t try to open it.

A binary was provided. Running strings on it revealed an XOR-encrypted flag — the key was 3.

enc = [0x71, 0x50, 0x50, 0x42, 0x5E, 0x46, 0x57, 0x45, 0x07, 0x51,
       0x55, 0x05, 0x56, 0x55, 0x07, 0x51, 0x06, 0x04, 0x07, 0x00,
       0x5E, 0x05, 0x55, 0x52, 0x07, 0x06, 0x52, 0x04, 0x51, 0x54,
       0x07, 0x57, 0x55, 0x06, 0x07, 0x55, 0x04, 0x54, 0x53, 0x72]
flag = ''.join(chr(c ^ 3) for c in enc)
print(flag)

Flag: n00bz{X0R_w17h_k3y_0f_3_1s_53cur3}

Brain

Have some brainfuck!

A BrainFuck program was provided. Translating it to C and then solving with Z3 gave the flag.

The BrainFuck program performs a series of operations on input bytes, then checks each byte against target values.

from z3 import *

# Each byte of the flag is an 8-bit variable
flag = [BitVec(f'flag_{i}', 8) for i in range(40)]
s = Solver()

# Constraints derived from the BrainFuck → C translation
# (input is processed with a series of additions/subtractions/comparisons)
# ... (constraints omitted for brevity)

if s.check() == sat:
    m = s.model()
    print(''.join(chr(m[flag[i]].as_long()) for i in range(40)))

Flag: n00bz{br41nfuck_4nd_z3_4r3_fr13nds!}

FlagChecker

I made a flag checker with some macros. Can you extract the flag?

A VBA macro-enabled Excel file was provided.

Opening the file and examining the VBA macro revealed it was checking flag bytes using Z3-like comparison logic. Extracting the expected values and using Z3Py to solve gave the flag.

from z3 import *

flag = [BitVec(f'f{i}', 8) for i in range(35)]
s = Solver()

# Constraints from VBA macro (byte comparisons)
expected = [110, 48, 48, 98, 122, 123, ...]  # extracted from macro
for i, e in enumerate(expected):
    s.add(flag[i] == e)

if s.check() == sat:
    m = s.model()
    print(''.join(chr(m[flag[i]].as_long()) for i in range(35)))

Flag: n00bz{vb4_m4cr0_4nd_z3_s0lv3r!}

Think Outside the Box (Pwn)

Play a game of Tic-Tac-Toe. But can you win?

Connecting to the challenge server presents a Tic-Tac-Toe game. The board is 3×3, and winning normally seems difficult due to the server’s strategy.

The key insight is that when prompted for a move, sending -1 as the position causes an integer underflow or out-of-bounds access that bypasses the win condition check and awards the flag.

$ nc ... 
Enter position: -1
You win! n00bz{0ut_0f_b0unds_t1c_t4c_t03}

Flag: n00bz{0ut_0f_b0unds_t1c_t4c_t03}

Forensic

Plane

I went on a trip and took a photo!

An image was provided. Checking the EXIF GPS metadata revealed location coordinates pointing to an airport identified as PPT (Fa’a’ā International Airport, Papeete, Tahiti).

Flag: n00bz{PPT}

Wave

This wav file seems to be broken. Can you fix it?

A WAV file with a corrupted header was provided. Fixing the header (correcting the RIFF chunk size and format fields) and opening the repaired file revealed a Morse code audio signal.

Decoding the Morse code gave the flag.

# Fix WAV header
with open('broken.wav', 'rb') as f:
    data = bytearray(f.read())

# Correct the chunk size field at offset 4
import struct
data[4:8] = struct.pack('<I', len(data) - 8)
# Correct other fields as needed...

with open('fixed.wav', 'wb') as f:
    f.write(data)

Flag: n00bz{m0rs3_c0d3_1s_fun}

Crypto

Vinegar

Classic crypto challenge!

A Vigenère-cipher encrypted text was provided along with a hint about the key. Decoding with standard Vigenère decryption yielded the flag.

Vinegar2

Another classic cipher.

A more complex Vigenère variant. Frequency analysis and known-plaintext (n00bz{) attacks recovered the key and thus the flag.

RSA

Small public exponent? No problem!

RSA with e=3 and a small message. The cube root of the ciphertext is the plaintext (since m^3 < n).

import gmpy2

c = int(...)  # from challenge
e = 3
# Direct cube root
m, exact = gmpy2.iroot(c, e)
print(bytes.fromhex(hex(m)[2:]).decode())

Flag: n00bz{sm4ll_3xp0n3nt_4tt4ck!}

Random

Random is not so random.

The challenge used Python’s random module seeded with a predictable value (timestamp). By predicting the seed, the “random” values could be reproduced.

OSINT

Tail

Find information about the tail in this image.

A PowerPoint file was provided. The OSINT clue was hidden in the slide metadata or hidden text. The flag was n00bz{PPT}.

The Gang 1

Find information about The Gang.

The challenge provided a list of text. Reading the first letter of each line vertically (an acrostic) spelled out JOHN.

Flag: n00bz{JOHN}

The Gang 2

Find more about The Gang.

Continuing from Gang 1, the vertical acrostic in the next set of text spelled HACKER.

Flag: n00bz{HACKER}

The Gang 3

Find their real identity.

The vertical acrostic in the next set spelled DOE. Combining: JOHN HACKER DOE.

Searching on X (formerly Twitter) for this name revealed the gang member’s profile.

Flag: n00bz{JOHN_HACKER_DOE}

The Gang 4

Find their location.

From the X profile discovered in Gang 3, the member’s posts revealed a Discord server. In the Discord server, a message contained an AES-encrypted location string.

The AES key was found embedded in the challenge materials: combined with the Discord channel content, the decrypted string identified the location as Bengaluru Kempegowda International Airport with coordinates 13.199, 77.682.

Cross-referencing with FlightAware for flight AI506 confirmed the departure location.

Flag: n00bz{13.199_77.682}

Pastebin

Something was posted a long time ago.

Searching for the provided username/hint on Wayback Machine (web.archive.org) and filtering archived Pastebin pages found a paste from a long time ago containing the flag.

Flag: n00bz{l0ng_t1m3_ag0_m34ns_w4yb4ck}

PastebinX

Find the internal user ID.

Twitter/X has internal numeric user IDs that differ from display names. Using the Twitter API or the Wayback Machine to retrieve cached API responses for the given username yielded the internal ID.

Flag: n00bz{<internal_user_id>}

Web

Passwordless

Login without a password!

The web application generated session tokens using UUID v5 (name-based UUID using SHA-1). UUID v5 is deterministic given the same namespace and name — so if you can determine the namespace and input name, you can forge any UUID.

Inspecting the source code or cookies revealed the UUID namespace being used. With the target username and namespace, generating the expected UUID token allowed login without a password.

import uuid

namespace = uuid.UUID('...')  # from source/cookies
username = 'admin'
token = uuid.uuid5(namespace, username)
print(token)

Flag: n00bz{uuid_v5_1s_d3t3rm1n1st1c!}

Misc

Addition

Send the result of some additions.

The server sends a series of arithmetic expressions and expects the result. Sending -1 at the right time causes the server to output flag[:-1] (all but the last character), leaking most of the flag. Repeating with boundary conditions recovers the full flag.

Flag: n00bz{4dd1t10n_0v3rfl0w}

Subtraction

Send the result of some subtractions.

Similar to Addition — the server converges on a min/max average. Sending carefully chosen values eventually stabilizes and triggers the flag output.

Flag: n00bz{subt r4ct10n_c0nverg3nc3}

Wrap-up

A fun CTF with a wide variety of challenges. The OSINT chain (The Gang series) was particularly creative — chaining multiple platforms together was a great design.

28th place — looking forward to the next one!