XMLDecoder介绍
XMLDecoder是java自带的以SAX方式解析xml的类,其在反序列化经过特殊构造的数据时可执行任意命令。
所谓的解析就是在java对象和xml文件之间的转化。
SAX是什么
SAX全称为Simple API for XML
,在Java中有两种原生解析xml的方式,分别是SAX和DOM。两者区别在于:
- Dom解析功能强大,可增删改查,操作时会将xml文档以文档对象的方式读取到内存中,因此适用于小文档
- Sax解析是从头到尾逐行逐个元素读取内容,修改较为不便,但适用于只读的大文档
SAX采用事件驱动的形式来解析xml文档,简单来讲就是触发了事件就去做事件对应的回调方法。
在SAX中,读取到文档开头、结尾,元素的开头和结尾以及编码转换等操作时会触发一些回调方法,你可以在这些回调方法中进行相应事件处理:
- startDocument()
- endDocument()
- startElement()
- endElement()
- characters()
简单demo
一个简单的pojo类
package org.example.XMLTest;
public class Person { private String name; private 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.XMLTest;
import java.beans.XMLDecoder; import java.beans.XMLEncoder; import java.io.*;
public class Demo1 { 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(person.getAge()); System.out.println(person.getName()); System.out.println("反序列化成功!"); }
public static void main(String[] args) throws FileNotFoundException { Demo1 xmlTest = new Demo1(); xmlTest.xmlEncode(); xmlTest.xmlDecode(); } }
|
序列化后的xml结构
<?xml version="1.0" encoding="UTF-8"?> <java version="1.8.0_65" class="java.beans.XMLDecoder"> <object class="org.example.XMLTest.Person"> <void property="age"> <int>18</int> </void> <void property="name"> <string>test</string> </void> </object> </java>
|
基于SAX的XML解析
接下来自己写一个基于SAX的xml解析
我们只要跟一下源码就可以发现它实现SAX形式是继承DefaultHandler类的,该类在org.xml.sax.helpers包下
package org.example.XMLTest;
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 Demo2 extends DefaultHandler { public static void main(String[] args) { SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); try { SAXParser parser = saxParserFactory.newSAXParser(); Demo2 dh = new Demo2(); String path = "person.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++) { System.out.println(attributes.getQName(i) + "=" + attributes.getValue(i)); } 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); } }
|
然后解析前面的person.xml,结果如下:
startDocument() startElement() version=1.8.0_65 class=java.beans.XMLDecoder characters() startElement() class=org.example.XMLTest.Person characters() startElement() property=age characters() startElement() characters() endElement() int characters() endElement() void characters() startElement() property=name characters() startElement() characters() endElement() string characters() endElement() void characters() endElement() object characters() endElement() java endDocument()
|
可以知道我们可以通过继承SAX的DefaultHandler类,重写其事件方法,就能拿到XML对应的节点、属性和值。
XMLDecoder也是基于SAX实现的xml解析,不过他拿到节点、属性、值之后通过Expression创建对象及调用方法。
反序列化漏洞原理
概括来说,XMLDecoder产生漏洞的原因主要有以下几个关键因素:
- XMLDecoder是java自带的以SAX方式解析xml的类,其在反序列化经过特殊构造的XML数据可以覆盖对应Beans成员值,这给构造gadget产生了可能。
- XMLDecoder使用反射来动态生成Beans,这给触发gadget产生了可能。
以上两个条件同时都具备,使得XMLDecoder产生远程代码执行漏洞的攻击面。
流程分析
这里用一个弹计算器的payload去分析一下
<java> <object class="java.lang.ProcessBuilder"> <array class="java.lang.String" length="1"> <void index="0"> <string>calc</string> </void> </array> <void method="start"> </void> </object> </java>
|
package org.example.XMLTest;
import java.beans.XMLDecoder; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream;
public class Attack { 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(); } }
|
可以直接在ProcessBuilder#start处打个断点然后从调用栈帧开始看
他是从readObject方法开始的,我们从那里开始跟进
跟进到XMLDecoder#parsingComplete方法,这里会调用DocumentHandler进行xml的解析
这个handler就是继承DefaultHandler类的,调用他的parse来对输入input进行解析,也就是以SAX方式解析
进到方法里面,他的解析写法也和我们前面写的逻辑是一样的,都要创建一个SAXParser,然后接下来的解析就重点去关注一下他的解析函数,我们继续跟进
在DocumentHandler的构造方法中指定了可用的标签类型
对应了com.sun.beans.decoder包中类
我们断在DocumentHandler#startElement方法
他首先解析java标签,设置Owner和Parent,owner就是DocumentHandler类,getElementHandler方法就是从DocumentHandler构造方法时创建的map中取对应的class,如果没有则会抛出异常
然后到解析object标签的时候,会去拿name和value也就是class和ProcessBuilder
然后就是通过addAttribute来添加属性
这里还会调用findClass,这里是网上走到了父类NewElementHandler的这个方法
然后就是解析array标签,同样添加属性,后面就是重复的过程,自己调试看一遍即可
解析完开始标签就到闭合标签了
进入到endElement方法
首先就是闭合的string标签
接着闭合void和array
然后解析void标签的的method属性
将this.method赋值为start,然后紧接着又是相关的闭合操作
中间的很多大同小异的流程就不记录了,看看文章即可
最终走到关键的地方ObjectElementHandler#getValueObject方法里
这里是构建了一个java.beans.Expression表达式类
然后看Expression的getValue方法
里面会调用invoke方法,会走到其父类Statement的invoke方法
内部就是利用反射来进行方法调用
最终就会返回ProcessBuilder这个类,继续往后
到这里他就会执行start方法,内部也是反射调用start方法,后续的步骤也是差不多的就不调了
最终就是相当于最后拼接了一个表达式:new java.lang.ProcessBuilder(new String[]{“calc”}).start();
Expression和Statement
两者都是java反射的封装
package org.example.XMLTest;
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.XMLTest;
import java.beans.Expression; import java.beans.Statement;
public class Test { 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[]{"小明"}); System.out.println(expression.getValue()); } catch (Exception e) { e.printStackTrace(); } } }
|
他们的区别就是Expression的getValue内部执行invoke方法后能够获取返回值,而Statement是没有getValue方法是获取不了返回值的
总结
XMLDecoder导致漏洞的原因就在于处理节点的时候,信任了外部输入的XML指定节点类型信息(class类型节点),同时在进行节点Expression动态实例化的时候(通过invoke实现set()方法,允许节点属性由XML任意控制
导致Expression的set()方法被重载为风险函数(本例中是start)。Expression动态解析因为Java反射特性实现了代码执行。
参考
https://y4er.com/posts/java-xmldecoder
https://www.cnblogs.com/LittleHann/p/17814641.html