Jackson反序列化漏洞研究
一、Jackson序列化库使用简介
0x1:Jackson背景
Jackson是一个强大而高效的Java库,处理Java对象及其JSON表示的序列化和反序列化。它是这项任务中使用最广泛的库之一,并在许多其他框架中作为默认的Json引擎使用。例如,虽然Spring框架支持各种序列化/反序列化库,但Jackson是默认引擎。
Jackson 功能很强大,既能满足简单的序列化和反序列化操作,也能实现复杂的、个性化的序列化和反序列化操作。到目前为止,Jackson 的序列化和反序列化性能都非常优秀,已经是国内外大部分 JSON 相关编程的首选工具。
Jackson从 2.0 开始改用新的包名 fasterxml,1.x 版本的包名是 codehaus。除了包名不同,他们的 Maven artifact id 也不同。1.x 版本现在只提供 bug-fix,而 2.x 版本还在不断开发和发布中。如果是新项目,建议直接用 2x,即 fasterxml jackson。
0x2:Jackson操作
1、ObjectMapper类
Jackson库中用于读写JSON的主要类是ObjectMapper,它在com.fasterxml.jackson.databind 包中,可以序列化和反序列化两种类型的对象。
- 普通的旧Java对象(POJO)
- 通用的JSON树模型
ObjectMapper 类提供了四个构造函数来创建一个实例,
2、将JSON转换为Java对象
最简单的输入形式是String,或者说,JSON格式的字符串。
考虑一个健康管理系统中的以下 HealthWorker 类。
package com.example.jackson_test; public class HealthWorker { private int id; private String name; private String qualification; private Double yearsOfExperience; // Constructor, getters, setters, toString() public void setId(int id) { this.id = id; } public int getId() { return id; } public void setName(String name) { this.name = name; } public String getName() { return name; } public void setQualification(String qualification) { this.qualification = qualification; } public String getQualification() { return qualification; } public void setYearsOfExperience(Double yearsOfExperience) { this.yearsOfExperience = yearsOfExperience; } public Double getYearsOfExperience() { return yearsOfExperience; } }
要把这个类的JSON字符串表示法转换成一个Java类,我们只需把这个字符串提供给readValue() 方法,同时提供我们要转换的类的.class 。
package com.example.jackson_test; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class JacksonTestApplication { public static void main(String[] args) throws JsonProcessingException { //SpringApplication.run(JacksonTestApplication.class, args); ObjectMapper objectMapper = new ObjectMapper(); String healthWorkerJSON = "{\"id\":1,\"name\":\"RehamMuzzamil\",\"qualification\":\"MBBS\",\"yearsOfExperience\":1.5}"; HealthWorker healthWorker = objectMapper.readValue(healthWorkerJSON, HealthWorker.class); System.out.println(healthWorker.getId()); System.out.println(healthWorker.getName()); System.out.println(healthWorker.getQualification()); System.out.println(healthWorker.getYearsOfExperience()); } }
注意:字段名必须与JSON字符串中的字段完全匹配,否则映射器会抛出一个错误。此外,它们必须有有效的公共getters和setters。Jackson还支持使用不同名称的别名,可以通过简单的注释将任何JSON字段映射到任何POJO字段。
3、Jackson常用注解
Jackson包含了很多注解,用来个性化序列化和反序列化操作,主要包含如下注解。
- @JsonProperty,作用在属性上,用来为 JSON Key 指定一个别名;
- @JsonIgnore,作用在属性上,用来忽略此属性;
- @JsonIgnoreProperties,忽略一组属性,作用在类上,比如 @JsonIgnorePropertiess({"id","phone"});
- @JsonAnySetter,标记在某个方法上,此方法接收 Key、Value,用于 Jackson 在反序列化过程中,未找到的对应属性都调用此方法。通常这个方法用一个 Map 来实现;
- @JsonAnyGetter,此注解标注在一个返回 Map 的方法上,Jackson 会取出 Map中的每一个值进行序列化;
- @JsonFormat,用于日期的格式化
- @JsonNaming,用于指定一个命名策略,作用于类或者属性上,类似 @JsonProperty,但是会自动命名。Jackson 自带了多种命名策略,你也可以实现自己的命名策略,比如输入的 Key 由 Java 命名方式转换为下划线命名方式:userName 转换为 user-name;
- @JsonSerialize,指定一个实现类来自定义序列化。类必须实现 JsonSerializer 接口;
- @JsonDeserialize,用户自定义反序列化,同 @JsonSerialize,类需要实现 JsonDeserialize 接口;
- @JsonView,作用在类或者属性上,用来定义一个序列化组。Spring MVC 的 Controller 方法可以使用同样的 @JsonView 来序列化属于这一组的配置。
4、@JsonAlias 和 @JsonProperty
每当JSON字符串和POJO中的属性/字段名称不匹配时,你可以通过不反序列化它们,或 "调整 "哪些JSON字段被映射到哪些对象字段来处理这种不匹配。
这可以通过@JsonAlias 和@JsonProperty 来实现。
- @JsonProperty对应于序列化和反序列化过程中的字段名
- @JsonAlias对应于反序列化过程中的替代名称
例如,一个常见的不匹配发生在大写字母的约定上:一个API可能会返回snake_case,而你期待的是CamelCase。
package com.example.jackson_test; public class HealthWorker { private int id; private String name; private String qualification; private Double yearsOfExperience; // Constructor, getters, setters, toString() public void setId(int id) { this.id = id; } public int getId() { return id; } public void setName(String name) { this.name = name; } public String getName() { return name; } public void setQualification(String qualification) { this.qualification = qualification; } public String getQualification() { return qualification; } public void setYearsOfExperience(Double yearsOfExperience) { this.yearsOfExperience = yearsOfExperience; } public Double getYearsOfExperience() { return yearsOfExperience; } }
而传入的JSON看起来像这样,
{ "id" : 1, "name" : "RehamMuzzamil", "qualification" : "MBBS", "years_of_experience" :1.5 }
上面JSON中,“years_of_experience”是不被认可的字段,尽管它们显然代表相同的属性。
我们可以通过设置 @JsonProperty 注解来避免这种情况。
此外,你可以同时使用这两个注解,通过设置 @JsonAlias,除了.class中声明的属性名之外,别名列表中的别名也同样可以被传入并接受。
package com.example.jackson_test; import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; public class HealthWorker { private int id; private String name; private String qualification; @JsonProperty("years_of_experience") @JsonAlias({"yoe", "yearsOfExperience", "experience"}) private Double yearsOfExperience; // Constructor, getters, setters, toString() public void setId(int id) { this.id = id; } public int getId() { return id; } public void setName(String name) { this.name = name; } public String getName() { return name; } public void setQualification(String qualification) { this.qualification = qualification; } public String getQualification() { return qualification; } public void setYearsOfExperience(Double yearsOfExperience) { this.yearsOfExperience = yearsOfExperience; } public Double getYearsOfExperience() { return yearsOfExperience; } }
5、从HTTP响应/URL将JSON转换为Java对象(POJO)
JSON被创建为一种数据交换格式,特别是用于网络应用,它是网络上最普遍的数据序列化格式。
虽然你可以检索结果,将其保存为一个字符串,然后使用readValue() 方法进行转换,但同时你也可以直接读取HTTP响应,给定一个URL,并将其反序列化为所需的类。通过这种方法,你可以跳过中间的String,直接解析HTTP请求的结果。
String API_KEY = "552xxxxxxxxxxxxxxxxx122&"; String URLString = "http://api.weatherapi.com/v1/astronomy.json?key="+API_KEY+"q=London&dt=2021-12-30\n"; URL url = new URL(URLString); // Create a URL object, don't just use a URL as a String ObjectMapper objectMapper = new ObjectMapper(); Astronomy astronomy = objectMapper.readValue(url, Astronomy.class);
6、实现一个自定义的Jackson序列化器
创建一个实现JsonSerializer接口的自定义序列化器类。该接口有两个泛型参数,第一个参数是要序列化的类型,第二个参数是要序列化为的JSON类型。
在自定义序列化器类中实现serialize方法。在该方法中,您可以编写与输入对象的序列化逻辑。使用JsonGenerator参数可以将属性写入JSON输出。
package com.example.jackson_test; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import java.io.IOException; public class CustomSerializer extends JsonSerializer<HealthWorker> { @Override public void serialize(HealthWorker value, JsonGenerator gen, SerializerProvider serializers) throws IOException { // 在此处实现您的序列化逻辑 gen.writeString(value.toString()); } }
注册自定义序列化器。您需要将自定义序列化器注册到ObjectMapper实例中,以便在序列化对象时使用它。
package com.example.jackson_test; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import org.springframework.boot.autoconfigure.SpringBootApplication; import java.io.IOException; @SpringBootApplication public class JacksonTestApplication { public static void main(String[] args) throws IOException { ObjectMapper objectMapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.addSerializer(HealthWorker.class, new CustomSerializer()); objectMapper.registerModule(module); HealthWorker obj = new HealthWorker(); String json = objectMapper.writeValueAsString(obj); System.out.println(json); } }
用于存储反序列化的Java定义如下,
package com.example.jackson_test; import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; public class HealthWorker { private int id; private String name; private String qualification; @JsonProperty("years_of_experience") @JsonAlias({"yoe", "yearsOfExperience", "experience"}) private Double yearsOfExperience; // Constructor, getters, setters, toString() public void setId(int id) { this.id = id; } public int getId() { return id; } public void setName(String name) { this.name = name; } public String getName() { return name; } public void setQualification(String qualification) { this.qualification = qualification; } public String getQualification() { return qualification; } public void setYearsOfExperience(Double yearsOfExperience) { this.yearsOfExperience = yearsOfExperience; } public Double getYearsOfExperience() { return yearsOfExperience; } }
0x3:在Spring项目中使用Jackson
使用Spring Initializr通过GUI创建一个骨架项目。Jackson不是一个内置的依赖项,所以你不能通过CLI或Spring Initializr将其包括在内,不过,引入Jackson就像修改你的pom.xml 文件一样容易。
<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>
Jackson提供了ObjectMapper.writeValueAsString()和ObjectMapper.readValue()两个方法来实现序列化和反序列化的功能。
User.java
package com.example.jackson_test; public class User { private String username; private String password; public User() { } public User(String username, String password) { this.username = username; this.password = password; } public String getUsername() { return username; } public String getPassword() { return password; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } }
JacksonTest.java
package com.example.jackson_test; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; public class JacksonTest { public static void main(String[] args) throws IOException { User user = new User("Sentiment","123456"); ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(user); System.out.println(json); User other = mapper.readValue(json,User.class); System.out.println(other); } }
另一个例子。
package com.example.jackson_test; class Person { public int age; public String name; @Override public String toString() { return String.format("Person.age=%d, Person.name=%s", age, name); } }
主程序,
package com.example.jackson_test; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.boot.autoconfigure.SpringBootApplication; import java.io.IOException; @SpringBootApplication public class JacksonTestApplication { public static void main(String[] args) throws IOException { Person p = new Person(); p.age = 10; p.name = "Alice"; ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(p); System.out.println(json); Person p2 = mapper.readValue(json, Person.class); System.out.println(p2); } }
Jackson的基础写法,是不存在漏洞的,但是Json规范里有很多额外自定义的“特性”,这些“特性”会引入额外的解析规则,同时也引入了额外的漏洞。
参考链接:
https://www.cnblogs.com/cnjavahome/p/8393178.html https://juejin.cn/post/7114895114559815717
二、Jackson反序列化漏洞原理分析
0x1:反序列化中的多态问题
和fastjson情况类似一样,在实际场景中常常需要解决多态的问题,比如:Apple是苹果还是苹果手机等,在fastjson中引入了@type字段来解决该问题。
而在Jackson中也有与之对应的方法,Jackson实现了JacksonPolymorphicDeserialization机制来解决这个问题,有两种方法:
- DefaultTyping
- @JsonTypeInfo注解
1、通过DefaultTyping的配置解决多态问题
jackson提供一个设置,叫enableDefaultTyping,有4个值,在com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping能看到
- JAVA_LANG_OBJECT
- OBJECT_AND_NON_CONCRETE
- NON_CONCRETE_AND_ARRAYS
- NON_FINAL
默认情况下,即无参数的enableDefaultTyping是第二个设置,OBJECT_AND_NON_CONCRETE。
总结一下这4个类型设置的区别,
DefaultTyping类型 | 描述说明 |
---|---|
JAVA_LANG_OBJECT | 属性的类型为Object |
OBJECT_AND_NON_CONCRETE | 属性的类型为Object、Interface、AbstractClass |
NON_CONCRETE_AND_ARRAYS | 属性的类型为Object、Interface、AbstractClass、Array |
NON_FINAL | 所有除了声明为final之外的属性 |
1)JAVA_LANG_OBJECT
当类里的属性声明为一个Object时,会对该属性进行序列化和反序列化,并且明确规定类名。(当然,这个Object本身也得是一个可被序列化/反序列化的类)。
例如下面的代码,我们给Person里添加一个Object object。
Dna.java
package com.example.jackson_test; class Dna { public int length = 100; }
package com.example.jackson_test; class Person { public int age; public String name; public Object object; @Override public String toString() { return String.format("Person.age=%d, Person.name=%s, %s", age, name, object == null ? "null" : object); } }
主程序,
package com.example.jackson_test; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.boot.autoconfigure.SpringBootApplication; import java.io.IOException; @SpringBootApplication public class JacksonTestApplication { public static void main(String[] args) throws IOException { Person p = new Person(); p.age = 10; p.name = "Alice"; p.object = new Dna(); ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT); String json = mapper.writeValueAsString(p); System.out.println(json); Person p2 = mapper.readValue(json, Person.class); System.out.println(p2); } }
可以看到,当开启了EnableDefaultTyping之后,Json序列化后多出来了一些关于com.example.jackson_test.Dna的信息。将来进行反序列化的时候会进行相应的类重建还原。
2)OBJECT_AND_NON_CONCRETE
当类里有Interface 、AbstractClass时,需要对其进行序列化和反序列化,可以指定OBJECT_AND_NON_CONCRETE。
package com.example.jackson_test; interface Sex { public void setSex(int sex); public int getSex(); }
package com.example.jackson_test; class MySex implements Sex { int sex; @Override public int getSex() { return sex; } @Override public void setSex(int sex) { this.sex = sex; } }
package com.example.jackson_test; class Dna { public int length = 100; }
package com.example.jackson_test; class Person { public int age; public String name; public Object object; public Sex sex; @Override public String toString() { return String.format("Person.age=%d, Person.name=%s, %s, %s", age, name, object == null ? "null" : object, sex == null ? "null" : sex); } }
主程序,
package com.example.jackson_test; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.boot.autoconfigure.SpringBootApplication; import java.io.IOException; @SpringBootApplication public class JacksonTestApplication { public static void main(String[] args) throws IOException { Person p = new Person(); p.age = 10; p.name = "Alice"; p.object = new Dna(); p.sex = new MySex(); ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE); String json = mapper.writeValueAsString(p); System.out.println(json); Person p2 = mapper.readValue(json, Person.class); System.out.println(p2); } }
3)NON_CONCRETE_AND_ARRAYS
除了上文提到的特征,还支持上文全部类型的Array类型。
4)NON_FINAL
包括上文提到的所有特征,而且包含即将被序列化的类里的全部、非final的属性,也就是相当于整个类、除final外的的属性信息都需要被序列化和反序列化。
package com.example.jackson_test; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.boot.autoconfigure.SpringBootApplication; import java.io.IOException; @SpringBootApplication public class JacksonTestApplication { public static void main(String[] args) throws IOException { Person p = new Person(); p.age = 10; p.name = "Alice"; p.object = new Dna(); p.sex = new MySex(); p.dna = new Dna(); ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); String json = mapper.writeValueAsString(p); System.out.println(json); Person p2 = mapper.readValue(json, Person.class); System.out.println(p2); } }
2、通过@JsonTypeInfo注解解决多态问题
@JsonTypeInfo注解是Jackson多态类型绑定的一种方式,支持下面5种类型的取值:
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) @JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME) @JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM)
0x2:序列化的基本逻辑
package com.example.jackson_test; public class User { private String username; private String password; private Object object; public User() { } public User(String username, String password, Object object) { this.username = username; this.password = password; this.object = object; } public String getUsername() { return username; } public String getPassword() { return password; } public Object getObject() { return object; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setObject(Object object) { this.object = object; } }
package com.example.jackson_test; public class User2 { public String name="Tana"; }
package com.example.jackson_test; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; public class JacksonTest { public static void main(String[] args) throws IOException { User user = new User("Sentiment","123456", new User2()); ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); String json = mapper.writeValueAsString(user); System.out.println(json); User other = mapper.readValue(json,User.class); System.out.println(other); } }
常规情况下,Jackson在序列化时候,要求field可以访问到,先走getter,如果没有的话就去找field,如果还没有,就跳过这个field。
- 在没有开启defaultTyping的情况下,所有的field都要是基本数据类型、数组、集合类型和由常见类型组成的类等常见的数据,一旦属性里包含了非基本数据类型,就直接抛异常了。
- 在开启了 dafaultTyping的情况下,同样会对所有field都做一遍序列化。但区别在于,对不常见的类型反序列化时,需要主动指明类名,所以结构会不一样。
也就是说,开启和关闭 defaultTyping 二者是相互无法处理对方的字符串的,因为协议不一样,就拿上面这个例子,开启前后序列化出来的内容如下:
// 关闭 defaultTyping {"username":"Sentiment","password":"123456","object":{"name":"Tana"}} com.example.jackson_test.User@15d0c81b // 开启 defaultTyping ["com.example.jackson_test.User",{"username":"Sentiment","password":"123456","object":["com.example.jackson_test.User2",{"name":"Tana"}]}] com.example.jackson_test.User@1a1d6a08
换句话说,在关闭defaultTyping情况,因为只能序列化和反序列化常规的基本数据类型,所以根本不存在Java对象注入攻击面,而当开启了defaultTyping后,Jackson理论上就打开了一个“Java任意对象反序列化注入”的攻击面。
0x3:反序列化的基本逻辑
首先对于不开启defaultTyping的,根据json里的key-value中的key,去找对应变量的setter,找不到的话就找对应变量,还找不到的话,如果没有设置 ignore unknown,就抛异常了。因为控制的都是基本类型,所以攻击者无法发动攻击,这是一种保守、安全的配置。
但是如果开启了defaultTyping,就不一样了,这里存在两种攻击面,
- 第一种格式是类名后面加一堆key-value,去反序列化指定的类
- 第二种是类名后面跟一个参数,但必须是基本类型,官方解释是方便使用,一般推荐String,这样就可以从String反序列化这个类了
接下来我们通过debug调试源码,深入了解一下jackson反序列的过程。代码和上一节一致。
该流程分析是对使用DefaultTyping方法的分析,但是用@JsonTypeInfo跟这个是一模一样的。
跟fastjson类似,反序列化时会调用对应类的setter和无参构造器,而由于object属性是User2类型的,这里会分别调用User类和User2类的无参构造,
根据一级级调用,到了vanillaDeserialize()中,先调用createUsingDefault()函数来调用指定类的无参构造函数来生成类实例:
跟进,又调用了__call()方法,
跟进call(),通过newInstance进行了实例化,也就是将User类进行了实例化,
继续跟进,因为User被实例化了所以调用了User类的无参构造,
接着回到vanillaDeserialize(),生成实例Bean后,就开始进入do while循环来循环解析键值对中的属性值并调用deserializeAndSet()函数来解析并设置Bean的属性值:
跟进后先调用了deserialize(),
继续跟进,这里if会判断,反序列化内容是否携带类型,这里第一个属性是String类型所以调用到了第二个分支里,
跟进deserialize()后,嗲用了p.getText(),获取到了p的属性值。
最后通过invoke命令调用到了username的setter方法。
password属性也是String类型的,所以通过前边的do while循环,在调用deserializeAndSet();获取对应的值,跟username过程完全一样。
继续跟踪User2类的反序列化。
获取到password的值后,接着进入下一轮循环调用deserializeAndSet(),接着调用deserialize()判断反序列化数据的携带类型,由于User类中的object属性值是user2类的实例化,所以它是一个数组类型调用到了deserializeWithType()这里,
跟进后又调用了deserializeTypedFromAny(),又调用_deserialize(),跟进后获取了deser的类型为BeanDeserializer,接着调用了该类的deserialize()。
跟进后获取了deser的类型为BeanDeserializer,接着调用了该类的deserialize()。
接着就回到了流程分析最开始的部分,调用vanillaDeserialize(),
之后就是分别调用User2的构造器或Setter方法。
0x4:反序列化中存在的漏洞攻击面
1、前提条件
满足下面三个条件之一即存在Jackson反序列化漏洞:
- 调用了ObjectMapper.enableDefaultTyping()函数
- 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.CLASS的@JsonTypeInfo注解
- 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.MINIMAL_CLASS的@JsonTypeInfo注解
2、漏洞场景
1)属性不为Object类时
当要进行反序列化的类的属性所属类的构造函数或setter方法本身存在漏洞时,这种场景存在Jackson反序列化漏洞。当然这种场景开发几乎不会这么写,除非程序员自己想留后门。
2)属性为Object类时
当属性类型为Object时,因为Object类型是任意类型的父类,因此扩大了我们的攻击面,我们只需要寻找出在目标服务端环境中存在的且构造函数或setter方法存在漏洞代码的类即可进行攻击利用。后面出现的Jackson反序列化的CVE漏洞、黑名单绕过等都是基于这个原理寻找各种符合条件的利用链。
这里我们假设目标服务端环境中存在其一个恶意类User2,其setter方法存在任意代码执行漏洞,存在于com.example.jackson_test包中。
User.java
package com.example.jackson_test; public class User { private String username; private String password; private Object object; public User() { } public User(String username, String password, Object object) { this.username = username; this.password = password; this.object = object; } public String getUsername() { return username; } public String getPassword() { return password; } public Object getObject() { return object; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setObject(Object object) { this.object = object; } }
User2.java
package com.example.jackson_test; public class User2 { public String name="Tana"; public User2() { System.out.println("User2无参构造"); } public void setName(String name) { System.out.println("User2.setName"); this.name = name; try { Runtime.getRuntime().exec("open -a Calculator"); } catch (Exception e) { e.printStackTrace(); } } }
package com.example.jackson_test; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; public class JacksonTest { public static void main(String[] args) throws IOException { User user = new User("Sentiment","123456", new User2()); ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); String json = mapper.writeValueAsString(user); System.out.println(json); User other = mapper.readValue(json,User.class); System.out.println(other); } }
总结一下:在Jackson反序列化中,若调用了enableDefaultTyping()函数或使用@JsonTypeInfo注解指定反序列化得到的类的属性为JsonTypeInfo.Id.CLASS或JsonTypeInfo.Id.MINIMAL_CLASS,则会调用该属性的类的构造函数和setter方法。
参考链接:
https://blog.csdn.net/caiqiiqi/article/details/100930192 https://xz.aliyun.com/t/12628 https://www.leadroyal.cn/p/630/ https://www.leadroyal.cn/p/594/ http://www.mi1k7ea.com/2019/11/13/Jackson%E7%B3%BB%E5%88%97%E4%B8%80%E2%80%94%E2%80%94%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/ https://github.com/JoyChou93/java-sec-code
三、真实CVE漏洞案例分析
0x1:CVE-2017-7525(基于TemplatesImpl利用链)
0x2:CVE-2017-17485(基于ClassPathXmlApplicationContext利用链)
0x3:CVE-2019-12086(基于MiniAdmin利用链)
0x4:CVE-2019-12814(基于JDOM XSLTransformer利用链)
参考链接:
http://www.mi1k7ea.com/2019/11/16/Jackson%E7%B3%BB%E5%88%97%E4%BA%8C%E2%80%94%E2%80%94CVE-2017-7525%EF%BC%88%E5%9F%BA%E4%BA%8ETemplatesImpl%E5%88%A9%E7%94%A8%E9%93%BE%EF%BC%89/ http://www.mi1k7ea.com/2019/11/17/Jackson%E7%B3%BB%E5%88%97%E4%B8%89%E2%80%94CVE-2017-1748%EF%BC%88%E5%9F%BA%E4%BA%8EClassPathXmlApplicationContext%E5%88%A9%E7%94%A8%E9%93%BE%EF%BC%89/ http://www.mi1k7ea.com/2019/11/19/Jackson%E7%B3%BB%E5%88%97%E5%9B%9B%E2%80%94%E2%80%94CVE-2019-12086%EF%BC%88%E5%9F%BA%E4%BA%8EMiniAdmin%E5%88%A9%E7%94%A8%E9%93%BE%EF%BC%89/ http://www.mi1k7ea.com/2019/11/22/Jackson%E7%B3%BB%E5%88%97%E4%BA%94%E2%80%94%E2%80%94CVE-2019-12384%EF%BC%88%E5%9F%BA%E4%BA%8Elogback%E5%88%A9%E7%94%A8%E9%93%BE%EF%BC%89/ http://www.mi1k7ea.com/2019/11/24/Jackson%E7%B3%BB%E5%88%97%E5%85%AD%E2%80%94%E2%80%94CVE-2019-12814%EF%BC%88%E5%9F%BA%E4%BA%8EJDOM-XSLTransformer%E5%88%A9%E7%94%A8%E9%93%BE%EF%BC%89/ http://www.mi1k7ea.com/2019/11/24/Jackson%E7%B3%BB%E5%88%97%E4%B8%83%E2%80%94%E2%80%94%E5%85%B6%E4%BB%96Gadgets/
四、防御和检测方式
0x1:白盒检测方法
第一个特征是关于EnableDefaultTyping 的,注意他的多个重载方法,共有4个。
ObjectMapper enableDefaultTyping()
ObjectMapper enableDefaultTyping(DefaultTyping dti)
ObjectMapper enableDefaultTyping(DefaultTyping applicability, JsonTypeInfo.As includeAs)
ObjectMapper enableDefaultTypingAsProperty(DefaultTyping applicability, String propertyName)
只要匹配到,就算命中了条件A。
第二个特征是在 readValue 时候,指定的类本身是Object或者里面一定要包含Object类型字段或者Object类型的setter。
public <T> T readValue(String content, JavaType valueType) public <T> T readValue(Reader src, Class<T> valueType) public <T> T readValue(Reader src, TypeReference valueTypeRef) public <T> T readValue(Reader src, JavaType valueType)
所以只要匹配到这个,就需要对第二个参数进行解析,确认这个Model是否是可以被攻击的Model。如果包含了Object或者本身就是个Object,就认为命中了条件B。
第三个特征是被反序列的类里面,有被 JsonTypeInfo 注解过的类,而且里面的内容是 JsonTypeInfo.Id.CLASS 或 JsonTypeInfo.Id.MINIMAL_CLASS 。记做条件C。
最终条件就是:
(A&&B) || C
0x2:防御方法
将对应的gadget添加到黑名单,一旦反序列化到黑名单的类,就直接终止,抛异常,防止发生危害。
参考链接:
https://github.com/shadowsock5/jackson-databind-POC/blob/master/Jackson.java https://www.leadroyal.cn/p/633/ https://github.com/shadowsock5/jackson-databind-POC#%E7%AC%AC%E4%BA%8C%E4%B8%AA%E7%89%B9%E5%BE%81%E6%98%AF%E5%9C%A8-readvalue-%E6%97%B6%E5%80%99%E6%8C%87%E5%AE%9A%E7%9A%84%E7%B1%BB%E6%9C%AC%E8%BA%AB%E6%98%AFobject%E6%88%96%E8%80%85%E9%87%8C%E9%9D%A2%E4%B8%80%E5%AE%9A%E8%A6%81%E5%8C%85%E5%90%ABobject%E7%B1%BB%E5%9E%8B%E5%AD%97%E6%AE%B5%E6%88%96%E8%80%85object%E7%B1%BB%E5%9E%8B%E7%9A%84setter