近几年里,java安全威胁性较高的就是反序列化漏洞了,原因在于反序列化漏洞通常会利用Runtime类来实现RCE命令执行从而控制目标服务器,但有时候我们会发现在某些情况下,Runtime类并不能执行一些较为复杂的命令,或者说无法获得我们想要的预期结果。

例如我们在linux系统下执行该命令是没问题,可以执行成功

java防止路径遍历漏洞 java命令执行漏洞_命令执行

但是通过java本地命令执行的Runtime类的exec方法来执行该命令无法获得我们预期的结果

java防止路径遍历漏洞 java命令执行漏洞_java_02

我们换一种方式,不执行这么复杂的命令,Runtime类可以成功执行命令并回显

java防止路径遍历漏洞 java命令执行漏洞_java防止路径遍历漏洞_03

 通过这两种情况的对比发现,java本地命令执行不允许命令中出现一些特殊符号,例如上面的示例中出现的“&&”符号,为什么会出现这种情况?实际上无论是windows还是linux下,java本地命令执行是不支持“|”,“<”,“>”,“&”等特殊符号的,接下来,我们来分析Runtime命令执行的调用链是如何执行命令的。

在Runtime类执行命令的调用链中可以看到exec方法并不是命令执行的终点

java防止路径遍历漏洞 java命令执行漏洞_java_04

我们跟进Runtime类的exec方法,发现exec是一个重载的方法

java防止路径遍历漏洞 java命令执行漏洞_安全漏洞_05

根据传入的参数大致可以分为两类,exec方法支持传入的命令可以是一个字符串类型,也可以是一个字符串数组类型

//字符串
    public Process exec(String command);
	public Process exec(String command, String[] envp);
	public Process exec(String command, String[] envp, File dir);
	//数组
	public Process exec(String cmdarray[]); 
	public Process exec(String[] cmdarray, String[] envp); 
	public Process exec(String[] cmdarray, String[] envp, File dir);

根据前面我们传入的参数,exec方法调用了一个重载的exec方法,继续跟进

java防止路径遍历漏洞 java命令执行漏洞_安全开发_06

exec方法首先会校验命令是否为空,然后使用了一个StringTokenizer类处理传入的command命令,StringTokenizer的作用大致就是给command字符串打上标记之类的,调用nextToken方法扫描command字符串并按“  \t \n \r \f  ”中的任意一个进行分割,因此这里会以空格为标记将command字符串分割放到cmdarray数组,然后调用exec方法。

思考一下:为什么要将命令分割成数组? 

可以看到Runtime类的exec方法底层是把cmdarray数组封装到了一个ProcessBuilder对象中并调用了start方法,也就是说Runtime类的exec方法底层实际上是调用了ProcessBuilder的start方法执行命令

java防止路径遍历漏洞 java命令执行漏洞_安全漏洞_07

在start方法中,prog会获取cmdarray数组中第一个元素,也就是“echo”,然后调用了checkExec方法进行安全校验

java防止路径遍历漏洞 java命令执行漏洞_安全漏洞_08

继续跟踪,接着会调用ProcessImpl.start方法,在start方法中有一个new UNIXProcess操作,也就是调用UNIXProcess类来执行系统命令

java防止路径遍历漏洞 java命令执行漏洞_命令执行_09

UNIXProcess的构造方法会调用forkAndExec方法创建命令执行环境(forkAndExec是一个native方法),native方法会去调用底层封装的系统调用去执行命令

java防止路径遍历漏洞 java命令执行漏洞_命令执行_10

forkAndExec方法会返回命令执行进程的pid,我们在命令行中确实会看到一个echo命令的进程,说明forkAndExec方法确实会根据prog的内容来创建命令执行进程

java防止路径遍历漏洞 java命令执行漏洞_java防止路径遍历漏洞_11

 再回到前面提出的一个思考问题,Runtime类的exec方法之所以把command中的命令通过StringTokenizer类分割成数组的目的就是为了让UNIXProcess类的forkAndExec方法根据prog来构建命令执行进程环境。

分析完Runtime类的命令执行流程,我们来看一个linux系统下奇怪的命令执行,以下命令在命令行窗口可以执行成功,但是在使用java本地命令环境下Runtime类的exec方法并没有执行成功。

/bin/sh -c "echo 111 > 3.txt"

根据前面的分析我们知道,Runtime类的exec方法中command会经过一个StringTokenizer类的分割和处理,从cmdarray数组的结果来看,在command原来的语义中"echo 111 > 3.txt"是一个整体,但是StringTokenizer类把"echo 111 > 3.txt"进行了拆分,破坏了command中命令原来的语义,导致在执行命令时返回的并不是我们想要的预期结果。

java防止路径遍历漏洞 java命令执行漏洞_java_12

 有同学可能会说,直接将command中的命令传给ProcessBuilder类的构造方法,然后调用start方法,绕过StringTokenizer类的处理,不就可以实现命令执行了。

我们来试一下,发现程序抛异常了,思考一下为什么会抛异常?

java防止路径遍历漏洞 java命令执行漏洞_命令执行_13

 理论上思路是可行的,实际上ProcessBuilder类的start方法中prog会获取cmdarray[0]中的内容,然后根据cmdarray[0]来构建命令执行的进程环境,如果直接在ProcessBuilder类的构造中传入command的话,那么forkAndExec方法就无法根据prog的内容来构建命令执行环境,至于具体的原因,我们接下来分析一下ProcessBuilder类的构造犯法。

ProcessBuilder类有两个构造方法,我们先来看String类型参数的构造,可以看到ProcessBuilder的构造中期望传入一个String[]数组

public ProcessBuilder(String... command) {
        this.command = new ArrayList<>(command.length);
        for (String arg : command)
            this.command.add(arg);
    }

如果直接传入一个字符串command的话,forkAndExec方法无法根据/bin/sh -c \"echo 111 > 3.txt\"来判断应该创建什么类型的命令执行进程环境,然后就会抛出异常(IOException)

String command = "/bin/sh -c \"echo 111 > 3.txt\"";

因此解决方案就是传入一个数组,再执行程序发现Project的目录下生成了一个3.txt文件。

java防止路径遍历漏洞 java命令执行漏洞_java防止路径遍历漏洞_14

对于ProcessBuilder类的另一个构造方法也是如此,按照要求传入一个ArrayList就可以实现了

public ProcessBuilder(List<String> command) {
        if (command == null)
            throw new NullPointerException();
        this.command = command;
    }

命令同样也是可以执行成功

java防止路径遍历漏洞 java命令执行漏洞_安全开发_15

另外,通过Runtime类的exec方法来执行命令,有时候传入的命令经过StringTokenizer类的分割后,破坏了原有的语义,那么也可以传入一个数组的方式

java防止路径遍历漏洞 java命令执行漏洞_java防止路径遍历漏洞_16

 当传入一个数组后会调用重载的exec方法,该方法没有调用StringTokenizer类,而是直接调用了ProcessBuilder类的start方法来执行命令,保留了命令语义完整性,例如ysoserial利用工具中的所有命令都是通过数组传参的。

关于如何通过反射UNIXProcess/ProcessImpl执行命令来绕过RASP机制,还有如何通过JNI编程实现命令执行,放在下一篇来讲吧。

参考资料:

http://www.lmxspace.com/2019/10/08/Java%E4%B8%8B%E5%A5%87%E6%80%AA%E7%9A%84%E5%91%BD%E4%BB%A4%E6%89%A7%E8%A1%8C/