XMLDecoder介绍

XMLDecoder是java自带的以SAX方式解析xml的类,其在反序列化经过特殊构造的数据时可执行任意命令。

所谓的解析就是在java对象和xml文件之间的转化。

SAX是什么

SAX全称为Simple API for XML,在Java中有两种原生解析xml的方式,分别是SAX和DOM。两者区别在于:

  1. Dom解析功能强大,可增删改查,操作时会将xml文档以文档对象的方式读取到内存中,因此适用于小文档
  2. 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 {
// 序列化对象到文件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(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();
}
}

image-20241025133812826

序列化后的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包下

image-20241025135348967

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++) {
// getQName()是获取属性名称,
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处打个断点然后从调用栈帧开始看

image-20241025230809734

他是从readObject方法开始的,我们从那里开始跟进

image-20241025231157408

跟进到XMLDecoder#parsingComplete方法,这里会调用DocumentHandler进行xml的解析

image-20241025231439438

这个handler就是继承DefaultHandler类的,调用他的parse来对输入input进行解析,也就是以SAX方式解析

image-20241026004719002

进到方法里面,他的解析写法也和我们前面写的逻辑是一样的,都要创建一个SAXParser,然后接下来的解析就重点去关注一下他的解析函数,我们继续跟进

在DocumentHandler的构造方法中指定了可用的标签类型

image-20241026005350433

对应了com.sun.beans.decoder包中类

我们断在DocumentHandler#startElement方法

image-20241026012931997

他首先解析java标签,设置Owner和Parent,owner就是DocumentHandler类,getElementHandler方法就是从DocumentHandler构造方法时创建的map中取对应的class,如果没有则会抛出异常

image-20241026014426012

然后到解析object标签的时候,会去拿name和value也就是class和ProcessBuilder

然后就是通过addAttribute来添加属性

image-20241026014707208

这里还会调用findClass,这里是网上走到了父类NewElementHandler的这个方法

然后就是解析array标签,同样添加属性,后面就是重复的过程,自己调试看一遍即可

解析完开始标签就到闭合标签了

进入到endElement方法

image-20241026015510343

首先就是闭合的string标签

image-20241026015649368

接着闭合void和array

然后解析void标签的的method属性

image-20241026015858555

将this.method赋值为start,然后紧接着又是相关的闭合操作

中间的很多大同小异的流程就不记录了,看看文章即可

最终走到关键的地方ObjectElementHandler#getValueObject方法里

image-20241026020414044

这里是构建了一个java.beans.Expression表达式类

image-20241026020515551

然后看Expression的getValue方法

image-20241026020824998

里面会调用invoke方法,会走到其父类Statement的invoke方法

image-20241026021017273

image-20241026021656440

内部就是利用反射来进行方法调用

image-20241026021040905

最终就会返回ProcessBuilder这个类,继续往后

image-20241026021157722

到这里他就会执行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