0x00. 前言

在一次内部安全测试中,碰到个java 站点,有一处任意代码执行漏洞,还可以回显,心理顿时美滋滋,但是当我执行稍微复杂点shell 命令的时候,发现回显明显不对,执行ls -l /opt 和 ls -l /opt/ |grep tomcat 两个命令输出结果完全一样,grep 完全没有生效,处于好奇,找到了相关开发,看了下漏洞问题出的源码,原来是参数没有过滤,直接丢进Runtime.getRuntime().exec()中执行了。虽然可以通过远程download 脚本直接都给Runtime.exec()执行绕过这个限制,但是出于好奇心,我google不少资料,查阅一番资料之后,终于找到了问题的原因,然后发现了个绕过限制的小技巧并获取完整的shell 执行命令环境,详文如下。

 

0x01. 先介绍线下Runtime类执行外部命令的方法介绍

要运行JVM中外的程序,Runtime类提供了如下方法:

简单解释如下:

`exec(String command)
在单独的进程中执行指定的字符串命令。

exec(String[] cmdarray)
在单独的进程中执行指定命令和变量。

exec(String[] cmdarray, String[] envp)
在指定环境的独立进程中执行指定命令和变量。

exec(String[] cmdarray, String[] envp, File dir)
在指定环境和工作目录的独立进程中执行指定的命令和变量。

exec(String command, String[] envp)
在指定环境的单独进程中执行指定的字符串命令。

exec(String command, String[] envp, File dir)
在有指定环境和工作目录的独立进程中执行指定的字符串命令。`

0x00. 中涉及的案例的环境就是第一个exec(String command) ,直接执行外部传进来的命令字符串,Runtime.getRuntime().exec() 执行外部命令的原理就是fork一个单独的进程,然后直接执行这个命令。exec(String command)这个方法是没法指定shell为命令上下文环境,所以这也就解释了为啥

ls -l /opt 和 ls -l /opt |grep tomtcat 结果一样

因为 | 是shell环境下的管道命令,只有shell的执行上下文环境才识别,直接fork进程执行ls 命令是不识别 |>重定向等shell中的复杂命令

如何突破exec() 无shell上下文执行环境限制获取完整shell执行环境呢,0x02 或有详细演示与分析

 

0x02. 突破限制之旅

我现在没了内部系统的那个测试环境,自己写了个演示代码,如下:

1、演示代码

执行javac Test.java 进行编译就可以了

2、演示分析 ls -l /opt与ls -l /opt |grep anquanke 为啥结果一样

1) 执行 ls -l /opt

2) 执行 ls -l /opt |grep anquanke

提示 当前目录中文件或目录:|grep 不存在
提示 当前目录下文件或目录:anquanke 不存在

为啥会这样呢?

这是因为Runtime.exec对传入的字符串是按照空格进行参数区分的,在这里
|grep 、anquanke 都被认为是文件或者目录,这里没有shell 上下文环境,管道命令 |是无法识别的

注: 这里要补充说明下,为啥ls -l /opt |grep anquanke会打印错误信息,因为代码就也把错误信息打印出来了,如果把错误信息去掉,那么两个命令执行结果就一样了

3、第一次突破尝试

既然Runtime.exec()无shell上下文环境,那么我调用sh -c 用sh直接执行命令,这下总可以了吧

1)执行 java Test 'sh -c /usr/bin/ls -l /opt |grep anquanke'

但是上图的执行结果直接显示当前目录的内容,而不是/opt 目录下的

这是为啥?

sh -c 根据空格进行区分要执行的命令,如上图,执行的命令就是/usr/bin/ls,后面的参数都会被忽略(因为 -l/opt|grep 、anquanke都被认为是sh的参数),要想让后面的 -l 之类参数也被识别,那就用引号括起来,如下

2)执行java Test 'sh -c "/usr/bin/ls -l /opt |grep anquanke"'

我用引号将传递给sh 执行的命令括起来了啊,为啥还是报错呢?

这是因为Rntime.exec() 区分命令的依据是空格, 上面的命令传递给Runtime.exec后相当于相当于:

sh -c '"/usr/bin/ls' '-l' '/opt|grep' 'anquanke'

这就明白为啥会报错了吧

4、第二次尝试突破限制,获取完整shell执行环境

上面的突破之所以失败,是因为受限于Runtime.exec 依据空格划分参数的规则,sh -c 有时候不能直接执行复杂的命令,于是想到可以用管道传递给sh自身然后间接执行复杂命令嘛

于是有了下面的突破命令:

java Test 'sh -c $@|sh 0 echo /usr/bin/ls -l /opt|grep anquanke'

解释下为啥这样就行

sh -c $@|sh 0 echo /usr/bin/ls -l /opt|grep anquanke

这个命令字符串传给Runtime.exec()之后,按照Runtime.exec 依据空格划分参数的规则,命令就变成:

sh -c '$@|sh' '0' 'echo' '/usr/bin/ls' '-l' '/opt|grep' 'anquanke'

就相当于:

这个要补充下知识点了: sh -c 'command' x1 x2 x3

x1 被认为是脚本名称,相当于$0, x2、x3 相当于 $1、$2

命令 sh -c '$@|sh' 0 echo /usr/bin/ls -l /opt|grep anquanke

中的 0 可以换成任意名称 ,比如xx:

sh -c '$@|sh' xx echo /usr/bin/ls -l /opt|grep anquanke

在shell 语法中 $@ 表示所有传递过来的位置参数,不包括$0, $0代表脚本名称

所以sh -c $@|sh xx echo /usr/bin/ls -l /opt|grep anquanke

这里的$@ 就相当于 echo /usr/bin/ls -l /opt|grep anquanke

最终就相当于执行如下命令

sh -c 'echo "/usr/bin/ls -l /opt|grep anquanke"|sh'

5、搞个shell吧,不然这样收尾稍显突兀


 

0x03. 总结

分析完就一个字爽, 学到了不少东东哈哈

 

0x04. 参考资料

http://hg.openjdk.java.net/jdk7/jdk7/jdk/file/tip/src/solaris/classes/java/lang/UNIXProcess.java.linux
https://www.jianshu.com/p/af4b3264bc5d