Java审计之命令执行
Java审计之命令执行
写在前面
其实之前第一次审计时也有遇到过命令执行的漏洞,包括之前学习反射时也有接触到。所以潜意识里对此也觉得这个洞审起来应该不会太难,重点看一下Runtime,ProcessBuilder,getMethod可能就行了。结果在网上一冲浪才发现姿势原来还有这么多... 😄 ,随即感觉学习了一波。PS:师傅们真的tql
Java-命令执行
首先回顾下那些类可以构造出命令执行
Runtime
Runtime.exec("command")
public class LocalRuntime extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
ServletOutputStream sos = resp.getOutputStream();
int len;
byte[] bytes = new byte[1024];
while ((len = ins.read(bytes))!=-1){
sos.write(bytes, 0, len);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
}
反射Runtime
1、反射获取Runtime的class对象
2、获取Runtime构造方法
3、newInstance一个新Runtime的实例对象
4、获取exec方法
5、invoke激活执行
public class ReflactRuntime extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String cmd = req.getParameter("cmd");
try {
Class cls = Class.forName("java.lang.Runtime");
Constructor constructor = cls.getDeclaredConstructor();
constructor.setAccessible(true);
Object runtime = constructor.newInstance();
Method exec = cls.getMethod("exec", String.class);
Process process = (Process) exec.invoke(runtime, cmd);
InputStream ins = process.getInputStream();
int len;
byte[] bytes = new byte[1024];
ServletOutputStream sos = resp.getOutputStream();
while ((len = ins.read(bytes)) != -1){
sos.write(bytes, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
}
ProcessBuilder
ProcessBuilder此类用于创建操作系统进程。每个ProcessBuilder
实例管理进程属性的集合。 start()
方法使用这些属性创建一个新的Process
实例。 start()
方法可以从同一实例重复调用,以创建具有相同或相关属性的新子进程。
ProcessBuilder.start()
和Runtime.exec
方法都可以创建一个本机进程并返回一个Process
子类的Process
(Runtime.exec底层调用的也是ProcessBuilder.start()
),可以用来控制进程并获取有关它的信息。
ProcessBuilder命令执行
1、创建ProcessBuilder实例化对象
2、调用start方法执行
3、返回的Process对象调用getInputStream获取输入流
4、读取输入流写入输出流
public class ProccessBuilder1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
InputStream ins = new ProcessBuilder(req.getParameterValues("cmd")).start().getInputStream();
ServletOutputStream sos = resp.getOutputStream();
int len;
byte[] buffer = new byte[1024];
while ((len = ins.read(buffer)) != -1){
sos.write(buffer,0, len);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
}
ProcessBuilder.start()和Runtime.exec()的一些区别
ProcessBuilder.start() 和 Runtime.exec() 方法都被用来创建一个操作系统进程(执行命令行操作),并返回 Process 子类的一个实例,该实例可用来控制进程状态并获得相关信息。
ProcessBuilder.start() 和 Runtime.exec()传递的参数有所不同,Runtime.exec()可接受一个单独的字符串,这个字符串是通过空格来分隔可执行命令程序和参数的;也可以接受字符串数组参数。
而ProcessBuilder的构造函数是一个字符串列表或者数组。列表中第一个参数是可执行命令程序,其他的是命令行执行是需要的参数。
反射ProcessBuilder
这里需要注意有几点是不同于反射调Runtime的地方
1、首需要用List.class
占位调用getConstructor(List.class)
获取ProcessBuilder
的有参构造。
2、传入的参数需要用Arrays.asList
做一下处理,因为调的是List类型的有参构造。
示例代码
public class ReflactProccessBuilder extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String arg = req.getParameter("cmd");
List<String> cmd = Arrays.asList(arg);
System.out.println(cmd);
Class cls = Class.forName("java.lang.ProcessBuilder");
Constructor constructor = cls.getConstructor(List.class);
constructor.setAccessible(true);
Object pb = constructor.newInstance(cmd);
Method start = cls.getMethod("start");
Process process = (Process) start.invoke(pb);
InputStream ins = process.getInputStream();
int len;
byte[] buffer = new byte[1024];
ServletOutputStream os = resp.getOutputStream();
while ((len = ins.read(buffer)) != -1){
os.write(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
}
在本地下更简短的形式
Class cls = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("open -a calculator")));
ProcessImpl
说到这里其实ProcessImpl
更为底层,ProcessBuilder.start()
调用的就是它。
UNIXProcess
和ProcessImpl
其实就是最终调用native
执行系统命令的类,这个类提供了一个叫forkAndExec
的native方法,如方法名所述主要是通过fork&exec
来执行本地系统命令。
但是因为不能直接调用ProcessImpl所以只能通过反射来获取
public class ReflactProccessImpl extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String[] cmds = req.getParameterValues("cmd");
Class clazz = Class.forName("java.lang.ProcessImpl");
Method start = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
start.setAccessible(true);
Process process = (Process) start.invoke(null, cmds, null, ".", null, true);
InputStream ins = process.getInputStream();
int len;
byte[] buffer = new byte[1024];
ServletOutputStream os = resp.getOutputStream();
while ((len = ins.read(buffer)) != -1){
os.write(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
}
UNIXProcess
类似于ProcessImpl类
public class UNIXProccess1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String[] cmds = req.getParameterValues("cmd");
Class clazz = Class.forName("java.lang.UNIXProcess");
Method start = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
start.setAccessible(true);
Process process = (Process) start.invoke(null, cmds, null, ".", null, true);
InputStream ins = process.getInputStream();
int len;
byte[] buffer = new byte[1024];
ServletOutputStream os = resp.getOutputStream();
while ((len = ins.read(buffer)) != -1){
os.write(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
}
审计时需要注意的点
其实如上大概就已经可以感觉出需要注意的地方了
java.lang.Runtime
java.lang.Runtime.getRuntime()
java.lang.Runtime.getRuntime().exec
getMethod().invoke()
java.lang.ProcessBuilder
java.lang.ProcessBuilder.start()
java.lang.ProcessImpl
java.lang.UNIXProcess
...
其实像ProcessImpl
和UNIXProcess
可能遇到的概率会很小,大概率可能会出现Runtime
和ProcessBuilder
以及反射去调用任意方法的执行,尤其是反射这个点,经常能看到getMethod().invoke()
的点审计时可以重点定位这些危险类和方法即可,然后路由可通且没有Filter
限制或者可以绕过即可RCE。
小结
其实对于审计命令执行来讲,这里知识粗略的列举了一些平常会接触到的命令执行的类,可能还是反射和Runtime以及ProcessBuilder会遇到的多些,包括有些还没说的比如readObject配合存在gadget的第三方依赖去打反序列化RCE或者通过JNI去构造命令执行等等,这些打算放到后面研究反序列化时再单独拿出来分析。
目前只是更多的是总结了不借助反序列化点的正常情况下在定位危险函数时需要注意哪些危险类和方法,思路和审计其他漏洞还是一样的。
Reference
javasec.org