> For the complete documentation index, see [llms.txt](https://xu-an.gitbook.io/sec/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://xu-an.gitbook.io/sec/lan/java/rce.md).

# 浅析Java命令执行

**Java执行命令的3种方法**

**首先了解下在Java中执行命令的方法：**

常用的是  java.lang.Runtime#exec() 和  java.lang.ProcessBuilder#start() ，除此之外，还有更为底层的 java.lang.ProcessImpl#start() ，他们的调用关系如下图所示：

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXm_Tpo7ZmAJx6GXRt%2Fimage.png?alt=media\&token=cf7a0f31-b4d1-47bb-b495-4f5b7b766a82)

其中，ProcessImpl 类是 Process 抽象类的具体实现，且该类的构造函数使用 private 修饰，所以无法在 java.lang 包外直接调用，只能通过反射调用 ProcessImpl#start() 方法执行命令。

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXmnjWVIgJ_dLq8m2M%2Fimage.png?alt=media\&token=a7aecbf9-ce2f-4d15-9482-9fb9f891f422)

**这3种执行方法如下：**

## java.lang.Runtime

```
public static String RuntimeTest() throws Exception {    InputStream ins = Runtime.getRuntime().exec("whoami").getInputStream();    ByteArrayOutputStream bos = new ByteArrayOutputStream();byte[] bytes = new byte[1024];int size;while((size = ins.read(bytes)) > 0)        bos.write(bytes,0,size);return bos.toString();}
```

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXmuD4oR-VmsPJI3wN%2Fimage.png?alt=media\&token=9ae37b90-fbea-4776-8aaa-095072d8494b)

## java.lang.ProcessBuilder

```java
public static String ProcessTest() throws Exception {
  String[] cmds = {"cmd","/c","whoami"};
    InputStream ins = new ProcessBuilder(cmds).start().getInputStream();
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] bytes = new byte[1024];
int size;
while((size = ins.read(bytes)) > 0)
        bos.write(bytes,0,size);
return bos.toString();
}
```

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXn4MgKkkttXjQPjp7%2Fimage.png?alt=media\&token=5091b060-061b-4220-981b-dc07c7f07a49)

## java.lang.ProcessImpl

```java
public static String ProcessImplTest() throws Exception {
    String[] cmds = {"whoami"};
    Class clazz = Class.forName("java.lang.ProcessImpl");
    Method method = clazz.getDeclaredMethod("start", new String[]{}.getClass(),Map.class,String.class,ProcessBuilder.Redirect[].class,boolean.class);
    method.setAccessible(true);
    InputStream ins = ((Process) method.invoke(null,cmds,null,".",null,true)).getInputStream();
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] bytes = new byte[1024];
int size;
while((size = ins.read(bytes)) > 0)
      bos.write(bytes,0,size);
return bos.toString();
}
```

问题：

当直接将命令字符 echo echo\_test > echo.txt 传给 java.lang.Runtime#exec()执行时报错：

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXnCgqvtGI-vaXfwdD%2Fimage.png?alt=media\&token=32533be1-b882-46c7-bd13-52c6f29ed69a)

加上cmd /c 可以成功执行：

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXnFxromMVSWA8nOq8%2Fimage.png?alt=media\&token=a3c57f03-7749-4af7-8a8d-5582b4fb0142)

我们跟进下代码看看是什么原因导致的？

命令执行解析流程：

传入命令字符串echo echo\_test > echo.txt进行调试，跟进java.lang.Runtime#exec(String)，该方法又会调用java.lang.Runtime#exec(String,String\[],File)。

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXnJAhX7Ul1KCIK2mR%2Fimage.png?alt=media\&token=3358acec-c637-4b7d-84a7-0db373f7af0e)

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXnKsr8TbEGhjWo_nU%2Fimage.png?alt=media\&token=2c8c48df-d455-4c97-896c-1e6e03bc5128)

在该方法中调用了StringTokenizer类，通过特定字符对命令字符串进行分割，本地测试如下：

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXnRiPSmAaurHC9wJt%2Fimage.png?alt=media\&token=6deee740-f020-4c67-9571-33d19e3ed5cf)

所以命令字符串echo echo\_test > echo.txt经过StringTokenizer类处理后得到命令数组:{"echo","echo\_test",">","echo.txt"} 。另外java.lang.Runtime#exec()共有6个重载方法，代码如下：

```java
public Process exec(String command) throws IOException {return exec(command, null, null);}public Process exec(String cmdarray[]) throws IOException {return exec(cmdarray, 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.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);}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();}
```

这6个重载函数根据参数不同进行区分，主要是传入字符串跟数组两种形式，但是最终调用的都是最后一个exec(String\[],String\[],File)，在该函数内部首先调用ProcessBuilder类的构造函数创建ProcessBuilder对象，然后调用start()，最终返回一个Process对象。

所以Runtime#exec()底层还是调用的ProcessBuilder#start(),且传入构造函数的参数要求是数组类型(如下图)，所以传给Runtime#exec()的命令字符串需要先使用StringTokenizer类分割为数组再传入ProcessBuilder类。

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXnrxJpK16o8a_2L2N%2Fimage.png?alt=media\&token=76a90545-453a-42b4-b853-496581598d41)

接着跟进java.lang.ProcessBuilder#start()，取出cmdarray\[0]赋值给prog,如果安全管理器SecurityManager开启,会调用SecurityManager#checkExec()对执行程序prog进行检查，之后调用ProcessImpl#start()。

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXnuNEfNSSwy-8Y07t%2Fimage.png?alt=media\&token=194389bc-9022-4aad-8903-c1c405ebb692)

跟进 java.lang.ProcessImpl#start() ，Windows 下会调用 ProcessImpl 类的构造方法，如果是 Linux 环境，则会调用 java.lang.UNIXProcess#init<> 。

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXo7Ume8Ct9S6QU9Cm%2Fimage.png?alt=media\&token=732d326b-5770-4786-b910-8278b693edaa)

### **跟进java.lang.ProcessImpl的构造方法**

该方法内allowAmbiguousCommands变量为 "是否允许调用本地进程" 的开关，在安全管理器未开启且jdk.lang.Process.allowAmbiguousCommands不为false时，allowAmbiguousCommands变量值才为true。当系统允许调用本地进程时，进入Legacy mode(传统模式)，会调用needsEscaping()，当prog存在空格且未被双引号包裹时需要使用quoteString()进行处理，接着调用createCommandLine()将命令数组拼接为命令字符串，最后调用create()创建进程。

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXoAoxwJeax_-T2S2l%2Fimage.png?alt=media\&token=180c8f19-c846-4506-a95f-c092a9311342)

传统模式下，当可执行程序prog存在\t 或空格时，该函数返回true，即需要双引号包裹处理。

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXoEKJN6sNKpo-YEey%2Fimage.png?alt=media\&token=f07b2ed1-f8d7-4130-aaf5-fe3d6113c765)

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXoFz4Htc9BFTTb_ZE%2Fimage.png?alt=media\&token=34580642-0a02-4bcf-b1ab-0b0cc53ef278)

最后调用ProcessImpl#create()，这是一个native方法，根据JNI命名规则，会调用到ProcessImpl\_md.c 中的Java\_Java\_lang\_ProcessImpl\_create()，该函数会调用Windows系统API函数：CreateProcessW()，用来创建一个新的Windows进程。创建成功后，将新进程的句柄返回给ProcessImpl#create()。

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXoJ73PauZL0TQFQ7M%2Fimage.png?alt=media\&token=973575b5-4e85-4d3d-b0aa-7bd91e4b5363)

看下CreateProcessW()怎么处理我们传入的命令的：当第一个参数(lpApplicationName)为0时，第二个参数pcmd(lpCommandLine)需要提供启动程序及所需参数，彼此间以空格隔开。

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXoMLi8JppZhLcIxd5%2Fimage.png?alt=media\&token=6e4d5375-9bb5-4b06-875c-f277b093b59c)

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXoPlf5OemJX7Tv9rH%2Fimage.png?alt=media\&token=81a32429-283e-40f7-b42a-9db58db9d2d6)

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXoRQniGokuUDVMw68%2Fimage.png?alt=media\&token=3175ac68-0486-4d04-8d81-af0b736c31e3)

测试 ProcessImpl#create() 方法：

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXoVr_g-qBM5owcQv8%2Fimage.png?alt=media\&token=253e34e2-9e70-441d-8e1c-886e1d528643)

加上cmd /c之后，成功执行命令：

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXoYhmKdF5-WnXrmdx%2Fimage.png?alt=media\&token=383683e5-bc92-426a-bee2-891ffa5c6e00)

### **需要添加cmd /c的原因:**

在传入 echo echo\_test > echo.txt 命令字符串时，出现错误("java.io.IOException: Cannot run program "echo": CreateProcess error=2, 系统找不到指定的文件。")。原因是echo为命令行解释器cmd.exe的内置命令，并不是一个单独可执行的程序(如下图)，所以如果想执行echo命令写文件需要先启动cmd.exe，然后将echo命令做为cmd.exe的参数进行执行。

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXoaOCO-W-kpa5rD9i%2Fimage.png?alt=media\&token=8de01eb3-2859-4640-92a7-c78780f11cee)

另外关于cmd下的 /c 参数，当未指定时,运行如下示例程序,系统会启动一个pid为8984的cmd后台进程，由于cmd进程未终止导致java程序卡死。当指定/c时，cmd进程会在命令执行完毕后成功终止。

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXod4eNaB7VupK-fF0%2Fimage.png?alt=media\&token=f64ac55e-d44e-41b7-a570-389d9478fa5d)

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXoey1Pp-F5zYHXz4i%2Fimage.png?alt=media\&token=a57572cc-3511-42f4-ba4a-b127e9d8928b)

![](https://172932098-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MC0hWgbNjaxH4i6ny5D%2F-MKXlGzxRis0mugcbtem%2F-MKXogcWVaD11RwGsLPY%2Fimage.png?alt=media\&token=b7a5513f-3d38-4e38-9b8b-e72a9feb8128)

&#x20;所以在Windows环境下，使用Runtime.getRuntime()执行的命令前缀需要加上cmd /c，使得底层Windows的processthreadsapi.h#CreateProcessW()方法在创建新进程时，可以正确识别cmd且成功返回命令执行结果。
