前言
关于Java的命令执行其实一直都没有单独学习过,正好昨天师傅问了一个问题:命令执行时字符串和字符串数组用哪个更好一些。当时被问得有点懵难道不都一样么?其实不然,借此重新了解下RCE以及失效问题。
单例模式
常规命令执行代码:
Runtime.getRuntime().exec("calc");
在看过Runtime类后,一直有个问题:exec()就在Runtime类中,为什么不直接Runtime.exec()
进行调用,而是中间加上getRuntime()?
这个就是由单例模式决定的:
单例模式(Singleton)的目的是为了保证在一个进程中,某个类有且仅有一个实例。
因为这个类只有一个实例,因此,自然不能让调用方使用new Runtime()来创建实例了。所以,单例的构造方法必须是private,这样就防止了调用方自己创建实例,但是在类的内部,是可以用一个静态字段来引用唯一创建的实例的,这个实例也就是getRuntime()
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
所以我们在获取Runtime实例时,就要通过getRuntime()方法获取。
流程分析
exec()有以下几个重载方法,根据不同形参进行调用
public Process exec(String command)-----在单独的进程中执行指定的字符串命令。
public Process exec(String [] cmdArray)---在单独的进程中执行指定命令和变量
public Process exec(String command, String [] envp)----在指定环境的独立进程中执行指定命令和变量
public Process exec(String [] cmdArray, String [] envp)----在指定环境的独立进程中执行指定的命令和变量
public Process exec(String command,String[] envp,File dir)----在有指定环境和工作目录的独立进程中执行指定的字符串命令
public Process exec(String[] cmdarray,String[] envp,File dir)----在指定环境和工作目录的独立进程中执行指定的命令和变量
先看下大致流程,跟进exec(),会调用StringTokenizer()
把传入的conmmand字符串按中的 \t \n \r \f
分割成数组cmdarray。(为什么数组更好原因就在这里,等会在看)
public StringTokenizer(String str) {
this(str, " \t\n\r\f", false);
}
分割后,由于cmdarray变成了数组,便会进入其他有参exec()方法,其中实例化了ProcessBuilder,并调用了start()方法,这里的cmdarray的值就是我们传入的calc,其中environment、directory都是对值进行初始化,主要还是start()这里
跟进后又调用了,process实现类的start(),执行了calc(其中调用过程牵扯到linux的UNIXProcess,而windows中没有所以就不跟进了)
ProcessBuilder
根据上边的流程可以发现:真正执行代码的地方其实是:
new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
所以除了常规的Runtime.getRuntime().exec()
可以换成
new ProcessBuilder("calc").start();
反射调用:
Class<ProcessBuilder> pbc = ProcessBuilder.class;
Constructor<ProcessBuilder> cons = pbc.getDeclaredConstructor(String[].class);
Method method = pbc.getDeclaredMethod("start");
Object o = cons.newInstance(new String[][]{{"calc"}});
method.invoke(o);
失效问题
最后看下失效问题,这里失效主要是使用String类型导致,这里把calc命令换成echo Sentiment && echo 6666
正常情况下&&会当做连接符,执行下一个命令
但是在java如果用String类型命令,执行后的结果变成了Sentiment!&&echo 123
,也就是&&也当作字符串进行输出了
原因也就在于StringTokenizer方法的处理,将后边的&echo其解析成了一段字符
所以这里就需要用字符串数组类型,来解决这个问题,此时将命令换为:
String[] cmd = {"cmd","/c","echo Sentiment!&&echo 123"};
#liinux
String[] cmd = {"/bin/bash","-c","echo Sentiment!&&echo 123"};
执行后得到了我们想要的结果:
再看下代码,由于直接用了数组,因此在执行了exec()时,直接执行到了形参为数组的exec()方法中,直接执行ProcessBuilder.start()方法中,成功执行命令
因此在遇到命令中有\t\n\r\f
这类转义字符时,需要用字符串数组类型。其实除了这种方式外还可以通过对命令进行编码的形式解决,不过只针对于linux
参考
Java Runtime.getRuntime().exec由表及里 - 先知社区 (aliyun.com)