序言
往事依稀浑似梦,都随风雨到心头。
今天来分析Jackson。
简介
Jackson是一个开源的Java序列化和反序列化工具,可以将Java对象序列化为XML或JSON格式的字符串,以及将XML或JSON格式的字符串反序列化为Java对象。
序列化操作
在jackson内部,需要进行序列化的函数是:
提供了ObjectMapper.writeValueAsString()
和ObjectMapper.readValue()
两个方法来实现序列化和反序列化的功能。
ObjectMapper.writeValueAsString()
———序列化ObjectMapper.readValue()
————————反序列化
pom.xml
:
1 | <dependencies> |
来个小Demo:
1 | public class Test { |
运行结果:
JacksonPolymorphicDeserialization
简单地说,Java多态就是同一个接口使用不同的实例而执行不同的操作。
那么问题来了,如果对多态类的某一个子类实例在序列化后再进行反序列化时,如何能够保证反序列化出来的实例即是我们想要的那个特定子类的实例而非多态类的其他子类实例呢?——Jackson实现了JacksonPolymorphicDeserialization机制来解决这个问题。
JacksonPolymorphicDeserialization即Jackson多态类型的反序列化:在反序列化某个类对象的过程中,如果类的成员变量不是具体类型(non-concrete),比如Object、接口或抽象类,则可以在JSON字符串中指定其具体类型,Jackson将生成具体类型的实例。
简单地说,就是将具体的子类信息绑定在序列化的内容中以便于后续反序列化的时候直接得到目标子类对象,其实现有两种:
- DefaultTyping
- @JsonTypeInfo注解。
下面具体看一下。
DefaultTyping
com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping
Jackson提供一个enableDefaultTyping设置,其包含4个值:
1 | public enum DefaultTyping { |
JAVA_LANG_OBJECT
当类里的属性声明为一个Object时,会对该属性进行序列化和反序列化,并且明确规定类名。(当然,这个Object本身也得是一个可被序列化/反序列化的类)。
举个例子,给 People 里添加一个 Object object 的属性:
1 | public class test1 { |
看看结果:
也就是说,在反序列化的时候,会将类中的夹带的其他类跟着一起还原出来。
OBJECT_AND_NON_CONCRETE
当类里有 Interface 、 AbstractClass 时,对其进行序列化和反序列化。(当然,这些类本身需要是合法的、可以被序列化/反序列化的对象)。
此外,enableDefaultTyping()默认的无参数的设置就是此选项。
看看下面这个:
1 | public class test2 { |
看看结果:
NON_CONCRETE_AND_ARRAYS
支持上文全部类型的Array类型。
例如下面的代码,我们的Object里存放0range的对象数组:
1 | public class test2 { |
结果:
NON_FINAL
除了前面的所有特征外,包含即将被序列化的类里的全部、非final的属性,也就是相当于整个类、除final外的属性信息都需要被序列化和反序列化。
例如下面的代码,添加了类型为0range
的变量,非Object也非虚,但也可以被序列化出来。
1 | public class test2 { |
看结果:
总结
DefaultTyping的几个设置选项是逐渐扩大适用范围的,如下表:
DefaultTyping类型 | 描述说明 |
---|---|
JAVA_LANG_OBJECT | 属性的类型为Object |
OBJECT_AND_NON_CONCRETE | 属性的类型为Object、Interface、AbstractClass |
NON_CONCRETE_AND_ARRAYS | 属性的类型为Object、Interface、AbstractClass、Array |
NON_FINAL | 所有除了声明为final之外的属性 |
@JsonTypeInfo注解
@JsonTypeInfo注解是Jackson多态类型绑定的一种方式,支持下面5种类型的取值:
1 | (use = JsonTypeInfo.Id.NONE) |
讲到底,其实就是给类中属性加注解。
JsonTypeInfo.Id.NONE
demo如下:
1 | public class JTTest { |
结果:
和没有设置值为JsonTypeInfo.Id.NONE的@JsonTypeInfo注解是一样的。
这种方式的输出结果实际上是我们最想要的,这里只需要相关参数的值,并没有其他一些无用信息。
JsonTypeInfo.Id.CLASS
修改User类中的object属性@JsonTypeInfo注解值为JsonTypeInfo.Id.CLASS。
输出看到,object属性中多了”@class”:”com.fxc.Height”,即含有具体的类的信息,同时反序列化出来的object属性Height类对象,即能够成功对指定类型进行序列化和反序列化:
也就是说,在Jackson反序列化的时候如果使用了JsonTypeInfo.Id.CLASS
修饰的话,可以通过@class的方式指定相关类,并进行相关调用。
JsonTypeInfo.Id.MINIMAL_CLASS
修改User类中的object属性@JsonTypeInfo注解值为JsonTypeInfo.Id.MINIMAL_CLASS。
输出看到,object属性中多了”@c”:”com.fxc.Height”,即使用@c替代料@class,官方描述中的意思是缩短了相关类名,实际效果和JsonTypeInfo.Id.CLASS类似,能够成功对指定类型进行序列化和反序列化,都可以用于指定相关类并进行相关的调用。
JsonTypeInfo.Id.NAME
修改User类中的object属性@JsonTypeInfo注解值为JsonTypeInfo.Id.NAME。
输出看到,object属性中多了”@type”:”Height”,但没有具体的包名在内的类名,因此在后面的反序列化的时候会报错,也就是说这个设置值是不能被反序列化利用的。
JsonTypeInfo.Id.CUSTOM
其实这个值时提供给用户自定义的意思,我们是没办法直接使用的,需要手动写一个解析器才能配合使用,直接运行会抛出异常:
总结
所以按照上述分析,3种情况下可以触发Jackson反序列化漏洞
1、enableDefaultTyping()
2、@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
3、@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
调试分析
调试代码:
1 | public class JavaLangObject { |
这里函数调用极其复杂,总体归纳出下面这张图:
整体流程:
BeanDeserializer.deserialize()函数中,调用了vanillaDeserialize()函数;
跟进去,BeanDeserializer.vanillaDeserialize()函数的实现比较简单,先调用createUsingDefault()函数来调用指定类的无参构造函数来生成类实例;
BeanDeserializer.vanillaDeserialize()函数调用完无参的类的构造函数生成实例Bean后,就开始进入do while循环,来循环解析键值对中的属性值并调用deserializeAndSet()函数来解析并设置Bean的属性值;
跟进该SettableBeanProperty.deserialize()函数,可以看到有两个反序列化的代码逻辑,其中if判断语句会判断当前反序列化的内容是否携带类型,若是则调用deserializeWithType()函数解析,否则直接调用deserialize()函数解析:
跟进AbstractDeserializer.deserializeWithType()函数中,进一步调用了AsArrayTypeDeserializer.deserializeTypedFromObject()函数来解析:
其中 BeanDeserializerBase#vanillaDeserialize
中有两个关键点:
1、StdValueInstantiator#createUsingDefault
方法负责调用 AnnotatedConstructor#call
中call方法,然后通过反射方式来寻找我们从json中输入的类。
2、MethodProperty#deserializeAndSet
方法负责寻找相关setter设置,这里也是通过invoke反射的方式。
简单梳理一遍,Jackson反序列化的过程为:
- 先调用通过无参的构造函数生成目标类实例
- 接着是根据属性值是否是数组的形式即是否带类名来分别调用不同的函数来设置实例的属性值,其中会调用Object类型属性的构造函数和setter方法。
结论
在Jackson反序列化中,若调用了enableDefaultTyping()函数或使用@JsonTypeInfo注解指定反序列化得到的类的属性为JsonTypeInfo.Id.CLASS或JsonTypeInfo.Id.MINIMAL_CLASS,则会调用该属性的类的构造函数和setter方法。
利用方式
前提条件
满足下面三个条件之一即存在Jackson反序列化漏洞:
- 调用了ObjectMapper.enableDefaultTyping()函数;
- 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.CLASS的@JsonTypeInfo注解;
- 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.MINIMAL_CLASS的@JsonTypeInfo注解;
漏洞原理
由之前的结论知道,当使用的JacksonPolymorphicDeserialization机制配置有问题时,Jackson反序列化就会调用属性所属类的构造函数和setter方法。
而如果该构造函数或setter方法存在危险操作,那么就存在Jackson反序列化漏洞。
CVE-2017-7525(基于TemplatesImpl利用链)
环境限制
Jackson 2.6系列 < 2.6.7.1
Jackson 2.7系列 < 2.7.9.1
Jackson 2.8系列 < 2.8.8.1
JDK版本为1.7.0_21
本地用的jar包:jackson-annotations-2.7.9,jackson-core-2.7.9,jackson-databind-2.7.9,commons-codec-1.12,commons-io-2.5,spring-core-4.3.13.RELEASE。
PoC
直接上代码,首先是Exploit.java,恶意类:
1 | public class Exploit extends AbstractTranslet { |
然后是PoC:
1 | public class PoC { |
Test.java
1 | public class Test { |
运行结果,成功弹出计算器:
这里我们看下PoC:
1 | { |
这里解释下设置的几个JSON键值对:
- transletBytecodes——Base64编码的Exploit恶意类的字节流,编码原因可参考之前的Fastjson系列;
- transletName——TemplatesImpl类对象的_name属性值;
- outputProperties——为的是能够成功调用setOutputProperties()函数,该函数是outputProperties属性的setter方法,在Jackson反序列化时会被自动调用;
跟进调试
在mapper.readValue(jsonInput, Mi1k7ea.class);
中打下断点;同时,我们由之前Fastjson中的分析也知道,TemplatesImpl利用链的其中一步是调用了getOutputProperties()函数,我们也在这里打下断点。
下面开始调试,其中反序列化的处理过程和之前调试的一样,我们直接跟到关键的地方看看就好。
我们知道在BeanDeserializer.vanillaDeserialize()函数中会先新建Bean实例,然后调用deserializeAndSet()函数来解析属性值并设置到该Bean中;而在deserializeAndSet()函数中,会反射调用属性的setter方法来设置属性值。
前两个属性transletBytecodes和transletName都是通过反射机制调用setter方法设置的,但是outputProperties属性在deserializeAndSet()函数中是通过反射机制调用它的getter方法,这就是该利用链能被成功触发的原因,虽然Jackson的反序列化机制只是调用setter方法,但是是调用SetterlessProperty.deserializeAndSet()来解析outputProperties属性而前面两个属性是调用的MethodProperty.deserializeAndSet()解析的,其中SetterlessProperty.deserializeAndSet()函数中是调用属性的getter方法而非setter方法。
利用链:getOutputProperties()->newTransformer()->getTransletInstance()->defineTransletClasses()->恶意类构造函数
PoC不写该属性值的话会报错,我们调试分析下原因。
跟踪到getOutputProperties()->newTransformer()->getTransletInstance()这条调用链时发现,问题出在TemplatesImpl.getTransletInstance()函数中:
由于此处_name为null,导致程序提前return了,并未进入后面生成该Java类实例的代码中,从而也无法成功触发漏洞。
由前面调试分析可知,transletBytecodes和transletName属性值都是通过调用MethodProperty.deserializeAndSet()函数来反射调用其setter方法来设置的。
这里重新带上transletName属性,再次调试,跟进设置transletName属性值时的MethodProperty.deserializeAndSet()函数中,发现其调用的setter方法就是TemplatesImpl.setTransletName()函数:
在大版本下,JDK1.7和1.8中,com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类是有所不同的。
当然,在小版本较高的1.7和某些1.8的还是能够成功触发的,具体的可自行测试。
在我本地的JDK 1.8.0_73 版本中,看到在TemplatesImpl.getTransletInstance()方法中调用了defineTransletClasses()函数来定义Java类,跟进看看:
区别在于新建TransletClassLoader类实例的代码,其中调用了_factory
属性,但是该属性值我们没有在PoC中设置,默认为null,于是就会抛出异常了。
那么如何设置这个_factory
属性呢?我们在PoC中随便填入如'_factory':{},
,会看到如下错误信息:
1 | com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "_factory" (class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl), not marked as ignorable (5 known properties: "uriresolver", "transletBytecodes", "outputProperties", "transletName", "stylesheetDOM"]) |
可以看到,这个错误是Jackson.databind报的,说的是TemplatesImpl类已知的只有5个配置项,即”uriresolver”, “transletBytecodes”, “outputProperties”, “transletName”, “stylesheetDOM”。
在里面没有看到tfactory相关字样,也就是说,Jackson压根就不支持我们在序列化的TemplatesImpl类的内容上添加并解析_tfactory属性。
补丁分析
这里将jackson-databind-2.7.9换成jackson-databind-2.7.9.1。
尝试运行会报错如下,显示因为某些安全原因禁止了该类的加载:
1 | com.fasterxml.jackson.databind.JsonMappingException: Illegal type (com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl) to deserialize: prevented for security reasons |
调试分析,在调用BeanDeserializerFactory.createBeanDeserializer()函数创建Bean反序列化器的时候,其中会调用checkIllegalTypes()函数提取当前类名,然后使用黑名单进行过滤:
注意:实际调试的时候回调用两次BeanDeserializerFactory.createBeanDeserializer()->checkIllegalTypes(),第一次由于是Mi1k7ea类,因此不会被过滤;第二次是TemplatesImpl类,由于其在黑名单中,因此被过滤了。
在jackson-databind-2.7.9.1-sources.jar!/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java中,存在默认的黑名单DEFAULT_NO_DESER_CLASS_NAMES,将TemplatesImpl类以及早期其他常用反序列化利用类都过滤了。
1 | static { |
CVE-2017-17485(基于ClassPathXmlApplicationContext利用链)
影响版本
Jackson 2.7系列 < 2.7.9.2
Jackson 2.8系列 < 2.8.11
Jackson 2.9系列 < 2.9.4
不受JDK限制,可直接在JDK1.8上运行。
需要服务端环境存在额外的jar包,以本地环境为例:jackson-annotations-2.7.9,jackson-core-2.7.9,jackson-databind-2.7.9,spring-beans-5.0.2.RELEASE,spring-context-5.0.2.RELEASE,spring-core-5.0.2.RELEASE,spring-expression-5.0.2.RELEASE,commons-logging-1.2。
PoC
该漏洞需要 Spting spel表达式的配合。
首先在本地起一个http服务,spel.xml:
1 | <beans xmlns="http://www.springframework.org/schema/beans" |
PoC代码:
1 | public class PoC { |
成功触发:
跟进调试
本次的利用链是基于org.springframework.context.support.ClassPathXmlApplicationContext类,利用的原理就是SpEL表达式注入漏洞。
我们在mapper.readValue(payload, Object.class);
上打上断点开始调试。
首先进入:
调试到UntypedObjectDeserializer.deserializeWithType()函数,其中会调用AsArrayTypeDeserializer.deserializeTypedFromAny()函数来解析我们数组形式的JSON内容:
继续往下调试,我们在看会调用BeanDeserializerBase.deserializeFromString()函数来反序列化字符串内容,它会返回一个调用createFromString()函数从字符串中创建的实例对象:
跟进去看StdValueInstantiator.createFromString()函数,此时_fromStringCreator变量为AnnotatedConstructor类实例,参数value值为http://127.0.0.1:8000/spel.xml
,接着就是调用AnnotatedConstructor.call1():
继续向下调试,this._fromStringCreator.call1(value);
这个函数,发现调用了Constructor.newInstance()方法来创建新的实例:
往下调试,会调用到ClassPathXmlApplicationContext类的构造函数,看到configLocations参数值为spel.xml文件所在的URL地址,由于refresh参数值为True,因此会调用到refresh()函数:
注意:前面调用newInstance()是新建我们的利用类org.springframework.context.support.ClassPathXmlApplicationContext的实例,但是我们看到并没有调用ClassPathXmlApplicationContext类相关的setter方法,这是因为该类本身就没有setter方法,但是拥有构造函数,因此Jackson反序列化的时候会自动调用ClassPathXmlApplicationContext类的构造函数。而这个点就是和之前的利用链的不同之处,该类的漏洞点出在自己的构造函数而非在setter方法中。
下面我们继续调试看看ClassPathXmlApplicationContext类的构造函数中是哪里存在有漏洞。
跟进refresh()函数,进行一系列refresh之前的准备操作后,发现调用了invokeBeanFactoryPostProcessors()函数,顾名思义,就是调用上下文中注册为beans的工厂处理器:
跟进invokeBeanFactoryPostProcessors()函数中调用了getBeanNamesForType()函数来获取Bean名类型:
跟进往下,进一步调用doGetBeanNamesForType()函数:
在doGetBeanNamesForType()函数中,调用isFactoryBean()判断当前beanName是否为FactoryBean,此时beanName参数值为”pb”,mbd参数中识别到bean标签中的类为java.lang.ProcessBuilder:
在isFactoryBean()函数中,调用predictBeanType()函数获取Bean类型:
跟进predictBeanType函数,通过调用determineTargetType()函数来预测Bean类型:
跟进去,determineTargetType()函数中通过调用getTargetType()函数来确定目标类型:
跟下去,AbstractBeanFactory.resolveBeanClass()->AbstractBeanFactory.doResolveBeanClass(),用来解析Bean类,其中调用了evaluateBeanDefinitionString()函数来执行Bean定义的字符串内容,此时className参数指向”java.lang.ProcessBuilder”:
跟进AbstractBeanFactory.evaluateBeanDefinitionString()函数,其中调用了this.beanExpressionResolver.evaluate(),此时this.beanExpressionResolver指向的是StandardBeanExpressionResolver,也就是说已经调用到对应的SpEL表达式解析器了:
跟进StandardBeanExpressionResolver.evaluate()函数,发现调用了Expression.getValue()方法即SpEL表达式执行的方法,其中sec参数是我们可以控制的内容即由spel.xml解析得到的SpEL表达式:
至此,整个调用过程就大致过了遍。简单地说,就是传入的需要被反序列化的org.springframework.context.support.ClassPathXmlApplicationContext类,它的构造函数存在SpEL注入漏洞,进而导致可被利用来触发Jackson反序列化漏洞。
补丁分析
看一下换成jackson-databind-2.7.9.2版本的jar试试,会报错,显示由于安全原因禁止了该非法类的反序列化操作:
com.fasterxml.jackson.databind.JsonMappingException: Illegal type (org.springframework.context.support.ClassPathXmlApplicationContext) to deserialize: prevented for security reasons
在jackson-databind-2.7.9.2-sources.jar!\com\fasterxml\jackson\databind\jsontype\impl\SubTypeValidator.java中可以看到具体的黑名单信息,很遗憾的是没看到我们的利用类:
1 | static { |
那么如何修补的呢?调试看看。
在调用BeanDeserializerFactory.createBeanDeserializer()时,其中会调用_validateSubType()函数对子类型进行校验:
在SubTypeValidator._validateSubType()函数中看到,先进行黑名单过滤,发现类名不在黑名单后再判断是否是以”org.springframe”开头的类名,是的话循环遍历目标类的父类是否为”AbstractPointcutAdvisor”或”AbstractApplicationContext”,是的话跳出循环然后抛出异常:
而我们的利用类其继承关系是这样的:
…->AbstractApplicationContext->AbstractRefreshableApplicationContext->AbstractRefreshableConfigApplicationContext->AbstractXmlApplicationContext->ClassPathXmlApplicationContext
可以看到,ClassPathXmlApplicationContext类是继承自AbstractApplicationContext类的,而该类会被过滤掉,从而没办法成功绕过利用。