从算数扩展到 RCE
算数扩展导致的 RCE,有趣,可惜利用场景比较少见。
起源
大家在初三的时候就知道了 shell 可以通过 $变量名
来实现引用变量
。举例 I=$(whoami); echo "I am $I"
,结果是 I am Macr0phag3
。
与之对应的还有一个特性,官方名字应该是 算术扩展
(Arithmetic Expansion),参考链接
但有一个限制,即表达式中的元素必须是数字、算数函数或者变量,如果不符合会报错。可以用的算术符可以参考这个
最后得到的值也是数字。若传入的是字符串,则会得到为 0;如果字符串含有非字母,则会报错:
1 |
|
所以这到底有什么用呢?别着急,来看些例子。
信息泄露
1 |
|
运行:
1 |
|
报错会带出 PATH
里的内容。可见 (( var == 0 ))
会展开里面的内容,并且实际上还是递归的。
变量覆盖
递归展开,例如这个变量覆盖的例子:
1 |
|
运行
1 |
|
递归展开了 var='username=200'
,从而改变了看似无法改变的 username
的值。当然,递归展开的时候,得到的值也只会是数字,所以 username 最后会是数字。
既然能够覆盖变量,那么覆盖一个 PATH 也是手到擒来,这会导致命令替换,从而执行任意命令。假设有以下脚本:
1 |
|
这个脚本执行了 id
。那么我们可以通过覆盖 PATH,使得这个脚本在执行的时候变成执行我们秘制的 id
:
1 |
|
(0
可以换为任意数字)
可惜的是,这个利用场景有点苛刻,要满足:
- 源脚本在算术扩展后执行了一个命令
- 能在源脚本运行的目录下创建目录
0
- 能够在
0
下面创建同名的恶意脚本
命令执行
覆盖 PATH 实现的命令执行,可以,但是不够舒服。如果在算式中使用数组,并且索引为命令,那么在算术扩展的时候会将该命令替换为命令执行的结果,从而实现命令执行:
1 |
|
运行:
1 |
|
由于算术扩展的输入是字母类型,所以只有在命令的结果含有特殊字符的时候才有回显,所以可以拼一个特殊字符是它报错:
1 |
|
还可以这样,让命令的输出走 stderr:
1 |
|
这样 0$(whoami>&2)
实际上就是 0
,可以避免报错。不懂的话,可以看这个,更加直观一些:
1 |
|
虽然可以避免报错,但是由于命令输出走的是 stderr,不一定会回显到应用上。
不过说实话,都能执行任意命令了,其实无所谓输出不输出,反弹 shell 完事:
1 |
|
利用场景
可被用于算术扩展的语句不止 if (( var == 0 ))
,还可以是 if [[ $var -lt 0 ]]
、if [ -v "$var" ]
、echo "$((var))"
。有这个缺陷的也不止 bash,例如 zsh 也有类似的问题,比如 echo "$((var))"
。不过那三个 if
我试了一下不太行:
1 |
|
最后,总的来说,这个利用方式其实比较少见。可能的利用场景,例如:
- 执行用户可控环境变量的固定脚本。例如 url 中某个参数给用户提供了环境变量修改的权限,例如执行脚本时的语言(
LC_CTYPE
之类的)。这种情况下有机会造成命令执行。 - 受限的 shell 环境,或许能够通过这个方法进行逃逸。
- 结合 suid 进行提权。可以通过这个来留提权的后门,不过现在大部分的 shell 都不会理会 shell 脚本的 suid。
解决方案
目前我还没找到通用的解决方案,恐怕只能通过避免使用这些会进行算术扩展的语句,如果非得使用,可以对输入进行消毒处理。
- By:tr0y.wang
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论