Java下多种执行命令的姿势及问题

警告
本文最后更新于 2020-01-30,文中内容可能已过时。

Java中执行命令有很多姿势,但是有时候带有|,<,>等符号的命令没办法正常执行。为什么呢?

要想了解为什么,我们首先需要知道Java中有哪些方式可以执行命令。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package exec;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;

public class RuntimeExec {

    public static void main(String[] args) throws Exception {
        InputStream in = Runtime.getRuntime().exec("whoami").getInputStream();
        byte[] bcache = new byte[1024];
        int readSize = 0;   //每次读取的字节长度
        ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
        while ((readSize = in.read(bcache)) > 0) {
            infoStream.write(bcache, 0, readSize);
        }
        System.out.println(infoStream.toString());
    }
}

20200130160246

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package exec;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;

public class ProcessExec {
    public static void main(String[] args) {
        try {
            InputStream in = new ProcessBuilder("whoami").start().getInputStream();
            byte[] bs = new byte[2048];
            int readSize = 0;   //每次读取的字节长度
            ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
            while ((readSize = in.read(bs)) > 0) {
                infoStream.write(bs, 0, readSize);
            }
            System.out.println(infoStream.toString());
        } catch (Exception e) {
            System.out.println(e.toString());
        }
    }
}

20200130160321

ProcessImpl是更为底层的实现,Runtime和ProcessBuilder执行命令实际上也是调用了ProcessImpl这个类,对于ProcessImpl类我们不能直接调用,但是可以通过反射来间接调用ProcessImpl来达到执行命令的目的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package exec;

import java.io.ByteArrayOutputStream;
import java.lang.ProcessBuilder.Redirect;
import java.lang.reflect.Method;
import java.util.Map;

public class ProcessImplExec {
    public static void main(String[] args) throws Exception {
        String[] cmds = new String[]{"whoami"};
        Class clazz = Class.forName("java.lang.ProcessImpl");
        Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, Redirect[].class, boolean.class);
        method.setAccessible(true);
        Process e = (Process) method.invoke(null, cmds, null, ".", null, true);
        byte[] bs = new byte[2048];
        int readSize = 0;
        ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
        while ((readSize = e.getInputStream().read(bs)) > 0) {
            infoStream.write(bs, 0, readSize);
        }
        System.out.println(infoStream.toString());
    }
}

20200130160338

了解了Java中的几种执行命令的函数,我们来看下有什么问题。

在windows中,命令前缀要加cmd /c

 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
package exec;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.Timer;

public class RuntimeExec {

    public static void main(String[] args) {
        Process process = null;
        try {
            String cmd ="echo 1 > 1.txt";
            process = Runtime.getRuntime().exec(cmd);
            BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("gbk")));
            String line = null;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

20200130160405

加上cmd /c之后

20200130160425

打断点分析下,跟进exec()函数java.lang.Runtime#exec(java.lang.String)

1
2
3
public Process exec(String command) throws IOException {
    return exec(command, null, null);
}

继续跟进

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public Process exec(String command, String[] envp, File dir)
    throws IOException {
    if (command.length() == 0)
        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);
}

先判断了command传入的命令是否为空,然后经过StringTokenizer类

20200130160447

继续往下看之后发现,经过StringTokenizer类之后返回了一个以空格分隔的数组

20200130160500

接着往下跟发现走到了

1
2
3
4
5
6
7
public Process exec(String[] cmdarray, String[] envp, File dir)
    throws IOException {
    return new ProcessBuilder(cmdarray)
        .environment(envp)
        .directory(dir)
        .start();
}

也就是说Runtime的底层实际上还是ProcessBuilder。我们知道ProcessBuilder.start方法是命令执行,那么跟进这个start()。

20200130160527

发现String prog = cmdarray[0]拿到的就是我们可执行文件,然后判断security是否为null,如果不为null就会校验checkExec。接下来return了一个java.lang.ProcessImpl.start

20200130160548

也就是说Runtime和ProcessBuilder的底层实际上都是ProcessImpl。而不能执行echo命令的原因是因为java找不到这个东西,也就是没有环境变量。所以加上cmd /c就行了。

在谈Linux下的问题时,我们首先要知道一个点

20200130160612

如图所示,/bin/sh -c echo 111 > 3.txt虽然也创建了文件,但是并没有内容,也就是说我们一般通过/bin/sh -c "echo 111 > 3.txt"这种方式来写文件,转化为代码的话就是

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

但是在上文我们知道了一点,StringTokenizer会根据空格将我们的命令划分为数组,那么我们的命令会被划分为{"/bin/sh","-c",""echo","111",">","3.txt""},那么整个命令就变味了,达不到我们想要的效果。

怎么办呢?在ProcessBuilder中有几个构造方法,当传入字符串时会分割为数组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public ProcessBuilder(String... command) {
    this.command = new ArrayList<>(command.length);
    for (String arg : command)
        this.command.add(arg);
}

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

但是传入的是字符串数组时会直接this.command = command,避免了StringTokenizer的空格问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package exec;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;

public class RuntimeExec {

    public static void main(String[] args) {
        Process process = null;
        try {
            String[] cmd = {"/bin/sh", "-c", "echo 111 > 3.txt"};
            process = Runtime.getRuntime().exec(cmd);
            BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("gbk")));
            String line = null;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

有没有更好的办法?有的!Linux下可以用bash的base64编码,Windows下用powershell的base64编码。

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。