FallCTF 25 (solved)
Today I walked through several CTF challenges across forensics, crypto, web, and binary exploitation, progressively deepening my debugging and reversing workflow. Here’s a summary of what I learned and how I solved each challenge.
Forensics Challenge — “Find the real email”
Goal: Identify which .eml file contained the legitimate flag email among 100+ spoofed ones.
Approach & Tools:
- Used Python to parse all
.emlheaders and check for authentication markers likeReceived-SPF: pass. - Found that all 100 were SPF-valid, so I built a Python script to inspect other email metadata.
- The solution relied on detecting the unique valid email signature (not guessing).
Key Steps:
- Parsed each
.emlusingemailandrelibraries. - Compared headers like
From,Message-ID, and body content patterns. - The script isolated the correct legitimate message containing the real flag.
Lesson learned:
- In forensics, it’s not about brute-forcing — it’s about recognizing authentic metadata patterns.
Crypto Challenge — “RSA e = 2”
Goal: Decrypt a message encrypted with RSA using an invalid public exponent e = 2.
Approach & Tools:
- Used
sympyandCrypto.Util.numberto attempt factoring and modular square roots. - Since
e=2, ciphertext = plaintext² mod n → so solving means finding modular square roots. - Applied the
gmpy2.iroot()trick after factoringn.
Key Steps:
- Factored
nintopandq. - Reconstructed private key logic for e=2.
- Recovered possible plaintexts (several roots mod n).
- Tested each for readable ASCII.
Lesson learned:
- Small public exponents like 2 are insecure because decryption can reduce to modular square roots.
Pwn Challenge — “admin-switch”
Goal: Exploit a stack buffer overflow to trigger win() and read the flag.
Approach & Tools:
- Used
gets()overflow vulnerability to overwrite the adjacent integeradmin_key. - Set
admin_keyto0x1337to satisfy the admin condition.
Key Steps:
- Found struct layout in C code:
username[32]followed byint admin_key. - Calculated offset: 32 bytes + 4 for alignment → needed
36bytes of input. - Sent payload:
'A'*32 + p32(0x1337)viapwntools.
Lesson learned:
- Classic stack overflow exploiting local struct adjacency — no shellcode, just logic manipulation.
Pwn Challenge — “jumper” (debugging with GDB)
Goal: Debug a segfaulting binary and find the flag hidden in memory.
Approach & Tools:
- Used GDB to step through instructions and inspect registers.
- Analyzed disassembly to identify invalid memory writes.
- Overrode registers to make
win()run safely.
Key Steps:
-
Disassembled
win()and found invalid dereferences (movb $0x2a,(%rax)). -
Patched
%raxto point to writable memory using:set $rax=0x404000 jump *0x401192 -
Continued execution — flag printed successfully.
Lesson learned:
- GDB debugging can repair faulty state dynamically — you can “fix” broken binaries in memory.
Web Challenge — “Aggie HTTP”
Goal: Interact with a custom HTTP variant “AHTTP/1.1” and retrieve the flag.
Approach & Tools:
- Used
openssl s_clientandcurlto manually craft raw requests. - Initially discovered redirect to
/flag, but server required custom headers. - Sequential hints revealed the required header (
Flag: Please) and JSON body ({"flag":"yes"}).
Key Steps:
-
Tested requests manually:
printf 'POST /flag AHTTP/1.1\r\nHost: aggie-http\r\nFlag: Please\r\nContent-Type: application/json\r\n\r\n{"flag":"yes"}\r\n' \ | openssl s_client -quiet -connect fallctf.cybr.club:443 -servername aggie-http -
Server returned
gigem{...}flag.
Lesson learned:
- Understanding HTTP fundamentals (headers, methods, JSON structure) lets you adapt to protocol quirks.
Web / XXE Challenge — “Slop Shoveler”
Goal: Exploit an XML endpoint vulnerable to XXE (XML External Entity Injection).
Approach & Tools:
lxmlwithresolve_entities=True→ confirmed XXE vulnerability.- Crafted payload referencing
/opt/flag.txtvia afile://entity. - Sent via
curlPOST to/load-image.
Key Steps:
curl -X POST https://slop-shoveler.fallctf.cybr.club/load-image \
-H "Content-Type: application/xml" \
-d '<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///opt/flag.txt">]>
<image><id>&xxe;</id></image>'
Lesson learned:
- Whenever XML parsing uses external entity resolution, it’s a classic XXE path traversal vector.
Git Forensics — “Gitting tired”
Goal: Recover a hidden flag from a .git directory.
Approach & Tools:
- Used
grepdirectly on the repo’s object files.
Key Steps:
grep -r "gigem{" src/.git/
- Flag found immediately inside one of the object blobs.
Lesson learned:
.gitleaks often contain full historical data — always check objects and commits, not just the source tree.
Pwn Challenge — “stranded”
Goal: Gain control of function pointer to call home() instead of crashing.
Approach & Tools:
- Remote binary prints address of
vuln(). - Used pwntools to calculate relative offset to
home().
Key Steps:
- Extracted address of
vuln()from output. - Computed target = vuln + (home - vuln) offset.
- Sent that address back as input.
- Program jumped to
home()→ flag printed.
Lesson learned:
- Function pointer hijacking with known address leak → relative ret2win exploit.
Crypto Challenge (skipped mid-way) — “RSA e=2 extended”
Status: Incomplete
-
Tried reconstructing plaintexts from multiple modular roots, but they didn’t decode cleanly.
-
Learned to consider:
e=2attacks rely on finding modular square roots.- When roots fail to produce valid text, additional CRT combination or padding stripping may be needed.
Misc Binary (in progress) — “uh-what” (file signature recovery)
Goal: Rebuild corrupted file from byte patterns.
Progress:
- Discovered
- header
GILF→ tried reconstructing known magic numbers (JPEGFFD8, PNG89504E47, BMP424D, etc.). - Organizer hint pointed to file signature matching.
- Managed partial reconstruction of BMP header but file didn’t render correctly — likely remaining XOR or offset corruption.
Lesson learned:
- Magic bytes (
file(1)+ Wikipedia file signatures) are essential for forensics reconstruction.
Reversing / Crackme — “crackme1”
Goal: Recover or bypass password check to access flag.
Approach & Tools:
- Used Ghidra (decompiler) to inspect binary logic.
- Found comparison:
strcmp(user, "tamu_admin")andstrcmp(hexhash, admin_hash). - Plan: Patch conditional jump (
jne→jmp) to always succeed.
Key Steps (to be done after Ghidra install):
- Analyze
main()and identify conditional. - Patch or bypass in assembly (or
gdbcall to success path).
Lesson learned:
- Decompilers like Ghidra simplify reversing — and conditional patching can turn authentication into automatic success.
Overall Takeaways
| Category | Skill Learned | Key Tool |
|---|---|---|
| Forensics | Header analysis & authenticity detection | Python (email, re) |
| Crypto | Weak exponent RSA & modular math | sympy, Crypto.Util.number |
| Binary Exploitation | Stack overflow → struct overwrite | pwntools, gdb |
| Reverse Engineering | Function disassembly & patching | Ghidra, radare2 |
| Web Exploitation | Protocol tampering & XXE payloads | curl, openssl s_client |
| Version Control Forensics | Git history analysis | grep, .git/objects |
Reflection
Today’s session helped me combine binary analysis, Python scripting, manual protocol crafting, and forensics intuition.
Each challenge was small but reinforced one principle:
“CTFs aren’t about guessing — they’re about observing patterns and reasoning from structure.”