PwnCollege - XSS
PwnCollege - CSRF
PwnCollege - note basic
247CTF - The Flag Safe
A secret flag is safely hidden away within client-side scripts. We were told JavaScript is an insecure medium for secret storage, so we decided to use C++ instead.
Analysis
The following radare2 transcript lists only the two exports and first two hashes relevant to the analysis; it is an excerpt, not a runnable solver. The solver below retains all 40 hashes. The original WASM, character-search run and final candidate are unavailable, so only table completeness and Python syntax are checked here.
1 | ❯ r2 flag.wasm |
Obviously the flag is stored as a series of bcrypt hashes, which are
all prefixed with $2b$12$.
Cracking the Flag
If we test single characters, we can see that the flag start with 2, but the next character is unknown.
Since the hashes are cumulative, each one validates the entire flag prefix up to that point. We can recover the flag by brute-forcing each character in sequence and appending it to the previously discovered string.
1 | import bcrypt |
Result
1 | [+] 247CTF{<flag-redacted>} |
247CTF - Flag Errata
We stored the flag within this binary, but made a few errors when trying to print it. Can you make use of these errors to recover the flag?
Overview
1 | ❯ file flag_errata.exe |
A PE64 binary (GCC 7.3 / MinGW-w64) with a monstrous 53KB
main function. It prompts for a 40-character password,
validates each character through error-code accumulation, then returns
-247 on any mismatch.
The core pattern repeats 40 times:
1 | sub_401560(); // intentionally failing WinAPI call |
The password is never stored anywhere. It's implicitly encoded by the count and sequence of WinAPI calls whose error codes reconstruct each character.
The Error Code Cycle
13 functions form a fixed cycle, each designed to fail deterministically:
| # | Function | Address | API Call | Expected Error | Code |
|---|---|---|---|---|---|
| 0 | errgen_open_247ctf_dll_system32 |
0x401560 |
CreateFileA("C:\Windows\System32\247CTF.dll", ...) |
ERROR_FILE_NOT_FOUND |
2 |
| 1 | errgen_open_247ctf_dll_nested |
0x40159B |
CreateFileA("C:\247CTF\247CTF\247CTF.dll", ...) |
ERROR_PATH_NOT_FOUND |
3 |
| 2 | errgen_open_user32_dll_readwrite |
0x4015D6 |
CreateFileA("user32.dll", GENERIC_RW, exclusive) |
ERROR_SHARING_VIOLATION |
32 |
| 3 | errgen_getthreadtimes_null |
0x401611 |
GetThreadTimes(0, 0, 0, 0, 0) |
ERROR_INVALID_HANDLE |
6 |
| 4 | errgen_findfiles_exhaust |
0x401634 |
FindFirstFile("C:\*") + loop
FindNextFile |
ERROR_NO_MORE_FILES |
18 |
| 5 | errgen_virtualqueryex_null |
0x401674 |
VirtualQueryEx(proc, NULL, NULL, 8) |
ERROR_BAD_LENGTH |
24 |
| 6 | errgen_open_cmdexe_twice |
0x401697 |
CreateFileA("cmd.exe") x2 (exclusive) |
ERROR_SHARING_VIOLATION |
32 |
| 7 | errgen_open_user32_invalid_create |
0x401704 |
CreateFileA("user32.dll", ..., dwCreation=0) |
ERROR_INVALID_PARAMETER |
87 |
| 8 | errgen_setconsoledisplaymode |
0x40173F |
SetConsoleDisplayMode(FULLSCREEN) |
Varies by OS | ~87 |
| 9 | errgen_open_247ctf_colon |
0x401764 |
CreateFileA("247CTF:", ...) |
ERROR_INVALID_NAME |
123 |
| 10 | errgen_loadlibrary_fake_dll |
0x4017A2 |
LoadLibraryA("C:\247CTF.DLL") |
ERROR_MOD_NOT_FOUND |
126 |
| 11 | errgen_createmutex_twice |
0x4017B0 |
CreateMutexA("247CTF") x2 |
ERROR_ALREADY_EXISTS |
183 |
| 12 | errgen_createprocess_user32 |
0x4017DF |
CreateProcessA("user32.dll" as exe) |
ERROR_BAD_EXE_FORMAT |
193 |
Each character block calls \(N\)
consecutive functions from this cycle, accumulates the error codes, then
checks sum % 126 against the input.
Algebraic Solution
The key insight: since the error codes cycle deterministically, we can bypass the Windows environment entirely and solve this as pure modular arithmetic.
I use Arch BTW.
Let:
- \(S = \sum_{k=0}^{12} e_k\), the sum of one full cycle (all 13 error codes)
- \(P(r) = \sum_{k=0}^{r-1} e_k\), the partial sum of the first \(r\) codes
For character \(i\) with \(n_i\) total API calls:
\[q_i = \left\lfloor \frac{n_i}{13} \right\rfloor, \qquad r_i = n_i \bmod 13\]
\[\text{password}[i] \equiv q_i \cdot S + P(r_i) \pmod{126}\]
The flag format 247CTF{<flag-redacted>} gives us 8
known characters for free. Three of them — blocks 2, 3, 6 — share the
same remainder \(r = 9\): same position
in the error-code cycle, different lap counts. Shared \(P(9)\) cancels on subtraction.
Blocks 2, 3, 6 correspond to 7 (ASCII \(55\), \(q =
16\)), C (\(67\),
\(q = 10\)), { (\(123\), \(q =
24\)):
\[\begin{aligned} 16S + P(9) &\equiv 55 \pmod{126} && \quad (1) \\ 10S + P(9) &\equiv 67 \pmod{126} && \quad (2) \\ 24S + P(9) &\equiv 123 \pmod{126} && \quad (3) \end{aligned}\]
Subtracting \((2)\) from \((1)\) eliminates \(P(9)\):
\[\begin{aligned} (16 - 10)\,S &\equiv 55 - 67 \pmod{126} \\ 6S &\equiv -12 \equiv 114 \pmod{126} \end{aligned}\]
Since \(\gcd(6, 126) = 6\) divides \(114\), we may divide the entire congruence by \(6\) (reducing the modulus to \(126/6 = 21\)):
\[S \equiv 19 \pmod{21} \qquad \Longrightarrow \qquad S \in \{19,\; 40,\; 61,\; 82,\; 103,\; 124\}\]
Similarly, \((3) - (1)\) yields a second constraint:
\[\begin{aligned} 8S &\equiv 68 \pmod{126} \\ 4S &\equiv 34 \pmod{63} && \quad (\text{dividing by } \gcd(2, 126) = 2) \end{aligned}\]
To isolate \(S\), multiply both sides by the modular inverse \(4^{-1} \equiv 16 \pmod{63}\), since \(4 \times 16 = 64 \equiv 1\):
\[S \equiv 16 \times 34 = 544 \equiv 40 \pmod{63}\]
Intersecting via CRT: \(40 \bmod 21 = 19\;\checkmark\), so both congruences agree.
\[\boxed{\,S = 40\,}\]
Back-substituting into \((1)\):
\[\begin{aligned} 16 \times 40 + P(9) &\equiv 55 \pmod{126} \\ 640 + P(9) &\equiv 55 \pmod{126} \\ 10 + P(9) &= 55 && \quad (640 \bmod 126 = 10) \\ P(9) &= 45 \end{aligned}\]
Same process with the remaining known characters fills all partial sums:
| \(P(r)\) | Value | Source |
|---|---|---|
| \(P(3)\) | 10 | '2' (ASCII 50) |
| \(P(4)\) | 16 | \(e_3 = 6\) |
| \(P(5)\) | 34 | \(e_4 = 18\) |
| \(P(6)\) | 58 | '4' (ASCII 52) |
| \(P(7)\) | 90 | 'T' (ASCII 84) |
| \(P(9)\) | 45 | '7' (ASCII 55) |
| \(P(10)\) | 42 | 'F' (ASCII 70) |
| \(P(12)\) | 99 | \(e_{10}, e_{11}\) |
With \(S\) and all \(P(r)\) determined, every character computes directly:
| Block | Calls | \(q\) | \(r\) | Calculation | Char |
|---|---|---|---|---|---|
| 0 | 16 | 1 | 3 | \((40+10) \bmod 126\) | 2 |
| 7 | 191 | 14 | 9 | \((560+45) \bmod 126\) | e |
| 9 | 109 | 8 | 5 | \((320+34) \bmod 126\) | f |
| 15 | 285 | 21 | 12 | \((840+99) \bmod 126\) | 9 |
| 20 | 298 | 22 | 12 | \((880+99) \bmod 126\) | a |
| 39 | 35 | 2 | 9 | \((80+45) \bmod 126\) | } |
All 40 characters land in the valid charset —
247CTF{<flag-redacted>} suffix — confirming the
solution without ever running the binary.
Character-by-Character Breakdown
| Block | Calls | q | r | Formula | Char |
|---|---|---|---|---|---|
| 0 | 16 | 1 | 3 | \((1 \times 40 + 10) \bmod 126 = 50\) | 2 |
| 1 | 45 | 3 | 6 | \((3 \times 40 + 58) \bmod 126 = 52\) | 4 |
| 2 | 217 | 16 | 9 | \((16 \times 40 + 45) \bmod 126 = 55\) | 7 |
| 3 | 139 | 10 | 9 | \((10 \times 40 + 45) \bmod 126 = 67\) | C |
| 4 | 46 | 3 | 7 | \((3 \times 40 + 90) \bmod 126 = 84\) | T |
| 5 | 101 | 7 | 10 | \((7 \times 40 + 42) \bmod 126 = 70\) | F |
| 6 | 321 | 24 | 9 | \((24 \times 40 + 45) \bmod 126 = 123\) | { |
| 7 | 191 | 14 | 9 | \((14 \times 40 + 45) \bmod 126 = 101\) | e |
| 8 | 280 | 21 | 7 | \((21 \times 40 + 90) \bmod 126 = 48\) | 0 |
| 9 | 109 | 8 | 5 | \((8 \times 40 + 34) \bmod 126 = 102\) | f |
| 10 | 19 | 1 | 6 | \((1 \times 40 + 58) \bmod 126 = 98\) | b |
| 11 | 256 | 19 | 9 | \((19 \times 40 + 45) \bmod 126 = 49\) | 1 |
| 12 | 109 | 8 | 5 | \((8 \times 40 + 34) \bmod 126 = 102\) | f |
| 13 | 241 | 18 | 7 | \((18 \times 40 + 90) \bmod 126 = 54\) | 6 |
| 14 | 215 | 16 | 7 | \((16 \times 40 + 90) \bmod 126 = 100\) | d |
| 15 | 285 | 21 | 12 | \((21 \times 40 + 99) \bmod 126 = 57\) | 9 |
| 16 | 17 | 1 | 4 | \((1 \times 40 + 16) \bmod 126 = 56\) | 8 |
| 17 | 215 | 16 | 7 | \((16 \times 40 + 90) \bmod 126 = 100\) | d |
| 18 | 217 | 16 | 9 | \((16 \times 40 + 45) \bmod 126 = 55\) | 7 |
| 19 | 215 | 16 | 7 | \((16 \times 40 + 90) \bmod 126 = 100\) | d |
| 20 | 298 | 22 | 12 | \((22 \times 40 + 99) \bmod 126 = 97\) | a |
| 21 | 285 | 21 | 12 | \((21 \times 40 + 99) \bmod 126 = 57\) | 9 |
| 22 | 280 | 21 | 7 | \((21 \times 40 + 90) \bmod 126 = 48\) | 0 |
| 23 | 215 | 16 | 7 | \((16 \times 40 + 90) \bmod 126 = 100\) | d |
| 24 | 217 | 16 | 9 | \((16 \times 40 + 45) \bmod 126 = 55\) | 7 |
| 25 | 12 | 0 | 12 | \((0 \times 40 + 99) \bmod 126 = 99\) | c |
| 26 | 38 | 2 | 12 | \((2 \times 40 + 99) \bmod 126 = 53\) | 5 |
| 27 | 12 | 0 | 12 | \((0 \times 40 + 99) \bmod 126 = 99\) | c |
| 28 | 191 | 14 | 9 | \((14 \times 40 + 45) \bmod 126 = 101\) | e |
| 29 | 109 | 8 | 5 | \((8 \times 40 + 34) \bmod 126 = 102\) | f |
| 30 | 16 | 1 | 3 | \((1 \times 40 + 10) \bmod 126 = 50\) | 2 |
| 31 | 217 | 16 | 9 | \((16 \times 40 + 45) \bmod 126 = 55\) | 7 |
| 32 | 12 | 0 | 12 | \((0 \times 40 + 99) \bmod 126 = 99\) | c |
| 33 | 16 | 1 | 3 | \((1 \times 40 + 10) \bmod 126 = 50\) | 2 |
| 34 | 109 | 8 | 5 | \((8 \times 40 + 34) \bmod 126 = 102\) | f |
| 35 | 45 | 3 | 6 | \((3 \times 40 + 58) \bmod 126 = 52\) | 4 |
| 36 | 217 | 16 | 9 | \((16 \times 40 + 45) \bmod 126 = 55\) | 7 |
| 37 | 285 | 21 | 12 | \((21 \times 40 + 99) \bmod 126 = 57\) | 9 |
| 38 | 17 | 1 | 4 | \((1 \times 40 + 16) \bmod 126 = 56\) | 8 |
| 39 | 35 | 2 | 9 | \((2 \times 40 + 45) \bmod 126 = 125\) | } |
Anti-Analysis
- TLS Callbacks: Two callbacks
(
0x401C50,0x401C20) execute beforemain— a common anti-debug vector. - 53KB of bloat: Up to 321 API calls per character, making manual static analysis impractical without scripting.
- Environment sensitivity: Functions like
CreateProcessAandSetConsoleDisplayModereturn different error codes under a debugger, making dynamic analysis a trap. The algebraic approach sidesteps this entirely by reasoning at the mathematical level, independent of the Windows runtime.
247CTF - Angr-y Binary
Why waste time creating multiple functions, when you can just use one? Can you find the path to the flag in this angr-y binary?
Challenge Overview
The challenge provides a 32-bit ELF executable. The goal is to find the correct input that leads to the flag.
1 | ❯ file angr-y_binary |
Analysis
Using radare2 to inspect the disassembly, we identify
three key functions: print_flag, no_flag, and
maybe_flag.
Disassembly
1 | ;-- print_flag: |
The logic suggests we need to reach print_flag while
avoiding the paths that lead to no_flag.
Solution
Since the binary's path to the flag is complex, we use
angr to perform symbolic execution and find the correct
input.
Solver Script
1 | import angr |
Execution
Running the script gives us the password:
1 | ❯ python solve.py |
Connecting to the server with the found password:
1 | ❯ nc 0c4c28058a1f7a2f.247ctf.com 50230 |
The returned flag belongs to the launched service instance; no cross-instance invariance is established. Record the generation/retrieval method and use the current instance response instead of a fixed-answer spoiler.