🇳🇿VuwCTF 2025

Writeups for most of the challenges in VuwCTF 2025 hosted by Victoria University of Wellington in New Zealand

Starting now, my writeups will be heavily AI assisted so there may be some quality loss compared to previous ones, but I need to do it this way in the future so it's sustainable for me. Also check out krauq.ai, a free online CTF solver (AI chat with built-in tools, recipes, etc.). Now officially launched, no longer in beta.

Forensics

Matroiska

Category: Forensics Points: 100 Difficulty: Easy

We're given a PNG file matroiska1.png. The name hints at Russian nesting dolls (matryoshka), suggesting hidden layers.

Initial Analysis

Using binwalk or checking the file manually reveals extra data appended after the PNG's IEND chunk:

data = open('matroiska1.png', 'rb').read()
iend_pos = data.find(b'IEND')
hidden = data[iend_pos + 8:]  # Skip IEND + CRC
print(hidden[:50].hex())
# e53137227f38766b7965723f25293d3772326cd379657358...

Looking at the hidden data, we notice readable ASCII fragments like vkyer and yesXdgyer. These look like corrupted/encoded text - possibly XOR'd data where some bytes remain in printable range.

Finding the XOR Key

If this hidden data is actually another PNG, we can derive the XOR key by comparing against a known PNG header:

png_header = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,  # PNG signature
                    0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52]) # IHDR chunk

for i in range(16):
    key_byte = hidden[i] ^ png_header[i]
    print(chr(key_byte), end='')
# Output: layer2layer2laye

The XOR key is layer2 repeating.

Extracting All Layers

Each nested PNG uses an incrementing key (layer2, layer3, etc.):

This extracts:

  • layer2.png (178x362)

  • layer3.png

  • layer4.png

  • layer5.png (236x88)

Flag

The final layer (layer5.png) contains the flag as visible text in the image:


1.5x-engineer1

Category: Forensics Points: 356 Difficulty: Medium

Description

I had a dream where there were no standards for securely sending data over networks! Terrifying. Anyway one of my colleagues wanted to show off their new project and hid a flag!

Solution

Step 1: Analyze the PCAP

Opening 1.5x-engineer.pcapng in Wireshark, we find UDP traffic on port 9897 between two hosts:

  • 192.168.1.182 (victim)

  • 192.168.1.237 (C2 server)

Step 2: Identify the Sessions

The exfiltration uses multiple "sessions" indicated by the first byte of each UDP payload:

  • Session 1: Small metadata/status packets (69 bytes)

  • Session 2: Main data exfiltration (975 × 417 bytes + 1 × 129 bytes)

Step 3: Find the Part 1 Flag

The Session 1 packets contain ASCII text encoded in BCD format. Extracting and decoding the 69-byte packets:

The decoded Session 1 messages reveal:

Within the transmission metadata, we find the Part 1 flag hidden in the ASCII content.

Flag

Key Takeaways

  • The "1.5x" in the challenge name refers to the BCD encoding scheme: 3 bytes encode 2 bytes of data (ratio 1.5:1)

  • Session 1 contains metadata and the Part 1 flag

  • Session 2 contains the encrypted DOCX (Part 2)


Jellycat

Category: Forensics Points: 451 Difficulty: Easy

Description

A Windows memory dump containing a fake "firefox.exe" malware that displays ASCII art of a cat and encodes/decodes a flag.

Solution

  1. Extract the malware binary from memory:

  1. Find the encoded string from command line:

  1. Reverse engineer the binary:

Disassembling firefox.exe with radare2 revealed the cipher at 0x7ff6c3091539:

  • Subtract 0x32 from each ciphertext byte

  • XOR with jellycat ASCII art (cycling through 406 bytes)

  1. Decode:

Key Takeaway: The cipher tables found in strings were red herrings. The actual algorithm required extracting and reversing the malware binary. The flag references cnidaria (jellyfish phylum) + catus (cat) = jellycat.

Flag

VuwCTF{cnidaria_catus}


Undercut

Category: Forensics Points: 491 Difficulty: Medium

Description

A disk image forensics challenge with a hint "LLMs only" on the USB label. Contains a 50MB disk image with 6 FAT16 partitions.

Solution

The hint "LLMs only" and title "undercut" suggest we shouldn't focus on "GPT" (the AI) but rather GPT (GUID Partition Table). The flag is hidden in the partition GUIDs themselves.

Step 1: Extract the Partition GUIDs

The first partition's GUID starts with BZh11AY&SY - the magic header for bzip2 compressed data!

Step 2: Decompress with bzip2

Step 3: Decode ASCII85

Summary: The challenge was a clever play on the acronym "GPT" - the partition GUIDs in the GUID Partition Table contained bzip2-compressed, ASCII85-encoded flag data.

Flag

VuwCTF{1m_n0t_t4lk1ng_ab0ut_th4t_gpt}


PWN

Fruit Ninja

Category: PWN Points: 100 Difficulty: Easy

Description

A heap exploitation challenge featuring a fruit-slicing game with a Use-After-Free vulnerability.

Solution

Vulnerability: Use-After-Free in throw_away_fruit(): after freeing a fruit chunk, the pointer in fruit_basket[index] is not nulled out, leaving a dangling pointer accessible via edit_fruit().

Win Condition: perform_special_action() reads the flag if strcmp(leaderboard, "Admin") == 0.

Exploitation: Both fruits and the leaderboard are allocated as 0x24-byte chunks, so they share the same tcache bin.

  1. Slice a fruit → fruit_basket[0] = chunk_A

  2. Throw away fruit 0 → chunk_A goes to tcache, but fruit_basket[0] still points to it

  3. Reset leaderboard → malloc returns chunk_A for the new leaderboard

  4. Edit fruit 0 with "Admin" → UAF writes to leaderboard (same chunk)

  5. Special action → flag

Solve Script:

Flag

VuwCTF{fr33_th3_h34p_sl1c3_th3_fr00t}


Tōkaidō

Category: PWN Points: 100 Difficulty: Easy

Description

Buffer overflow with PIE bypass, requiring double return to win function.

Solution

Vulnerability:

  • gets() on a 16-byte buffer - classic stack overflow

  • No stack canary

  • PIE enabled, but main's address is leaked

The Trick: The win() function checks if (attempts++ > 0) before printing the flag. Since attempts starts at 0, we need to call win() twice:

  1. First call: attempts is 0 → prints "not attempted", increments to 1

  2. Second call: attempts is 1 → prints the flag

Exploit:

Flag

VuwCTF{eastern_sea_route}


Kiwiphone

Category: PWN Points: 400 Difficulty: Medium

Description

An off-by-one index error in a phonebook application allows stack corruption and ROP chain execution.

Solution

Vulnerability: Off-by-one index error in kiwiphone.c:109:

When the user enters index 0, the program writes to entries[-1], which overlaps with the phonebook.size field.

Exploitation:

  1. Corrupt size: Write to index 0 with +48 0 0-0 to set phonebook.size = 48

  2. Leak stack data: The program now prints 48 entries, leaking stack canary, saved RBP, and return address (libc)

  3. Calculate libc base: libc_base = ret_addr - 0x2a1ca

  4. Write ROP chain: Overwrite entries 17-22 with: [canary] [saved_rbp] [ret] [pop_rdi] [/bin/sh] [system]

  5. Trigger: Exit with -1 to return through our ROP chain

Key parts of solve script:

Flag

VuwCTF{c0nv3nient1y_3vil_kiwi_nuMb3r_f0rMatt1nG}


Blazingly Fast Memory Unsafe

Category: PWN Points: 475 Difficulty: Hard

Description

A Brainfuck JIT compiler with an unbalanced bracket vulnerability allowing arbitrary code execution.

Solution

Vulnerability: The ] (LOOP_END) instruction pops a return address from the stack and jumps to it if the current cell is non-zero. Unbalanced ] without matching [ pops values pushed during PROLOGUE - specifically the tape address, which resides in RWX memory.

Exploit Strategy:

  1. Stage 1: Write shellcode to tape using BF +/- operations, then trigger jump with ]

  2. Stage 2: Stage 1 calls read(0, tape, 256) to load execve shellcode from stdin

Key Constraint: Max input: 512 bytes. Optimization: use - for bytes >127 (e.g., 0xff costs 1 - instead of 255 +).

Final Payload:

Flag

VuwCTF{rU5tac3Ans_uN1te_agA1n5t_uN5aFe_l4ngUaG3s}


Idempotence

Category: PWN Points: 475 Difficulty: Hard

Description

A lambda calculus interpreter with a type confusion vulnerability.

Solution

The Bug (Line 162): In simplify_normal_order(), when reducing an application (F A):

The code assumes the function is always an ABS (abstraction), but it could be a VAR (variable). When a VAR is forced to ABS type, its bytes 16-23 (normally unused for VAR) are interpreted as the body pointer.

The UNKNOWN_DATA Leak: When print_expression() encounters an unknown type (>2), it dumps raw bytes. The flag starts with "VuwC" = 0x43777556 which is >2, triggering this path.

The Magic Expression:

Exploit Code:

Flag

VuwCTF{untyp3dCNFu5ioN}


Crypto

Delicious Cooking

Category: Crypto Points: 176 Difficulty: Easy

Description

Recover the password for user meatballfan19274 on a cooking forum.

Solution

The challenge provides a SQLite database users.db with a users table containing username, password (format: hash$salt), and security_q.

Examining the target user:

Key Observations:

  1. Ratatouille theme: All security questions are quotes from the movie Ratatouille

  2. Password hints: Some users had revealing security questions like "fav movie + bank pin"

The hash algorithm is SHA256(password_bytes + salt_bytes) where the salt is hex-decoded before concatenation.

Solution:

Flag

VuwCTF{ratatouille6281}


Totally Random Art

Category: Crypto Points: 275 Difficulty: Medium

Description

Recover a flag from ASCII art generated by a random walk algorithm.

Solution

Key Insight: The flag format is VuwCTF{...} (18 bytes). The first 4 bytes (VuwC) seed Python's random.Random(), making the random walk deterministic for any given flag content.

Algorithm Analysis from randart.py:

  1. Seed RNG with first 4 bytes of input

  2. For each remaining byte: steps, stroke = divmod(byte, 16)

  3. Random walk steps times on a 10×5 grid (8 directions, with reroll on revisit)

  4. Add stroke to landing cell (mod 16)

  5. Render using palette .:-=+*#%@oT0w&8R

Solution: Since the seed is fixed (VuwC) and we know the format (TF{ + 10 unknown chars + }), we can brute-force the 10-character body using hill climbing + exhaustive search.

The search converged:

  • r4ndM0_4p4 → 47/50 matches

  • r4nd0M_4RT → 50/50 matches

Flag

VuwCTF{r4nd0M_4RT}


Unorthodox IV

Category: Crypto Points: 500 Difficulty: Hard

Challenge Overview

The challenge presents a remote service that encrypts user input using some cipher with a randomized IV/mode. On each connection, we receive the encrypted flag and can submit our own plaintexts to be encrypted.

Key Observations

  1. 25 Random Modes: Each encryption randomly selects one of 25 different modes/IVs

  2. Mode Matching: When the same mode is used, identical plaintext prefixes produce identical ciphertext prefixes

  3. Per-Connection Flag: Each connection encrypts the flag with a randomly selected mode

  4. Mode Reachability: Not all connections can "reach" the flag's mode - we may need to reconnect

Attack Strategy

The attack is a byte-by-byte oracle attack:

  1. Connect and get the encrypted flag

  2. For each unknown character position:

    • First, probe to check if this connection can reach the flag's encryption mode (by checking if enc[:known_len] == flag[:known_len])

    • If not reachable after 50 attempts, reconnect

    • Once reachable, test each candidate character

    • When mode matches: if the next byte also matches, we found the character; otherwise eliminate that candidate

  3. Repeat until the full flag is recovered

Solution

How It Works

  1. Mode Reachability Check: Before testing candidates, we send 50 probes to see if this connection can even reach the flag's encryption mode. This saves time on "dead" connections.

  2. Elimination Strategy: When we get a mode match but the character byte doesn't match, we permanently eliminate that candidate. This information persists across reconnections.

  3. Cycling Through Candidates: We cycle through remaining candidates until we hit a mode match that confirms the correct character.

Flag


Web

Go Go Cyber Ranger

Category: Web Points: 100 Difficulty: Medium

Description

A Go web application with chained vulnerabilities: buffer overflow via rune/byte mismatch and command injection in flag check.

Solution

Vulnerability 1: Buffer Overflow via Rune/Byte Mismatch

The application validates input length using runes but copies using bytes:

Multi-byte UTF-8 characters (like emoji) count as 1 rune but occupy 4 bytes.

Vulnerability 2: Command Injection

Exploit Script:

Flag

VuwCTF{k33p_y03r_Go_M3mory_safe}


Just Upload It

Category: Web Points: 100 Difficulty: Easy

Description

A "Secure Image Uploader v2.1" that claims to use "magic number detection" and only accepts PNG files.

Solution

The upload functionality was a red herring. The actual vulnerability was path traversal in the /images/ endpoint.

URL-encoded path traversal bypassed the directory restriction:

The ..%2f (URL-encoded ../) allowed escaping the images directory to read the flag file.

Flag

VuwCTF{Just_up10d_ITl_ol}


Fishsite

Category: Web Points: 211 Difficulty: Medium

Description

A Flask web application with a login form vulnerable to SQL injection.

Solution

Vulnerable Code (fishsite.py:20):

Step 1: Bypass Login

Step 2: Discover the Flag Table

Step 3: Extract the Flag (Blind SQLi with Binary Search)

Flag

VuwCTF{h3art_0v_p3ar1}


Hangdle

Category: Web Points: 400 Difficulty: Medium

Description

A Wordle/Hangman-style game with prototype pollution and Pug template injection vulnerabilities.

Solution

Vulnerability 1: Prototype Pollution via Lodash

The application uses Lodash 4.17.4, vulnerable to prototype pollution through _.merge():

Vulnerability 2: Pug AST Injection

When visiting nodes, if a node doesn't have a block property, JavaScript looks up the prototype chain. Polluting Object.prototype.block with a malicious AST node injects code into the compiled template.

Exploit:

Flag

VuwCTF{the_wordle_answer_on_april_27_2025_was_weedy}


Reversing

Missing Function

Category: Reversing Points: 100 Difficulty: Easy

Description

I'm trying to find out how this program verifies the flag but I can't find the function it's calling anywhere!

Analysis

We're given a stripped ELF binary flag_verifier. Running file on it:

When we run the binary, it prompts for a flag and validates it:

Finding the Hidden Function

Disassembling the main function reveals something interesting - it uses mmap to allocate executable memory:

The program then copies data from the .data section into this executable region and calls it:

The "missing function" is actually shellcode embedded in the data section!

Extracting the Shellcode

We can dump the .data section to find the embedded code:

Disassembling the Verification Function

Extracting and disassembling the shellcode:

Understanding the Algorithm

The verification function:

  1. Checks that input length is exactly 29 bytes (0x1d)

  2. Stores encrypted flag data on the stack using overlapping writes

  3. Uses a 3-byte XOR key: [0x83, 0xf1, 0xa0]

  4. For each character position, XORs the encrypted byte with key[i % 3] and compares to input

Solution

Flag


Ngawari VM

Category: Reversing Points: 176 Difficulty: Easy

Description

A custom VM (ngawari_vm) that implements a Pushdown Automaton (PDA) - a state machine with a stack. It reads bytecode from flag_checker.txt and validates user input.

Solution

VM Format:

  • First line: <initial_state><initial_stack_symbol><accepting_states>

  • Instruction lines: <state><input><stack_top><new_state><push_chars>

Solution Approach:

  1. Parsed the PDA instructions from the bytecode file

  2. Used BFS to find an input string that successfully transitions through the automaton and ends in accepting state

Solver (key part):

Gotcha: The challenge file had CRLF line endings, causing \r to be included in push strings.

Flag

VuwCTF{VuwCTF_1s_s0_c00l_innit}


A New Machine

Category: Reversing Points: 356 Difficulty: Easy

Description

A Python bytecode file compiled with Python 3.14.0a4 (magic bytes 1d 0e 0d 0a).

Solution

The bytecode can't run on standard Python versions due to format changes between alpha and release. Built Python 3.14.0a4 from source in Docker to disassemble it.

Flag validation logic revealed:

The tuple (10201, 12996, 11025, 12100) are squared ASCII values: 101²=e, 114²=r, 105²=i, 110²=n → "erin"

Combining: sss + lith + erin + g = "slithering"

Flag

VuwCTF{ssslithering}


String Inspector

Category: Reversing Points: 400 Difficulty: Hard

Description

A statically-linked binary that validates a flag by repeatedly calling itself via execve syscall, subtracting a constant each iteration.

Solution

The binary expects a flag in format VuwCTF{XXXXXXXXXXXXX} (13 digits inside).

Key constants found in disassembly:

  • Subtraction value: 84673 (at 0x4017f8)

  • Target counter: 319993 (checked at 0x401989)

  • Target remainder: 42 (checked at 0x47f052)

Algorithm:

  1. Extract 13-digit number from flag

  2. Recursively subtract 84673 via self-execve calls

  3. Accept when: counter == 319993 AND remainder == 42

Solution:

Flag

VuwCTF{0027094767331}


Classy People Dont Debug

Category: Reversing Points: 400 Difficulty: Hard

Description

A stripped ELF binary with heavy anti-debugging that prompts for a flag and checks if it's correct.

Solution

Anti-Debugging Techniques:

  1. ptrace self-trace

  2. Watchdog process checking TracerPid

  3. Memory map inspection for Frida/ASan

  4. Parent process check for debuggers

  5. Timing checks

  6. VM detection

  7. Code integrity check (SHA256)

Main Flag Checking Logic:

Understanding sub_402f88:

Solution Script:

Flag

VuwCTF{very_classy_d0'nt_6ou_s33}


Trianglification

Category: Reversing Points: 484 Difficulty: Easy

Description

No description available in notes.

Solution

Understanding the Encryption

Reversing the binary reveals it's an image encryption tool using OpenCV. The encryption scheme:

  1. Divides the image into 5 regions based on a triangle with vertices at (89,44), (49,124), (129,124)

  2. The triangle is subdivided by midpoints into regions: above, left, right, under, and inside

  3. Each region has a random mask value (0-255)

  4. For each pixel at (x,y), the XOR key is computed as: key = (mask * x - y) & 0xFF

  5. Pixels in overlapping regions XOR their masks together

Breaking the Encryption

The key insight is that natural images have smooth gradients - neighboring pixels have similar values. We can exploit this to recover the masks:

  1. Identify "pure" pixels - pixels that belong to exactly one region (for clean mask recovery)

  2. Brute-force each mask - for each region, try all 256 possible mask values

  3. Score by smoothness - decrypt sample pixels and measure the difference between neighboring pixels; the correct mask produces the smoothest result

Full Decryption

Once masks are recovered, decrypt each pixel:

The decrypted image reveals an elephant with the flag text overlaid.

Flag

VuwCTF{The_L3phant_1s_TRiang1efied}


Math Solver

Category: Reversing Points: 484 Difficulty: Medium

Description

A stripped, statically-linked ELF binary containing an encrypted flag and a constraint-based math puzzle on an 11×11 grid.

Solution

The binary contains a flag format string: VuwCTF{m4th_when_%08lX_acr0ss_%02d_is_aw3s0ME}

Grid Structure: An 11×11 grid with special byte values:

  • 0xf9 = wall/boundary

  • 0xfa = empty cell (to be filled)

  • 0xfb = division operator (/)

  • 0xfc = multiplication operator (*)

  • 0xfd = subtraction operator (-)

  • 0xfe = addition operator (+)

  • 0xff = constraint marker

Solving the constraint system algebraically:

Flag

VuwCTF{m4th_when_95E68BBF_acr0ss_67_is_aw3s0ME}


OSINT

Computneter

Category: OSINT Points: 100 Difficulty: Easy

Description

i found this in e-waste what is it

Flag format is VuwCTF{Manufacturer_Model} and is case insensitive.

Solution

The battery has a number that can be looked up, checked compatible models.

Flag

VuwCTF{ASUS_G550JK}


Rogue

Category: OSINT Points: Easy Difficulty: 275

Description

Our backend dev's gone rogue and started selling a bunch of our flags! I tried to trick him, but he's too good. I know he's doing it though!

This flag is case sensitive.

Solution

Search the username in the email on github and find a page with a .github. Check the page source and find a comment with the flag.

Flag

Flag not recorded


It's News!

Category: OSINT Points: 500 Difficulty: Medium

Description

I took this photo of a newspaper clipping on campus. Can you help me ID it, and tell me which group keeps it safe?

Hint: I took this photo of a newspaper clipping on campus on my iPhone. Can you help me ID it, and tell me which group keeps it safe?

Hint 2: This challenge is as easy as 一, 二, 三

Flag format is VuwCTF{dd_mm_yyyy_originalpublicationname_currentcustodialorg} and is case insensitive

Solution

Looking up the text, can find a link to the actual clipping for the date. https://paperspast.natlib.govt.nz/newspapers/NZTIM18760728.2.26.6

Looking at the exifdata on the image, can see the GPS location, looking at the organizations on the university site we can see one at that location. Or AI solves with the hint.

Flag

VuwCTF{28_07_1876_newzealandtimes_waiteatapress}


Misc

Discord

Category: Misc Points: 100 Difficulty: Easy

Description

there's more lurking in the discord than just tickets, and people, and event updates, and news, and first blood trackers, and solv-

https://discord.gg/jaKK2UXnbE

Solution

Go to solve-stream channel, flag in description.

Flag

VuwCTF{can_you_spot_yourself_here?}


AutomatonCSC

Category: Misc Points: 100 Difficulty: Easy

Description

Robotnic did some Vibe Coding and accidentally created an disloyal automaton which is trying to access his secrets. Luckily he coded his website to stop it... for now.

Solution

  1. Check robots.txt

  2. View source code

  3. Navigate to: https://automatoncsc.challenges.2025.vuwctf.com/robotnics_home_7x9k2m/flag.txt

Response: "Nooo! My plans have been spoiled :("

Flag

VuwCTF{We_love_you_NZCSC!!!}


Category: Misc Points: 100 Difficulty: Easy

Description

A challenge involving network services and fortune quotes.

Solution

The challenge hints at port 17, which is the QOTD (Quote of the Day) protocol. The "512 octets" reference confirms this (RFC 865 spec).

Simply connect multiple times until the flag appears:

The service returns random fortunes, one of which contains the flag.

Flag

VuwCTF{om_nom_nom_bytes}


Not Turing Complete

Category: Misc Points: 436 Difficulty: Hard

Description

Implement xxhash32 in a very limited programming language with only 3 variables (a, b, c), basic operators (+, -, *, /, ^, &, |), and no control flow.

Solution

Key Insights:

  1. Python's Arbitrary Precision Integers: Pack multiple values into a single variable at different bit positions

  2. Implementing Rotation with Arithmetic: rotl32(x, n) = ((x * 2^n) & MASK32) + (x / 2^(32-n))

  3. State Packing: Store running hash in high bits of a (at 2^256 offset) while keeping input in low bits

The solution generates 146 lines of NTC code that correctly implements xxhash32:

Running:

Flag

VuwCTF{Tur1NG_w4s_r1ght_0Oa0}

Last updated

Was this helpful?