🇳🇿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: layer2layer2layeThe XOR key is layer2 repeating.
Extracting All Layers
Each nested PNG uses an incrementing key (layer2, layer3, etc.):
This extracts:
layer2.png(178x362)layer3.pnglayer4.pnglayer5.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
Extract the malware binary from memory:
Find the encoded string from command line:
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)
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.
Slice a fruit →
fruit_basket[0] = chunk_AThrow away fruit 0 →
chunk_Agoes to tcache, butfruit_basket[0]still points to itReset leaderboard → malloc returns
chunk_Afor the new leaderboardEdit fruit 0 with "Admin" → UAF writes to leaderboard (same chunk)
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 overflowNo 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:
First call: attempts is 0 → prints "not attempted", increments to 1
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:
Corrupt size: Write to index 0 with
+48 0 0-0to setphonebook.size = 48Leak stack data: The program now prints 48 entries, leaking stack canary, saved RBP, and return address (libc)
Calculate libc base:
libc_base = ret_addr - 0x2a1caWrite ROP chain: Overwrite entries 17-22 with:
[canary] [saved_rbp] [ret] [pop_rdi] [/bin/sh] [system]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:
Stage 1: Write shellcode to tape using BF
+/-operations, then trigger jump with]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:
Ratatouille theme: All security questions are quotes from the movie Ratatouille
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:
Seed RNG with first 4 bytes of input
For each remaining byte:
steps, stroke = divmod(byte, 16)Random walk
stepstimes on a 10×5 grid (8 directions, with reroll on revisit)Add stroke to landing cell (mod 16)
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
25 Random Modes: Each encryption randomly selects one of 25 different modes/IVs
Mode Matching: When the same mode is used, identical plaintext prefixes produce identical ciphertext prefixes
Per-Connection Flag: Each connection encrypts the flag with a randomly selected mode
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:
Connect and get the encrypted flag
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
Repeat until the full flag is recovered
Solution
How It Works
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.
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.
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:
Checks that input length is exactly 29 bytes (0x1d)
Stores encrypted flag data on the stack using overlapping writes
Uses a 3-byte XOR key:
[0x83, 0xf1, 0xa0]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:
Parsed the PDA instructions from the bytecode file
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:
Extract 13-digit number from flag
Recursively subtract 84673 via self-execve calls
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:
ptrace self-trace
Watchdog process checking TracerPid
Memory map inspection for Frida/ASan
Parent process check for debuggers
Timing checks
VM detection
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:
Divides the image into 5 regions based on a triangle with vertices at (89,44), (49,124), (129,124)
The triangle is subdivided by midpoints into regions: above, left, right, under, and inside
Each region has a random mask value (0-255)
For each pixel at (x,y), the XOR key is computed as:
key = (mask * x - y) & 0xFFPixels 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:
Identify "pure" pixels - pixels that belong to exactly one region (for clean mask recovery)
Brute-force each mask - for each region, try all 256 possible mask values
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-
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
Check
robots.txtView source code
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!!!}
Fortune Cookie
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:
Python's Arbitrary Precision Integers: Pack multiple values into a single variable at different bit positions
Implementing Rotation with Arithmetic:
rotl32(x, n) = ((x * 2^n) & MASK32) + (x / 2^(32-n))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?