Java中的命令执行的几个类

Java中可以进行命令执行的类有RuntimeProcessBuilderProcessImpl等。

ProcessBuilder类

ProcessBuilder类用于创建操作系统进程。每个ProcessBuilder实例管理一个进程属性集,其start方法利用这些属性来创建进程。

ProcessBuilder的构造方法为ProcessBuilder(String... command),第一个参数为要执行的程序,后面的参数会被作为执行程序的参数。

如下是其基本用法:

1
2
3
4
5
6
7
8
InputStream in = new ProcessBuilder("ipconfig", "/all").start().getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] tmp = new byte[1024];
int len = 0;
while ((len=in.read(tmp)) > 0){
out.write(tmp, 0, len);
}
System.out.println(out.toString());

Runtime类

每个Java程序都一个单例的Runtime实例,其允许程序能够访问程序运行时的环境。Runtime实例可以通过Runtime.getRuntime()来获取。

Runtime有exec方法,可以执行系统命令,其有多个重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public Process exec(String command) throws IOException {
return exec(command, null, null);
}

public Process exec(String command, String[] envp) throws IOException {
return exec(command, envp, null);
}

public Process exec(String command, String[] envp, File dir) throws IOException {
if (command.isEmpty())
throw new IllegalArgumentException("Empty command");

StringTokenizer st = new StringTokenizer(command);
String[] cmdarray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++)
cmdarray[i] = st.nextToken();
return exec(cmdarray, envp, dir);
}

public Process exec(String cmdarray[]) throws IOException {
return exec(cmdarray, null, null);
}

public Process exec(String[] cmdarray, String[] envp) throws IOException {
return exec(cmdarray, envp, null);
}

public Process exec(String[] cmdarray, String[] envp, File dir) throws IOException {
return new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
}

可见,所有的exec方法最终都会调用到public Process exec(String[] cmdarray, String[] envp, File dir)这个方法,并且底层是调用的ProcessBuilderstart方法。

cmdarray: 要执行的命令数组,第一个是要执行的程序,之后的被作为参数传入
envp: 指明环境变量
dir: 指明工作路径

对于exec(String command),其参数就是要执行的命令,例如:

1
2
3
4
5
6
7
8
InputStream in = Runtime.getRuntime().exec("whoami").getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] tmp = new byte[1024];
int len = 0;
while ((len=in.read(tmp)) > 0){
out.write(tmp, 0, len);
}
System.out.println(bao.toString());

ProcessImpl类

ProcessBuilder.start()正是使用ProcessImpl类来创建新的进程的。

ProcessImpl类有start的静态方法,用于创建新的进程。由于ProcessImpl类不是public的,因此无法直接在java.lang包外调用,只能通过反射调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String[] cmds = new String[]{"whoami"};
byte[] bs = new byte[2048];
int readSize = 0;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

Class clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
Process p = (Process) method.invoke(null, cmds, null, ".", null, true);
InputStream inputStream = p.getInputStream();
while ((readSize = inputStream.read(bs)) > 0) {
outputStream.write(bs, 0, readSize);
}

System.out.println(outputStream.toString());

ScriptEngineManager类代码执行

ScriptEngineManager类用来实现Java和Js之间的调用,其全类名为javax.script.ScriptEngineManager

ScriptEngineManagergetEngineByExtension方法返回一个ScriptEngine接口的实现类,ScriptEngine接口中有一个eval方法,可以执行java代码

1
2
3
new ScriptEngineManager().getEngineByExtension("js").eval("java.lang.Runtime.getRuntime().exec(\"calc\")");

new javax.script.ScriptEngineManager().getEngineByName("nashorn").eval("java.lang.Runtime.getRuntime().exec(\"calc\")")

需要注意的是,这需要在有相应engine的环境中才能有效。

命令执行的一个问题

当执行例如whoami > a.txt命令的时候,会发现执行不会成功。

对于Runtime,原因是其参数会传到如下的方法中

1
2
3
4
5
6
7
8
9
10
public Process exec(String command, String[] envp, File dir) throws IOException {
if (command.isEmpty())
throw new IllegalArgumentException("Empty command");

StringTokenizer st = new StringTokenizer(command);
String[] cmdarray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++)
cmdarray[i] = st.nextToken();
return exec(cmdarray, envp, dir);
}

会将传入的字符串命令按照\t\n\r\f和空格进行分割,分割为字符串数组后再调用最终的exec方法。

经过调试即可看到whoami > a.txt变成了["whoami", ">", "a.txt"],再传入ProcessBuilder中,所以会出错。

解决方法

在window中,可以使用cmd /c添加在命令之前,将后面的所有内容作为参数

1
2
3
4
5
Runtime.getRuntime().exec("cmd /c echo 111 > a.txt");
// 底层调用ProcessBuilder,传入的参数为["cmd", "/c", "echo", "1223", ">", "a.txt"]

// 使用ProcessBuilder还可以进行如下调用
new ProcessBuilder("cmd", "/c", "echo 1223 > a.txt").start();

在linux中,则不能Runtime.getRuntime().exec("/bin/sh -c echo 111 > a.txt");这样调用,原因是sh -c需要一个字符串作为参数来执行


在linux中,只能进行如下调用

1
2
3
4
Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "echo 1 > a.txt"})

// 等价于
new ProcessBuilder("/bin/sh", "-c", "echo 1 > a.txt").start();

除此之外,linux还可以使用base64编码
/bin/bash -c {echo,d2hvYW1p}|{base64,-d}|{bash,-i}

文章作者: Dar1in9
文章链接: http://dar1in9s.github.io/2022/09/16/Java/Java中的命令执行/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Dar1in9's Blog