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

命令执行

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

Runtime

 1package exec;
 2
 3import java.io.ByteArrayOutputStream;
 4import java.io.InputStream;
 5
 6public class RuntimeExec {
 7
 8    public static void main(String[] args) throws Exception {
 9        InputStream in = Runtime.getRuntime().exec("whoami").getInputStream();
10        byte[] bcache = new byte[1024];
11        int readSize = 0;   //每次读取的字节长度
12        ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
13        while ((readSize = in.read(bcache)) > 0) {
14            infoStream.write(bcache, 0, readSize);
15        }
16        System.out.println(infoStream.toString());
17    }
18}

20200130160246

ProcessBuilder

 1package exec;
 2
 3import java.io.ByteArrayOutputStream;
 4import java.io.InputStream;
 5
 6public class ProcessExec {
 7    public static void main(String[] args) {
 8        try {
 9            InputStream in = new ProcessBuilder("whoami").start().getInputStream();
10            byte[] bs = new byte[2048];
11            int readSize = 0;   //每次读取的字节长度
12            ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
13            while ((readSize = in.read(bs)) > 0) {
14                infoStream.write(bs, 0, readSize);
15            }
16            System.out.println(infoStream.toString());
17        } catch (Exception e) {
18            System.out.println(e.toString());
19        }
20    }
21}

20200130160321

ProcessImpl

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

 1package exec;
 2
 3import java.io.ByteArrayOutputStream;
 4import java.lang.ProcessBuilder.Redirect;
 5import java.lang.reflect.Method;
 6import java.util.Map;
 7
 8public class ProcessImplExec {
 9    public static void main(String[] args) throws Exception {
10        String[] cmds = new String[]{"whoami"};
11        Class clazz = Class.forName("java.lang.ProcessImpl");
12        Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, Redirect[].class, boolean.class);
13        method.setAccessible(true);
14        Process e = (Process) method.invoke(null, cmds, null, ".", null, true);
15        byte[] bs = new byte[2048];
16        int readSize = 0;
17        ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
18        while ((readSize = e.getInputStream().read(bs)) > 0) {
19            infoStream.write(bs, 0, readSize);
20        }
21        System.out.println(infoStream.toString());
22    }
23}

20200130160338

问题

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

Windows

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

 1package exec;
 2
 3import java.io.BufferedReader;
 4import java.io.IOException;
 5import java.io.InputStreamReader;
 6import java.nio.charset.Charset;
 7import java.util.Timer;
 8
 9public class RuntimeExec {
10
11    public static void main(String[] args) {
12        Process process = null;
13        try {
14            String cmd ="echo 1 > 1.txt";
15            process = Runtime.getRuntime().exec(cmd);
16            BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("gbk")));
17            String line = null;
18            while ((line = br.readLine()) != null) {
19                System.out.println(line);
20            }
21        } catch (IOException e) {
22            e.printStackTrace();
23        }
24    }
25}

20200130160405

加上cmd /c之后

20200130160425

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

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

继续跟进

 1public Process exec(String command, String[] envp, File dir)
 2    throws IOException {
 3    if (command.length() == 0)
 4        throw new IllegalArgumentException("Empty command");
 5
 6    StringTokenizer st = new StringTokenizer(command);
 7    String[] cmdarray = new String[st.countTokens()];
 8    for (int i = 0; st.hasMoreTokens(); i++)
 9        cmdarray[i] = st.nextToken();
10    return exec(cmdarray, envp, dir);
11}

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

20200130160447

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

20200130160500

接着往下跟发现走到了

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

也就是说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

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

20200130160612

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

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

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

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

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

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

 1package exec;
 2
 3import java.io.BufferedReader;
 4import java.io.IOException;
 5import java.io.InputStreamReader;
 6import java.nio.charset.Charset;
 7
 8public class RuntimeExec {
 9
10    public static void main(String[] args) {
11        Process process = null;
12        try {
13            String[] cmd = {"/bin/sh", "-c", "echo 111 > 3.txt"};
14            process = Runtime.getRuntime().exec(cmd);
15            BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.forName("gbk")));
16            String line = null;
17            while ((line = br.readLine()) != null) {
18                System.out.println(line);
19            }
20        } catch (IOException e) {
21            e.printStackTrace();
22        }
23    }
24}

better?

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

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