# 浅析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且成功返回命令执行结果。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://xu-an.gitbook.io/sec/lan/java/rce.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
