前言

java安全里最关键的可以说就是命令执行了,无论是反序列化还是什么RCE漏洞,要说最终最能体现漏洞价值的话,那就是非命令执行莫属了。这次打算花点时间好好总结整理下java命令执行的几种方式,并做些浅面的分析。最近刚好看到了360BugCloud公众号的一篇java命令执行的调试分析文章,我也跟着调试了一遍,期间学到了不少,链接会在文末贴出来。

首先总的的来说,java命令执行可以分为4种方法,分别是 java.lang.Runtime#exec()、java.lang.ProcessBuilder#start()、java.lang.ProcessImpl#start()以及通过JNI的方式调用动态链接库,最后一种方式这篇文章暂不做分析,先看完前面比较常用的三种方法。

Runtime命令执行

在java反序列化中用到最多的就是Runtime类的exec方法来命令执行了,用法:

Runtime.getRuntime().exec("whoami")

实际上Runtime类的exec的重载方法有6个,如下:

Java通过命令执行cmd java 命令执行_Java通过命令执行cmd

例如本地运行命令ipconfig查看网络配置并返回信息:

InputStream ins = Runtime.getRuntime().exec("ipconfig").getInputStream();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] bytes = new byte[1024];
int size;
while(
(size = ins.read(bytes)) > 0)bos.write(bytes,0,size);
System.out.println(bos.toString()
);

但是这里有个问题,在渗透的过程中如果要遇到要写入文件的话,这里使用"echo xxx>test.txt"等类似的命令就会爆出如下的错误:

Java通过命令执行cmd java 命令执行_Java通过命令执行cmd_02

跟入代码调试查看下具体原因

Java通过命令执行cmd java 命令执行_创建进程_03

首先是跳到另外一个exec的重载方法,envp参数为null,file类型的dir参数也为null

Java通过命令执行cmd java 命令执行_字符串_04

接着将我们传入的command字符串带入到StringTokenizer类进行处理,跟入查看下

Java通过命令执行cmd java 命令执行_java_05

初始化参数后又调用了setMaxDelimCodePoint方法

Java通过命令执行cmd java 命令执行_创建进程_06

跟入到setMaxDelimCodePoint方法后,查看代码

Java通过命令执行cmd java 命令执行_java 安全调用_07

来看最后的处理结果

Java通过命令执行cmd java 命令执行_字符串_08

最后再重新调用了对应的exec重载方法

Java通过命令执行cmd java 命令执行_java 安全调用_09

跟入到java.lang.ProcessBuilder#start,首先会先取出cmdarray[0]赋值给prog,这里值为“echo”

Java通过命令执行cmd java 命令执行_java_10

接着,后面又调用了ProcessImpl.start

Java通过命令执行cmd java 命令执行_java_11

继续跟入查看ProcessImpl#start

Java通过命令执行cmd java 命令执行_字符串_12

接着,后面调用了ProcessImpl的构造方法,再跟入构造方法查看下

Java通过命令执行cmd java 命令执行_字符串_13

跟进ProcessImpl的构建方法后,首先是对系统的配置及环境变量进行检查,比如检测是否允许调用本地进程等配置,接着以cmd[0]为参数创建了一个File对象,然后调用其getPath方法得到路径并赋值给executablePath变量

Java通过命令执行cmd java 命令执行_字符串_14

往下,接着调用needsEscaping()方法对executablePath进行判断,如果其中包含空格,则调用quoteString()方法进行处理;然后调用createCommandLine()把字符串数组拼成字符串,最终的cmdstr为“echo xxx>test.txt”

Java通过命令执行cmd java 命令执行_创建进程_15

最后,再调用了create()方法创建进程,整个过程调试到这里好像也没发现问题所在,原因是最后关键的问题还在create方法创建进程中。

Java通过命令执行cmd java 命令执行_字符串_16

查看该create()方法的代码可看到,这是个native方法,后续是通过调用ProcessImpl_md.c的创建进程的方法来调用调用window系统的API接口,从而完成命令执行等操作。

Java通过命令执行cmd java 命令执行_java 安全调用_17

那么我们的问题该怎么解决呢?还是得回到ProcessImpl_md.c的创建进程的方法中,这方法会对最后的我们传入的cmdstr进行以空格分割,也就是"echo xxx>test.txt",会被分割会"echo"和"xxx>test.txt",然后第一部分的"echo"会被当成启动的执行模块,然而在window的系统环境变量中是找不到这个启动模块的(可以在cmd中输入命令“where echo”进行测试),所以运行后才会抛出文章一开始的“系统找不到指定文件”错误。

知道了问题所在,解决办法的思路就比较清晰了,可以把cmd做为启动的指定模块,然后以运行批处理的方式来达到命令执行,要以这样的方式的话就必须启动命令解释器,就是在批处理的语句前面加上"/c",最终的命令应该为“cmd /c echo xxx>test.txt”。

我们通过IDEA的调试器来测试一遍:

Java通过命令执行cmd java 命令执行_创建进程_18

OK,这回没有抛出其他错误了,在本地的项目位置在找到了新创建的test.txt文件

Java通过命令执行cmd java 命令执行_Java通过命令执行cmd_19

Reference