Java命令执行浅析
一、Java执行命令的几种方式
1、java.lang.Runtime
Runtime.getRuntime().exec("calc");
Runtime.getRuntime().exec(new String[]{"cmd", "/c", "whoami"});
主要分为两大类:
第一类入参为String类型
第二类入参为String[]类型
2、java.lang.ProcessBuilder
ProcessBuilder pb = new ProcessBuilder("whoami");
pb.start();
new ProcessBuilder("cmd", "/c", "net user").start();
new ProcessBuilder(list).start();
构造方法如下两类:
public ProcessBuilder(String... command)
public ProcessBuilder(List<String> command)
3、java.lang.ProcessImpl
Class clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
method.invoke(null, new String[]{"calc"}, null, null, null, false);
该类非Public修饰,所以在不同包下只能通过反射的方式去调用执行。
二、动态调试
跟进java.lang.Runtime类的调用过程:
调用Runtime.getRuntime()方法返回currentRuntime静态成员对象,
继续调用exec方法,这里exec方法重载,有多种同名不同参数类型方法,我们跟进参数为 (String command)的exec方法,继续往下走(exec方法返回类型均为Process抽象类):
进入public Process exec(String command, String[] envp, File dir) 方法:
实例化StringTokenizer类,将命令执行command参数传入,实现构造方法,会将特殊字符打上标识。
经过StringTokenizer类处理后,会得到一个cmdarray数组(根据空格进行切割命令)
进入exec(cmdarray, envp, dir)方法,发现return new ProcessBuilder(cmdarray).start();方法
跟进start()方法
将cmdarray数组第一位赋值给字符串 prog,经过SecurityManager安全管理类校验,默认返回NULL,跳出if判断,判断传入内容是否存在00,存在抛出异常结束运行,继续向下执行
发现ProcessImpl.start方法,这里我们可以得出结论,以上三种Java执行命令的过程,都是通过实现ProcessImpl.start来实现的,继续跟进,进入ProcessImpl构造方法
private ProcessImpl(String cmd[],
final String envblock,
final String path,
final long[] stdHandles,
final boolean redirectErrorStream)
SecurityManager类进行安全校验,通过变量 allowAmbiguousCommands 判断是否允许调用本地进程,赋值allowAmbiguousCommands 变量为 true,进入if条件
当系统允许调用本地进程时,进入Legacy mode(传统模式),会调用needsEscaping(),当prog存在空格且未被双引号包裹时需要使用quoteString()进行处理,接着调用createCommandLine()将命令数组再次拼接为字符串,最后调用native方法(java调用非java代码的接口)create()创建进程,成功执行命令。
三、Java执行命令常见疑惑
通过上述调试,我们可以得出一个结论,常见的命令执行方式均是通过一种手法来实现命令执行,即 通过ProcessImpl类实现。
1、不同方式执行命令传入的command参数,String、 String[]、List?
通过上述调试,我们知道,当传入的参数如果是String字符串,会在处理过程中,被处理为String[]数组,在最底层的调用过程中又会转换为String字符串进行拼接(windows下)。
而我们直接通过ProcessBuilder类执行命令时。可以传入List集合,但List集合已被泛型String约束,所以理论上也是传递的一个数组
所以我们传递的参数都是会被转换为数组类型便于进行校验、取值和判断
如,在ProcessBuilder类中start()方法中数组第一位存储的数值被SecurityManager类校验,随后遍历数组每一位是否存在\x00字符等类似情况
2、传入命令执行失败,字符无法解析?
linux命令行执行:
/bin/sh -c echo 111 > 3.txt
/bin/sh -c "echo 111 > 3.txt"
通过Java命令执行
经过StringTokenizer这个类进行拆分之后变成了**{"/bin/sh","-c","“echo”,“111”、">",“3.txt”"}**
StringTokenizer类对传入的命令进行了破坏,究其原因是因为该类以空格来分割命令字符串
command参数直接为数组时
调试,直接进入构造方法,不再需要经过StringTokenizer类处理,从而获得了正确的数组
Process exec(String[] cmdarray, String[] envp, File dir)
String[] command = { “/bin/sh”, “-c”, “echo 111 > 3.txt” };
同理类似
ls "My Directory"
会被解释为ls '"My' 'Directory"'
``ls > dir_会被解释为获取
>和
dir_listing`目录的列表。
重定向和管道字符的使用方式在正在启动的进程的上下文中没有意义
解决方案:直接传递数组类型,或通过Base64编码,且还确保参数中没有空格影响。
String cmds = "bash -c {echo,base64==}|{base64,-d}|{base64,-i}";
3、Java命令执行什么时候写 cmd /c ?
通过上面的调试,最终通过create native函数调用,
根据JNI命名规则,会调用到ProcessImpl_md.c (jdk8u-jdk)中的Java_Java_lang_ProcessImpl_create()函数,该函数会调用processCreate函数,最终调用Windows系统API函数:CreateProcessW(),用来创建一个新的Windows进程。创建成功后,将新进程的句柄返回给ProcessImpl#create()。
偶尔有时命令执行有效负载Runtime.getRuntime().exec()
会失败。使用Webshell,反序列化漏洞或其他向量时可能会发生这种情况。
通俗的讲就是,
当第一个参数(lpApplicationName)为Null时,第二个参数lpCommandLine需要提供启动程序及所需参数,彼此间以空格隔开。
即默认写法第lpApplicationName参数为cmd.exe lpCommandLine参数为/c 如果没有,就把传递的第一个参数当做环境变量中存在的.exe文件调用。
如图所示,当执行类似ping whoami命令时,可以省略 cmd 写法
/c参数,若不指定/c参数,cmd进程将不会关闭,java程序会一直阻塞,当指定/c时,cmd进程会在命令执行完毕后成功终止。
整体流程图,如下所示,暂未调试linux下命令执行过程,有待完善,文章借鉴了很多blog,如有错误,还望斧正:)
参考链接:
https://mp.weixin.qq.com/s/V16es4xvwfJ7owJqPqvNxQ
https://mp.weixin.qq.com/s?__biz=MzI0NzEwOTM0MA==&mid=2652471913&idx=1&sn=f8c29997df75dbe910be431e5beb2faa&scene=21#wechat_redirect
https://www.freebuf.com/articles/web/236925.html
https://blog.spoock.com/2018/11/07/java-reverse-shell/
https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa
OTM0MA==&mid=2652471913&idx=1&sn=f8c29997df75dbe910be431e5beb2faa&scene=21#wechat_redirect
https://www.freebuf.com/articles/web/236925.html
https://blog.spoock.com/2018/11/07/java-reverse-shell/
https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa