🌸 FastJson
FastJson
是一个由阿里巴巴开发的高性能JSON处理库,支持Java
对象与JSON
字符串之间的互相转换。
本次漏洞研究基于FastJson
的1.2.24
版本。也就是最早出现FastJson
反序列化漏洞的版本。
CVE-2017-18349,FastJson<=1.2.24
🍂 Demo
先来熟悉一下什么是Json
:
package org.y4y17;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;public class Main {public static void main(String[] args) {String s = "{\"name\":\"Y4y17\",\"age\":17}";JSONObject jsonObject = JSON.parseObject(s);System.out.println(jsonObject);System.out.println(jsonObject.get("name"));System.out.println(jsonObject.get("age"));}
}
这里简单的写了一个Demo
,String s = "{\"name\":\"Y4y17\",\"age\":17}";
创建了一个字符串,然后利用JSON.parseObject
方法来将字符串解析为对象。
JSON.parseObject
,是将Json
字符串转化为相应的对象;JSON.toJSONString
,是将对象转化为Json
字符串。
然而在反序列化的时候,可以指定转化的对象类型!此时JSON.parseObject
方法便会将其转化为对应的一个javaBean
,比如我们这里存在一个JavaBean
:
package org.y4y17;public class Person {private String name;private int age;public Person() {System.out.println("调用了constructor方法");}public String getName() {System.out.println("调用了getName方法");return name;}public void setName(String name) {this.name = name;System.out.println("调用了setName方法");}public int getAge() {System.out.println("调用了getAge方法");return age;}public void setAge(int age) {System.out.println("调用了setAge方法");this.age = age;}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +'}';}
}
那么在转化为Java
对象的时候可以通过指定要转化的类,来完成对应对象的转化。如下代码:
package org.y4y17;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;public class Main {public static void main(String[] args) {String s = "{\"name\":\"Y4y17\",\"age\":17}";Person person = JSON.parseObject(s, Person.class);System.out.println(person.getAge());}
}
在上面的Person
类中的各个set
和get
方法中,打印了相关的方法名,以便更加清晰的看到调用关系。如上代码的执行结果如下:
当我们指定了要转化的类的时候,发现整个转化的过程中,先调用了构造器,然后就是调用相关的set
方法和get
方法(在这里的get
方法是在getAge()
调用的时候触发的)。
然而在FastJson
反序列化的时候,可以指定一个@type
字段,用来表明指定反序列化的目标恶意对象类。比如我们在String
字符串里面添加一个@type
字段。
String s = "{\"@type\":\"org.y4y17.Person\",\"name\":\"Y4y17\",\"age\":17}"
package org.y4y17;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;public class Main {public static void main(String[] args) {String s = "{\"@type\":\"org.y4y17.Person\",\"name\":\"Y4y17\",\"age\":17}";JSONObject jsonObject = JSON.parseObject(s);System.out.println(jsonObject);}
}
然而在上面的JSON
转化为Java
对象的时候,通过写入@type
字段,实现了指定类的反序列化,成功的调用了Person
类中的set
和get
方法以及构造器。
🍂 流程分析
下断点进行调试:
跟进到JSON
的parseObject
方法中:在JSON
类中可以看到存在很多种方法,其中他们的参数是不同的:
在parseObject
方法中:
首先parse
主要负责解析我们传递的text
,最后便会返回这个Person
类对象,在这个过程中就会调用构造器和set
方法,而最后的return
,将对象强制转化为JSONObject
对象,这个过程中会调用到Person
类中的get
方法!继续跟进到parse
方法中:
其中这个parse
方法也是一个静态的方法,可以直接在外部进行调用:
在上述的代码中,会创建一个默认的JSON
解析器:
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
然后利用创建的解析器进行解析,最终返回Person
类对象。ParserConfig.getGlobalInstance()
是创建了一个解析器,进行初始化,主要就是初始化一些配置,可以跟进一下看:
可以看到这个parserConfig
类中的构造器:
放进去了一下默认的反序列化器;比如:derializers.put(Map.class, MapDeserializer.instance)
;如果是Map
类型的话,就使用MapDeserializer
反序列化器
同时整个过程中还设置了一些黑名单,也就是当时在这个漏洞出现之前,禁止指定的类:
如这里的java.lang.Thread
,线程类。
接下来JSON
扫描器,就会从头开始扫描我们传递进来的string
:
首先就会判断第一个字符是不是一个" { "
,如果是的话,也就是代表着一个JSON
格式的字符串,进入到if
条件里面:
或者是不是一个“[”
,如果是的话,其实就是一个数组。由于我们传递的就是一个符合JSON
格式的字符串,所以if
直接就进入了,其他的都不会进去,此时就会创建好默认的JSON
解析器了,下一步就是解析的过程:
跟进到这个parse
方法中:
parse
方法中通过switch
来匹配第一个字符到底是什么,因为我们是一个左大括号,所以继续往下走:
这里便会创建一个JSONObject
,他其实是一个Map
这里相当于是创建了一个新的Map
,size
为0
这里我们继续跟进到parseObject
方法中!先是经过一系列的判断:
因为这里我们是“{”
,所以这里所有的if条件都不满足,直接过:
接下来经过了一个死循环!必须存在break
或者return
才能结束。if
和while
主要是进行了重复的事情,寻找“,”
,如果存在的话,就跳过。
然后继续判断是不是双引号和:、}等符号:整个过程中就是取出来了一个key
!也就是@type
继续往下走的话,其实会去判断拿到这个key
是不是和默认的key
一样!默认的可以就是@type
之后就会通过loadClass
进行类加载了!继续跟进:
先从缓存中查找是不是已经加载过了,或者有相关的记录(空间换时间),如果存在的话,就直接拿出来用了!
之后便会判断类名的首字符是不是“[”
,也就代表着数组,之后又判断是不是以“L”
开头,同时以“;”
结尾。我们这里都不满足,所以就不管了,继续往下走:
这里就会获取到AppClassLoader
,通过AppClassLoader
进行加载!然后就会把他放到缓存中,最后return
这个类。此时整个loaderclass
也就结束了。
接下来回到了默认的JSON
解析器中:
这里if
条件都不满足,继续往下走吧:
这里的object
就是最开始创建的JSONObject
,当时说他是一个Map
,整个过程中还没有往里面存放东西,所以他的size
就是0
,这里的if
条件还是不成立,继续往下走就会发现:ObjectDeserializer deserializer = config.getDeserializer(clazz);
从config
里面获取了一个反序列化器!将我们的类放到里面。而这个获取到的反序列化器就是前面我们看到ParserConfig
构造器里面初始化的那些,当然我们这里继续跟进,看看他获取到的反序列化器是什么?
这里从构造器初始化的那些反序列化器中获取对应的,其实是没有的!继续往下走是泛型 然后调用了:getDeserializer
跟进之后,发现还是一直在找这个反序列化器,一直找不到,就各种判断是不是type
为空等,是不是自己通过注解的方式写了一个反序列化器,显然我们是没有的!所以这里的if都是进不去的!
之后就开始判断是不是黑名单里面的~ 继续往下走:
然后又去判断了是不是Enum
等,到最后一直找不到,就创建了一个JavaBeanDeserializer
反序列化器!继续跟进到这个方法中:
这里存在一个变量asmEnable
变量,初始化是true
。
继续走,调用了JavaBeaninfo
类中的getBuilderClass
方法,由于传递的jsonType
是一个null
值,最终方法return null
。还是继续往下走,superclass
复制本身,通过一个死循环,获取这个类的修饰符,判断是不是public
。如果不是的话,那么asmEnable
就会被设置为false
整个过程中又去判断了是不是参数为空,指定的类是不是一个接口!(反正又是各种不满足!)
最后调用了一个JavaBeaninfo
的build
方法,跟进看一下:
到这个build
方法中,发现获取了Person
类中的所有的Filed
,以及Public
属性修饰的方法,以及默认的构造器!然后判断这个默认的构造器是不是为空,如果不为空的话,就设置一下可访问!
接下来就是三个for循环:
首先第一个就是获取所有的set
方法!第二个是获取所有的public
属性的变量,第三个就是获取get
方法!先看第一个方法:
这里先挨个遍历这些方法,他的方法名字长度是不是小于4,因为set就占了三个字符长度了~ 又判断了这个方法的修饰符是不是静态的,以及方法的返回值是什么,因为set
方法一般就是没有返回值的!
当方法是set
相关的方法的时候,经过上述的条件判断,一直往下走:
获取到set
之后的第一个字母,判断是不是大写的!然后判断了一个静态变量是不是true
,如果不是的话,那么就将这个大写字母转换为小写字母!然后将后面的其他字符进行拼接!
之后就是获取这个方法中的变量:
最后通过add
方法,把整个获取到信息,全部放入到List
里面!
在这个方法中还创建一个FieldInfo
,这里我们跟进到这个方法中:
在这个方法中存在一个Feild
,对整个逻辑存在相关的影响:
就是这个getOnly
变量,正常来说他是一个false
,往下走的时候,可以清楚的看到else{ }
里面存在着这个变量的覆盖!getOnly=true
正常这个types
就是参数的长度,我们这里就是1,如果不是1的话,那就会走到这个else
代码里面,然后给getOnly
进行赋值!(这里下面也就没有什么其他的东西了,继续往下走就回到了add
方法,这里的getOnly
有什么用后续再说!)
最终经过上面的for
循环,整个List
里面就存放了两个值,一个是age
,一个是name
:
之后便是进入到获取类中所有的变量,因为当前的类中是没有Public
修饰的变量的,所以就不用看了。
进入到第三个for
循环,就是获取get
方法,但是这里并不会add
(像第一个for
循环,寻找set
方法时),
因为在这个for
循环中,会去判断这个方法的返回值类型是不是Collection
或者是不是Map
等,如果是的话,才会进行后续的add
,还有一个条件就是:
他在找get
的时候,会看一下fieldList
里面是不是存在这个字段,如果存储过了,那就不会add
。所以这里总结一下在寻找get
方法的时候,触发add
方法的两个条件:
-
- 返回值类型需要是
Map、Collections
等要求的那些 - 同时
fieldList
里面没有这个字段(换句话说就是这个字段,只有get
方法,而没有set
方法!)
- 返回值类型需要是
最后直接返回了一个JavaBeanInfo
,其实在创建反序列化器的整个过程中就是在获取我们这个Person
类中的所有的信息!
继续往下走:
下面依然会去通过一些if条件,这个asmEnable
还是存在可以修改的情况!比如clazz
不是一个接口,默认的构造器是null
,同样会修改asmEnable
为false
!
接下来就是会通过一个for
循环!可以遍历Field
里面的getonly
,如果有一个是true
的话,就可以修改asmEnbale
为false
了!但是这里并不满足!
最后就会判断这个asmEnable
是不是false
,如果是的话,就会创建一个JavaBeanDeserializer
反序列化器!否则的话,会利用asmFactory
去创建一个反序列化器!
所以这里创建的反序列化器并不是默认的那个反序列化器:
而是一个叫FastJsonASMDeserializer
的反序列化器!他是一个临时创建的类,所以这里是没办法调试的!回顾整个创建的过程中,其实我们在之前有说到过一个field
,就是getonly
。当他满足的为true
的时候,asmEnable
也就变成了false
。此时创建的反序列化器就是默认的,此时也就可以进行调试了。
也就是Fieldinfo
类中的构造器中,他去获取了方法的参数,判断参数类型的长度是不是1,如果不是的话,就可以进入到else
代码中,将getOnly
设置为true
!
那么在往前去找我们从哪里进来这个构造器的:
是在JavaBeanInfo
类中的两个for
循环中出现的调用!第一个就是寻找set
方法,第二个就是寻找get
方法!然而在第一个for
循环中的FieldInfo
里面其实是无法将getOnly
设置为true
的。原因就是他的参数类型长度肯定是1,所以在第一个for
循环中无法设置了!
只能在第二个for
循环中进行设置!也就是寻找get
方法的for
循环。但是之前我们就谈到过这个for
循环中的add
方法是需要满足条件才能进入的!
他的返回值必须要是if
条件里面的才行!所以我们这里需要创建一个返回值是如上类型的set
方法!之前还说到一个条件就是 这个field
只能有get
方法,没有set
方法,不然的话,在上面for
循环中,将这个field
加入到FeildList中就不会再调用get
方法了!
所以这里我们定义一个map
类型的field
:
package org.y4y17;import java.util.Map;public class Person {private String name;private int age;private Map map;public Map getMap() {System.out.println("调用了getMap方法");return map;}public Person() {System.out.println("调用了constructor方法");}public String getName() {System.out.println("调用了getName方法");return name;}public void setName(String name) {this.name = name;System.out.println("调用了setName方法");}public int getAge() {System.out.println("调用了getAge方法");return age;}public void setAge(int age) {System.out.println("调用了setAge方法");this.age = age;}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +'}';}
}
再次调试,直接断点在第二个for
循环那里,因为没有给map
变量设置set
方法,所以前面的寻找set
方法的时候,往FieldList
里面加入的还是age
和 name
两个field
:
此时是getMap
!我们往下跟进:
这里的if
条件满足,所以就会进入到这个if
条件里面了!
而且在fieldList
里面寻找也没找到,原因就是他根本就没有set
方法,所以没找到!
到这里的话就继续跟进到Fieldinfo
方法里面:
到这里的时候,getMap
方法的参数是空的,所以这里就不满足了,成功的进入到了else
代码里面,成功的将getOnly
设置成了true
!
最后return
。回到上层,继续看:
此时已经是返回了beaninfo
,里面的field就是三个了,分别就是age
、name
、map
;继续代码往下走就会经过for
循环,去获取field
的getonly
变量,如果是true
的话,就会将asmEnable
设置为false
!然而上面的map
的getonly
就已经被我们设置成了true
!所以能进入if条件:
这里可以看到map
的getOnly
确实就是true
。因此可以设置asmEnable
为false
。
然后往下走成功进入到if条件,创建了一个默认的JavaBeanDeserializer
对象!
然后将成功的创建了一个反序列化器,这个是可以调试的。(上述的目的仅仅是为了调试~ )接下来就是利用反序列化器进行反序列化操作了,继续跟进到deserialze
方法中:
发现调用了createInstance
方法,其中parser
就是默认的JSONparser
,type
便是指定的类,继续跟进:
最终通过构造器进行了newInstance
,也就执行了构造器方法!
接着继续往下走便是setvalue
:
赋值操作无非就是通过反射或者是通过调用set
方法!跟进到这个setValue
方法中!
然后就是通过invoke
进行调用赋值!这里其实有一个if条件,并没有满足,直接跳到了invoke
这里执行了。
这里的getOnly
变量的值是false
。因为他是一个set
方法。最后也就返回了整个调用过程和对象:
可以看到这里并没有执行get
方法,最开始的时候,就已经提到了上面是调用set
方法,而在toJSON
的时候才会进行调用get
方法!
继续跟进到toJSON
里面:
上面一直都在判断这个clazz
是个什么?都不满足,因为他是个Person
类!
这里的getobjectWriter
就是序列化的方法!
接下来就是创建了一个JSONObject
对象(和之前的一样就是一个Map
)
这里就是获取到了三个键值对!然后往JSONObject
里面存放!整个过程中也是通过调用invoke
方法来实现的get
方法执行:
Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
在javaBeanSerializer.getFieldValuesMap(javaObject)
方法中调用了invoke
方法:
继续跟进到这个getPropertyValue
方法中:
该方法中又调用了get
方法!继续跟进到这个get
方法中:
在get
方法中,先是判断了method
是不是为空,如果不为空的话,就通过invoke
方法来调用get
方法。
此时便成功的完成了整个的调用。整个过程中,我们传递的参数,并没有传递map
。如果我们传递map
的话,在利用反序列化器进行反序列化的时候,也是会调用getMap
的(原因是,寻找set
方法的时候,mapfield
并没有set
方法,仅仅有一个get
方法!)
🌸 流程总结
整个过程分为三个阶段
-
JSON
解析器解析阶段,此时还只是当作JSON
字符串来解析Java
反序列化器解析阶段,此时是因为在JSON
字符串中找到了@type
字段,便开始当作是Java
对象解析toJSON
阶段,此时会调用get方法
思考:整个流程分析完了,那么如何利用这个漏洞/缺陷来进行攻击呢?
-
- 只要能找到一个类,该类中的
set
或者get
方法中存在调用链,便可以利用
- 只要能找到一个类,该类中的
🍂 本地Demo
尝试创建一个类,实现弹计算器的操作:
package org.y4y17;import java.io.IOException;public class Test {public void setCmd(String cmd) throws IOException {Runtime.getRuntime().exec(cmd);}
}
创建一个Test
类!然后我们传递这个类,通过@type
,进行指定:
package org.y4y17;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;public class Main {public static void main(String[] args) {String s = "{\"@type\":\"org.y4y17.Test\",\"cmd\":\"open -a calculator\"}";JSONObject jsonObject = JSON.parseObject(s);System.out.println(jsonObject);}
}
运行后成功弹出计算器: