安全矩阵

 找回密码
 立即注册
搜索
查看: 4827|回复: 0

浅析Java命令执行

[复制链接]

991

主题

1063

帖子

4315

积分

论坛元老

Rank: 8Rank: 8

积分
4315
发表于 2020-10-26 08:59:38 | 显示全部楼层 |阅读模式
原文链接:浅析Java命令执行

在使用java.lang.Runtime#exec()执行命令时,为何有时候命令前缀需要加cmd /c或者bash -c?今天就来一探究竟!


Java执行命令的3种方法首先了解下在Java中执行命令的方法:
常用的是 java.lang.Runtime#exec()和 java.lang.ProcessBuilder#start(),除此之外,还有更为底层的java.lang.ProcessImpl#start(),他们的调用关系如下图所示:



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



这3种执行方法如下:
java.lang.Runtime
  1. 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();}
复制代码



java.lang.ProcessBuilder
  1. public static String ProcessTest() throws Exception {
  2.   String[] cmds = {"cmd","/c","whoami"};
  3.     InputStream ins = new ProcessBuilder(cmds).start().getInputStream();
  4.     ByteArrayOutputStream bos = new ByteArrayOutputStream();
  5. byte[] bytes = new byte[1024];
  6. int size;
  7. while((size = ins.read(bytes)) > 0)
  8.         bos.write(bytes,0,size);
  9. return bos.toString();
  10. }
复制代码



java.lang.ProcessImpl
  1. public static String ProcessImplTest() throws Exception {
  2.     String[] cmds = {"whoami"};
  3.     Class clazz = Class.forName("java.lang.ProcessImpl");
  4.     Method method = clazz.getDeclaredMethod("start", new String[]{}.getClass(),Map.class,String.class,ProcessBuilder.Redirect[].class,boolean.class);
  5.     method.setAccessible(true);
  6.     InputStream ins = ((Process) method.invoke(null,cmds,null,".",null,true)).getInputStream();
  7.     ByteArrayOutputStream bos = new ByteArrayOutputStream();
  8. byte[] bytes = new byte[1024];
  9. int size;
  10. while((size = ins.read(bytes)) > 0)
  11.       bos.write(bytes,0,size);
  12. return bos.toString();
  13. }
复制代码




问题:
当直接将命令字符 echo echo_test > echo.txt 传给 java.lang.Runtime#exec()执行时报错:



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



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

命令执行解析流程:
传入命令字符串echo echo_test > echo.txt进行调试,跟进java.lang.Runtime#exec(String),该方法又会调用java.lang.Runtime#exec(String,String[],File)。





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



所以命令字符串echo echo_test > echo.txt经过StringTokenizer类处理后得到命令数组:{"echo","echo_test",">","echo.txt"} 。另外java.lang.Runtime#exec()共有6个重载方法,代码如下:
  1. 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类。



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



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



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



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





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



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







测试 ProcessImpl#create() 方法:



加上cmd /c之后,成功执行命令:


需要添加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的参数进行执行。



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







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

未完待续...










回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-9-20 15:32 , Processed in 0.013150 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表