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}
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}
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}
问题
了解了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}
加上cmd /c
之后
打断点分析下,跟进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类
继续往下看之后发现,经过StringTokenizer类之后返回了一个以空格分隔的数组
接着往下跟发现走到了
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()。
发现String prog = cmdarray[0]
拿到的就是我们可执行文件,然后判断security是否为null,如果不为null就会校验checkExec。接下来return了一个java.lang.ProcessImpl.start
也就是说Runtime和ProcessBuilder的底层实际上都是ProcessImpl。而不能执行echo命令的原因是因为java找不到这个东西,也就是没有环境变量。所以加上cmd /c
就行了。
Linux
在谈Linux下的问题时,我们首先要知道一个点
如图所示,/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编码。
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。
评论