前言
Runtime命令执行
Runtime.getRuntime().exec("whoami")
实际上Runtime类的exec的重载方法有6个,如下: 例如本地运行命令ipconfig查看网络配置并返回信息: InputStream ins = Runtime.getRuntime().exec("ipconfig").getInputStream();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] bytes = new byte[1024];
int size;
while(
(size = ins.read(bytes)) > 0)bos.write(bytes,0,size);
System.out.println(bos.toString()
);
但是这里有个问题,在渗透的过程中如果要遇到要写入文件的话,这里使用”echo xxx>test.txt”等类似的命令就会爆出如下的错误: 下面就跟入代码调试查看下具体原因,直接在exec方法处打断点 首先是跳到另外一个exec的重载方法,envp参数为null,file类型的dir参数也为null 接着将我们传入的command字符串带入到StringTokenizer类进行处理,跟入查看下 初始化参数后又调用了setMaxDelimCodePoint方法 跟入到setMaxDelimCodePoint方法后,查看代码 来看最后的处理结果 最后再重新调用了对应的exec重载方法,这里补充一点,最后exec方法最终都会到下面这个类进行处理 跟入到ProcessBuilder#start()方法,首先会先取出cmdarray[0]赋值给prog,这里值为“echo”,到这里我们也知道了其实Runtime.getRuntime().exec()的底层实际上也是ProcessBuilder 接着,后面又调用了ProcessImpl.start() 继续跟入查看ProcessImpl#start方法 接着,后面调用了ProcessImpl的构造方法,再跟入构造方法查看下 跟进ProcessImpl的构建方法后,首先是对系统的配置及环境变量进行检查,比如检测是否允许调用本地进程等配置,接着以cmd[0]为参数创建了一个File对象,然后调用其getPath方法得到路径并赋值给executablePath变量 往下,接着调用needsEscaping()方法对executablePath进行判断,如果其中包含空格,则调用quoteString()方法进行处理;然后调用createCommandLine()把字符串数组拼成字符串,最终的cmdstr为“echo xxx>test.txt” 最后,再调用了create()方法创建进程,整个过程调试到这里好像也没发现问题所在,原因是最后关键的位置还在create方法创建进程中。 查看该create()方法的代码可看到,这是个native方法,后续是通过调用ProcessImpl_md.c的创建进程的方法来调用调用window系统的API接口,从而完成命令执行等操作。 那么我们的问题该怎么解决呢?还是得回到ProcessImpl_md.c的创建进程的方法中,这方法会对最后的我们传入的cmdstr进行以空格分割,也就是”echo xxx>test.txt”,会被分割会”echo”和”xxx>test.txt”,然后第一部分的”echo”会被当成启动的执行模块,然而在window的系统环境变量中是找不到这个启动模块的(可以在cmd中输入命令“where echo”进行测试),所以运行后才会抛出文章一开始的“系统找不到指定文件”错误。 知道了问题所在,解决办法的思路就比较清晰了,可以把cmd做为启动的指定模块,然后以运行批处理的方式来达到命令执行,要以这样的方式的话就必须启动命令解释器,就是在批处理的语句前面加上”/c”,最终的命令应该为“cmd /c echo xxx>test.txt”。 我们通过IDEA的调试器来测试一遍: OK,这回没有抛出其他错误了,在本地的项目位置在找到了新创建的test.txt文件
ProcessBuilder
先简单了解下ProcessBuilder这个类,ProcessBuilder类是J2SE 1.5在java.lang中新添加的一个新类,此类用于创建操作系统进程,它提供一种启动和管理进程(也就是应用程序)的方法。在J2SE 1.5之前,都是由Process类处来实现进程的控制管理。 来看看是该类怎么完成命令执行的,比如执行ipconfig命令: Process p = null;
// 执行ipconfig /all命令
p = new ProcessBuilder("cmd", "/c ipconfig /all").start();
byte[] b = new byte[1024];
int readbytes = -1;
StringBuffer sb = new StringBuffer();
// 读取进程输出值
InputStream in = p.getInputStream();
try {
while ((readbytes = in.read(b)) != -1) {
sb.append(new String(b, 0, readbytes));
}
} catch (IOException e1) {
} finally {
try {
in.close();
} catch (IOException e2) {
}
}
System.out.println( sb.toString());
}
至于 p = new ProcessBuilder("cmd", "/c ipconfig /all").start();这里为什么要在ipconfig前面加/c,在前面的Runtime类的调试中已经有解释了,这里就不再重复,这里主要是记录学习下ProcessBuilder这个类。 前面已经说过,在J2SE 1.5之前,都是由Process类处来实现进程的控制管理,那为什么还要ProcessBuilder这个类呢?区别又在哪? 其实区别在于两个类的性质就不一样了,process类是一个抽象类,要创建一个进程的话,一般是通过Runtime.exec()和ProcessBuilder.start()来间接创建其实例,而ProcessBuilder是一个final类,有两个带参数的构造方法,可以通过构造方法来直接创建ProcessBuilder的对象,并且ProcessBuilder为进程提供了更多的控制,例如,可以设置当前工作目录,还可以改变环境参数,可以说ProcessBuilder类是process类的一个扩展和包装。 还有个问题,就是Runtime.exec()和ProcessBuilder.start()有什么区别呢? 从前面我们知道,Runtime.exec()有六种重载方法,在传入命令参数时,其中java.lang.Runtime.exec(String)可以接受单一字符串参数,也可以接受其他字符串数组等参数,而ProcessBuilder.start()是通过构建方法传入后才调用start()方法,但ProcessBuilder类构造函数只接受字符串的varargs数组或List的字符串,其中数组或列表中的每个字符串都被假定为单个参数。 所以最关键的区别是,同个字符串参数对于ProcessBuilder类和Runtime类的命令执行结果来说是不同的,举个例子,比如同样是执行命令“ipconfig /all” 传入Runtime类的exec方法后,之后会先把“ipconfig /all”以空格隔开分为两部分,第一部分的“ipconfig”会作为启动模块,第二部分的”/all“作为第一部分的命令行参数,而ipconfig模块在我的系统环境是存在的,如下: 同样可以正常命令执行,没什么问题
到了ProcessBuilder类这里,同样的“ipconfig /all”传进来的话就抛出错误
这是因为ProcessBuilder类把整个“ipconfig /all”都标记化了,在后面的调用中会把这整一串都当做启动模块的名字寻找,而我本地系统中肯定是不存在,所以就会抛出“系统找不到指定文件”的错误。
看到ProcessBuilder#start方法的代码来会更好理解
prog变量获取的是cmdarray[0],在后面的调用中,cmdarray[0]可以理解为就是作为启动模块的值来调用,再看到后面ProcessImpl类构造方法的代码就很清晰了
这里的executablePath就是cmdarray[0],也就是字符串”ipconfig /all”,这样一来肯定是找不到对应启动模块的,于是就抛出了上述错误。 知道了原因就比较好解决了,把”ipconfig /all”先分开来再以varargs数组或List的字符串形式传入就可以了,如下: Process p = null;
// 执行ipconfig /all命令
p = new ProcessBuilder("ipconfig","/all").start();
ProcessImpl ProcessImpl类是Process抽象类的实现类,主要就是实现了Process类的waitFor、destory等等进程关键操作方法。 ProcessImpl类需要值得注意的就是它没有共有构造方法,只有一个private类型的方法,所以是不能直接实例化ProcessImpl类的,虽然我们不能直接new一个ProcessImpl,但是可以利用反射去调用非public类的方法,如下: String[] cmds = {"ipconfig","/all"};
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);
System.out.println(bos.toString());
总结
这次简单调试分析了下java.lang.Runtime#exec()、java.lang.ProcessBuilder#start()、java.lang.ProcessImpl#start()这三种java命令执行的方法,也多了点对这几个类之间关系的理解,若文中有什么错误的地方,望师傅们多多斧正。 Reference
https://s.yam.com/ddxhQ http://www.lmxspace.com/2019/10/08/Java%E4%B8%8B%E5%A5%87%E6%80%AA%E7%9A%84%E5%91%BD%E4%BB%A4 (点击“阅读原文”查看链接) - End - 精彩推荐 Google Chromium CVE-2019-5826 UAF分析
Glibc 2.27关于Tcache的增强保护
美国联邦调查局近期正在对美国医疗卫生保健机构遭受勒索软件攻击的事件进行调查
快报!坦桑尼亚大选前夕竟出现大规模网络服务中断
戳“阅读原文”查看更多内容 本文始发于微信公众号(安全客):Java安全之命令执行
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论