一、前言 在之前的buu月赛中,遇到了一道使用jackson依赖打内存马的题,由于并没有学习过jackson反序列化漏洞,这里对这个漏洞进行学习,以便于后面的题目复现。
二、关于jackson Java生态圈中有很多处理JSON和XML格式化的类库, 常见的解析器:Jsonlib,Gson,fastjson,Jackson。Jackson是其中比较著名的一个,可以将Java对象序列化为XML或JSON格式的字符串,以及将XML或JSON格式的字符串反序列化为Java对象。并且jacksun的使用比较简单,速度快,且不依赖除JDK外的其他库。
使用的依赖
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.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>
三、jackson序列化与反序列化 在jackson中提供了ObjectMapper.writeValueAsString()
和ObjectMapper.readValue()
两个方法来进行序列化和反序列化,通过ObjectMapper.writeValueAsString()
方法将java对象序列化为json格式数据,通过ObjectMapper.readValue()
方法将json格式数据反序列化成java对象。
下面我们通过一个demo来演示:
定义一个Product类:
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 public class Product { private String name; private double price; private String description; public Product(String name, double price, String description) { this.name = name; this.price = price; this.description = description; } public Product(){} // Getters and Setters for the class properties public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @Override public String toString() { return "Product{" + "name='" + name + '\'' + ", price=" + price + ", description='" + description + '\'' + '}'; } }
测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class test { public static void main(String[] args) throws Exception { Product product=new Product("苹果",10,"优质"); ObjectMapper objectMapper =new ObjectMapper(); String w = objectMapper.writeValueAsString(product); System.out.println("这里是序列化:"); System.out.println(w); Product r=objectMapper.readValue(w, Product.class); System.out.println("这里是反序列化"); System.out.println(r); } }
运行结果:
四、jackson中的多态问题 我们这里先了解一些java中的多态:
多态是同一个行为具有多个不同表现形式或形态的能力。
我们可以理解为同一个接口,使用不同的实例而执行不同操作。
这里以菜鸟教程的一个demo来更好的说明:
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 public class Test { public static void main(String[] args) { show(new Cat()); // 以 Cat 对象调用 show 方法 show(new Dog()); // 以 Dog 对象调用 show 方法 Animal a = new Cat(); // 向上转型 a.eat(); // 调用的是 Cat 的 eat Cat c = (Cat)a; // 向下转型 c.work(); // 调用的是 Cat 的 work } public static void show(Animal a) { a.eat(); // 类型判断 if (a instanceof Cat) { // 猫做的事情 Cat c = (Cat)a; c.work(); } else if (a instanceof Dog) { // 狗做的事情 Dog c = (Dog)a; c.work(); } } } abstract class Animal { abstract void eat(); } class Cat extends Animal { public void eat() { System.out.println("吃鱼"); } public void work() { System.out.println("抓老鼠"); } } class Dog extends Animal { public void eat() { System.out.println("吃骨头"); } public void work() { System.out.println("看家"); } }
可以看到我们在主函数中都调用了show方法,但是输出的结果却不一样
这也就是我们说的同一个接口,使用不同的实例而执行不同操作。
五、JacksonPolymorphicDeserialization机制解决
jackson`的多态问题 我们上面说了多态就是使用同一个接口,使用不同的实例实现不同的操作
那么现在就出现了一个问题,我们如果在对多态类的某一个子类实例在进行序列化后反序列化,那么反序列化出来的实例是我们想要的那个子类的实例吗,会不会反序列化出其他子类的实例。
然后jackson就是通过JacksonPolymorphicDeserialization
机制这个机制来解决这个问题的。
JacksonPolymorphicDeserialization
在反序列化某个类对象的过程中,如果这个类的对象不是具体的类型,例如object,接口或者抽象类,那么我们就可以在json字符串中指定其具体类型。这样jackson就可以生成具体类型的实例。
JacksonPolymorphicDeserialization
机制就是将具体的子类信息绑定在序列化的内容中,从而在后续进行反序列化的时候直接得到目标子类对象。
JacksonPolymorphicDeserialization
机制有两种方法来解决这个问题:DefaultTyping
和@JsonTypeInfo
注解。
DefaultTyping jackson提供一个enableDefaultTyping
设置
包含四个值
在默认情况下,也就是enableDefaultTyping
在无参数的情况下,默认选择OBJECT_AND_NON_CONCRETE
我们下面结合我们上面的demo详细分析一下:
JAVA_LANG_OBJECT 我们这里添加一个类:
1 2 3 public class hacker { public String skill = "hacker"; }
修改 Product类添加一个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 47 48 49 50 51 52 public class Product { private String name; private double price; private String description; public Object object; public Product(String name, double price, String description,Object object) { this.name = name; this.price = price; this.description = description; this.object=object; } public Product(){} // Getters and Setters for the class properties public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @Override public String toString() { return "Product{" + "name='" + name + '\'' + ", price=" + price + ", description='" + description + '\'' + ", object=" + object + '}'; } }
修改测试类,添加enableDefaultTyping()
并设置为JAVA_LANG_OBJECT:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class test { public static void main(String[] args) throws Exception { Product product=new Product("苹果",10,"优质",new hacker()); ObjectMapper objectMapper =new ObjectMapper(); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT); String w = objectMapper.writeValueAsString(product); System.out.println("这里是序列化:"); System.out.println(w); Product r=objectMapper.readValue(w, Product.class); System.out.println("这里是反序列化"); System.out.println(r); } }
运行测试:
1 2 3 4 这里是序列化: {"name":"苹果","price":10.0,"description":"优质","object":["blog.hacker",{"skill":"hacker"}]} 这里是反序列化 Product{name='苹果', price=10.0, description='优质', object=blog.hacker@5dfcfece}
注释掉enableDefaultTyping()
:
1 2 3 4 这里是序列化: {"name":"苹果","price":10.0,"description":"优质","object":{"skill":"hacker"}} 这里是反序列化 Product{name='苹果', price=10.0, description='优质', object={skill=hacker}}
通过上面是否添加enableDefaultTyping()
并设置JAVA_LANG_OBJECT
对比可以看到,添加后会在序列化的时候多输出hacke的类名,并且在反序列化的时候,添加了enableDefaultTyping()
的只会输出hacker类对象。那么这里就是说对Object同时进行了序列化和反序列化操作。
OBJECT_AND_NON_CONCRETE OBJECT_AND_NON_CONCRETE
除了JAVA_LANG_OBJECT
的可以对Object对象进行序列化和反序列化,当类里面有接口,抽象类的时候,对其进行序列化和反序列化。而且enableDefaultTyping()
默认无参数的情况下的设置就是这个选项。
我们这里通过实现一个接口来进行测试:
1 2 3 4 5 public interface productjie { public void setprice(int price); public int getprice(); }
写一个类继承这个接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class myproductjie implements productjie { int price; @Override public void setprice(int price) { this.price=price; } @Override public int getprice() { return price; } }
修改一下product类:
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 public class Product { private String name; private double price; private String description; public Object object; public productjie productjie; public Product(String name, double price, String description,Object object,productjie productjie) { this.name = name; this.price = price; this.description = description; this.object=object; this.productjie=productjie; } public Product(){} // Getters and Setters for the class properties public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @Override public String toString() { return "Product{" + "name='" + name + '\'' + ", price=" + price + ", description='" + description + '\'' + ", object=" + object + ", productjie=" + productjie + '}'; } }
修改测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class test { public static void main(String[] args) throws Exception { Product product=new Product("苹果",10,"优质",new hacker(),new myproductjie()); ObjectMapper objectMapper =new ObjectMapper(); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE); String w = objectMapper.writeValueAsString(product); System.out.println("这里是序列化:"); System.out.println(w); Product r=objectMapper.readValue(w, Product.class); System.out.println("这里是反序列化"); System.out.println(r); } }
运行结果:
1 2 3 4 这里是序列化: {"name":"苹果","price":10.0,"description":"优质","object":["blog.hacker",{"skill":"hacker"}],"productjie":["blog.myproductjie",{"price":0}]} 这里是反序列化 Product{name='苹果', price=10.0, description='优质', object=blog.hacker@6cc4c815, productjie=blog.myproductjie@3a82f6ef}
我们这里可以看到我们定义的接口属性被成功序列化和反序列化了
那我们如果还是用上面那个JAVA_LANG_OBJECT
设置值来测试一下:
我们可以看到我们替换为JAVA_LANG_OBJECT
的时候在进行反序列化的时候产生了报错。
抛出了一个异常这个异常的根本原因是 Jackson 在反序列化时遇到了一个抽象类型或接口类型,而无法确定如何实例化它,因为抽象类型不能直接实例化。
NON_CONCRETE_AND_ARRAYS NON_CONCRETE_AND_ARRAYS
除了上面的Object,接口,抽象类型可以被序列化和反序列化,还支持Array类型
这里直接将测试代码中的hacker对象修改为数组类型的就行l
修个测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class test { public static void main(String[] args) throws Exception { hacker[] hacker = new hacker[2]; hacker[0]=new hacker(); hacker[1]=new hacker(); Product product=new Product("苹果",10,"优质",hacker,new myproductjie()); ObjectMapper objectMapper =new ObjectMapper(); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS); String w = objectMapper.writeValueAsString(product); System.out.println("这里是序列化:"); System.out.println(w); Product r=objectMapper.readValue(w, Product.class); System.out.println("这里是反序列化"); System.out.println(r); } }
运行结果:
1 2 3 4 这里是序列化: {"name":"苹果","price":10.0,"description":"优质","object":["[Lblog.hacker;",[{"skill":"hacker"},{"skill":"hacker"}]],"productjie":["blog.myproductjie",{"price":0}]} 这里是反序列化 Product{name='苹果', price=10.0, description='优质', object=[Lblog.hacker;@20e2cbe0, productjie=blog.myproductjie@68be2bc2}
同样的我们将这个设置值换成前面的尝试运行:
这里更换为JAVA_LANG_OBJECT
测试
但是这里经过测试发现 OBJECT_AND_NON_CONCRETE
也可也正常序列化和反序列化数组
NON_FINAL NON_FINAL:除了前面的所有特征外,包含即将被序列化的类里的全部、非final的属性,也就是相当于整个类、除final外的属性信息都需要被序列化和反序列化。
这里就修改一些test代码的中的属性设置值就可以了
@JsonTypeInfo注解 @JsonTypeInfo
注解是 Jackson 库中的一个重要注解,用于在序列化和反序列化 JSON 数据时指定类型信息。它允许你为类的字段或属性添加类型信息,以便 Jackson 可以正确地识别和处理多态类型的数据。
使用 JsonTypeInfo.Id
枚举: 可以指定 use
属性为 JsonTypeInfo.Id
枚举值之一,以确定类型信息的使用方式。常见的枚举值包括:
JsonTypeInfo.Id.NONE
JsonTypeInfo.Id.CLASS
:使用类名作为类型信息。
JsonTypeInfo.Id.MINIMAL_CLASS
:使用类名的简短版本。
JsonTypeInfo.Id.NAME
:使用自定义的类型名称。
JsonTypeInfo.Id.CUSTOM
自定义处理器处理
JsonTypeInfo.Id.NONE 这里简化一下代码方便修改测试:
在object属性上面添加@JsonTypeInfo
注解,并指定为JsonTypeInfo.Id.NONE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import com.fasterxml.jackson.annotation.JsonTypeInfo; public class product { public int price; public String name; @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) public Object object; @Override public String toString() { return String.format("Person.age=%d, Person.name=%s, %s", price, name, object == null ? "null" : object); } }
test类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class test { public static void main(String[] args) throws IOException { Product product = new Product(); product.price=10; product.name="苹果"; product.object=new hacker(); ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(product); System.out.println(json); Product p2 = mapper.readValue(json, Product.class); System.out.println(p2); } }
运行结果:
1 2 {"price":10,"name":"苹果","object":{"skill":"hacker"}} Person.age=10, Person.name=苹果, {skill=hacker}
我们这里把注解给注释掉:
再次运行
可以看到两个结果是一样的,所以JsonTypeInfo.Id.NONE
时没有任何处理的,和没有使用注解的效果是一样的。
JsonTypeInfo.Id.CLASS 修改注解的值为JsonTypeInfo.Id.CLASS
1 2 {"price":10,"name":"苹果","object":{"@class":"blog.hacker","skill":"hacker"}} Person.age=10, Person.name=苹果, blog.hacker@4b9e13df
可以看到与上面的输出相比多了"@class":"blog.hacker"
,包含了具体的类的信息。可以对指定的类进行序列化和反序列化,我们根据输出结果可以看到成功对object属性的hacker类进行了序列化和反序列化。
JsonTypeInfo.Id.MINIMAL_CLASS 1 2 {"price":10,"name":"苹果","object":{"@c":"blog.hacker","skill":"hacker"}} Person.age=10, Person.name=苹果, blog.hacker@475530b9
与上面的输出做对比可以看到基本产不多但是@class换成了@c,官方描述缩短了相关类名,但是真实效果和JsonTypeInfo.Id.CLASS
是一样的,能够成功对我们指定的类型进行序列化和反序列化。
JsonTypeInfo.Id.NAME
可以看到在序列化输出的json对象中多了一个@type,但是具体的包名,类名却没有,这导致在后面反序列化的时候抛出了异常,无法正常反序列化。那么根据这个结果我们可以知道这个值是不能在反序列化利用的。
JsonTypeInfo.Id.CUSTOM
这个值在进行序列化的时候就抛出了异常。因为这个值是提供给用户自定义的,没有办法直接使用,需要手动写一个解析器才能配合使用这个值,直接调用就会抛出异常。
那这里其实根据我们前面的测试,,当@JsonTypeInfo注解设置为如下值之一并且修饰的是Object类型的属性时,我们可以使用JsonTypeInfo.Id.CLASS
,JsonTypeInfo.Id.MINIMAL_CLASS
可以利用来触发jackson反序列化漏洞。
六、jackson反序列化中类属性方法的调用 在上面我们学习了jackson是如何通过JacksonPolymorphicDeserialization
机制下的多态的序列化和反序列化
然后我们这里来看一下jackson在JacksonPolymorphicDeserialization
机制下类属性方法的调用。
DefaultTyping场景下: 这里对Product代码稍微做一个修改,每个类方法属性都加上一个输出语句,便于我们判断是否调用
Product类:
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 package blog; public class Product { private String name; private double price; private String description; public Object object; public productjie productjie; public Product(String name, double price, String description,Object object,productjie productjie) { this.name = name; this.price = price; this.description = description; this.object=object; this.productjie=productjie; System.out.println("这里是有参构造"); } public Product(){ System.out.println("这里是无参构造"); } // Getters and Setters for the class properties public String getName() { System.out.println("这里是getname方法"); return name; } public void setName(String name) { this.name = name; System.out.println("这里调用了setname方法"); } public double getPrice() { System.out.println("这里调用了getprice方法"); return price; } public void setPrice(double price) { System.out.println("这里调用了setprice方法"); this.price = price; } public String getDescription() { System.out.println("这里调用了getdescripition方法"); return description; } public void setDescription(String description) { System.out.println("这里调用了setdescripition方法"); this.description = description; } @Override public String toString() { return "Product{" + "name='" + name + '\'' + ", price=" + price + ", description='" + description + '\'' + ", object=" + object + ", productjie=" + productjie + '}'; } }
测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import com.fasterxml.jackson.databind.ObjectMapper; public class test { public static void main(String[] args) throws Exception { hacker[] hacker = new hacker[2]; hacker[0]=new hacker(); hacker[1]=new hacker(); Product product=new Product("苹果",10,"优质",new hacker(),new myproductjie()); ObjectMapper objectMapper =new ObjectMapper(); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS); String w = objectMapper.writeValueAsString(product); System.out.println("这里是序列化:"); System.out.println(w); Product r=objectMapper.readValue(w, Product.class); System.out.println("这里是反序列化"); System.out.println(r); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 这里是有参构造 这里是getname方法 这里调用了getprice方法 这里调用了getdescripition方法 这里是序列化: {"name":"苹果","price":10.0,"description":"优质","object":["blog.hacker",{"skill":"hacker"}],"productjie":["blog.myproductjie",{"price":0}]} 这里是无参构造 这里调用了setname方法 这里调用了setprice方法 这里调用了setdescripition方法 这里是反序列化 Product{name='苹果', price=10.0, description='优质', object=blog.hacker@6cc4c815, productjie=blog.myproductjie@3a82f6ef}
根据上面运行结果的输出,我们可以看到在序列化的时候调用了构造函数和get方法
但是在反序列化的时候调用了构造方法和set方法
我们继续分析在使用注解的情况下:
@JsonTypeInfo注解场景下: 修改Product代码在productjie属性上面添加注解属性
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 package blog; import com.fasterxml.jackson.annotation.JsonTypeInfo; public class Product { private String name; private double price; private String description; public Object object; @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) public productjie productjie; public Product(String name, double price, String description,Object object,productjie productjie) { this.name = name; this.price = price; this.description = description; this.object=object; this.productjie=productjie; System.out.println("这里是有参构造"); } public Product(){ System.out.println("这里是无参构造"); } // Getters and Setters for the class properties public String getName() { System.out.println("这里是getname方法"); return name; } public void setName(String name) { this.name = name; System.out.println("这里调用了setname方法"); } public double getPrice() { System.out.println("这里调用了getprice方法"); return price; } public void setPrice(double price) { System.out.println("这里调用了setprice方法"); this.price = price; } public String getDescription() { System.out.println("这里调用了getdescripition方法"); return description; } public void setDescription(String description) { System.out.println("这里调用了setdescripition方法"); this.description = description; } @Override public String toString() { return "Product{" + "name='" + name + '\'' + ", price=" + price + ", description='" + description + '\'' + ", object=" + object + ", productjie=" + productjie + '}'; } }
测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class test { public static void main(String[] args) throws Exception { Product product=new Product("苹果",10,"优质",new hacker(),new myproductjie()); ObjectMapper objectMapper =new ObjectMapper(); String w = objectMapper.writeValueAsString(product); System.out.println("这里是序列化:"); System.out.println(w); Product r=objectMapper.readValue(w, Product.class); System.out.println("这里是反序列化"); System.out.println(r); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 这里是有参构造 这里是getname方法 这里调用了getprice方法 这里调用了getdescripition方法 这里是序列化: {"name":"苹果","price":10.0,"description":"优质","object":{"skill":"hacker"},"productjie":{"@class":"blog.myproductjie","price":0}} 这里是无参构造 这里调用了setname方法 这里调用了setprice方法 这里调用了setdescripition方法 这里是反序列化 Product{name='苹果', price=10.0, description='优质', object={skill=hacker}, productjie=blog.myproductjie@6767c1fc}
我们看到这里调用的类属性方法是和上面在DefaultTyping场景下的结果是一样的。
动态调试 那么根据上面的测试,jackson在进行反序列化的过程中首先是通过构造函数生成实列,然后调用调用set方法设置实例的属性值,我们这里打断点动态调试一下这个过程
前面一部分代码都是读取json数据对象
然后调用deserialize方法对json对象进行反序列化操作
我们这里跟进去分析一下:
首先通过p.isExpectedStartObjectToken()
检查是否是在处理一个json对象
然后调用vanillaDeserialize
进行处理
跟进这个方法
1 final Object bean = _valueInstantiator.createUsingDefault(ctxt);
首先这里是先通过createUsingDefault
这个方法来调用指定类的无参构造函数来生成类实例
那么到这里我们也就知道了无参构造函数在jackson反序列化的调用过程
跟进这个方法
这里调用了call方法
跟进
进入call方法我们可以看到调用了newInstace()方法对_constructor
进行实例化,这里的_constructor
我们根据调试信息可以看到就是我们定义的product类
那么根据上面的调试分析我们可以知道实例化的过程
然后跳出createUsingDefault方法进行向下分析代码
我们这里简单分析一些代码
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME))
: 这个条件检查 JSON 标记(token)是否是字段名。在 JSON 中,字段名是一个字符串,例如 "name": "John"
中的 "name"
就是字段名。
String propName = p.getCurrentName();
: 如果 JSON 标记是字段名,那么这一行代码会获取当前字段名,并将其存储在 propName
变量中。
然后可以看到是一个dowhile循环用于处理对象的各个属性。在每次迭代中,它会获取下一个字段名,并继续处理,直到没有更多字段名为止。
如果找到了与字段名匹配的属性,那么它会进入这个条件块。在这个块中,它尝试使用属性的 deserializeAndSet
方法来将 JSON 数据中的值设置到 Java 对象的属性上。
我们这里跟进到这个deserializeAndSet方法
可以看到这里也调用了deserialize方法
跟进到这个deserialize方法:
可以看到这里有两个处理逻辑
第一个逻辑用于处理 JSON 中属性值为 null 的情况,将其映射为 Java 对象的属性值为 null。
第二个if判断
首先,它检查 _valueTypeDeserializer
是否为非空,即是否存在类型信息。如果存在类型信息,说明该属性值包含了类型信息。
如果存在类型信息,它会调用 _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer)
来执行反序列化操作。这个方法传递了当前的 JSON 解析器 p
、反序列化上下文 ctxt
以及类型信息 _valueTypeDeserializer
。
如果 _valueTypeDeserializer
为空,说明属性值不包含类型信息,它将简单地调用 _valueDeserializer.deserialize(p, ctxt)
来执行属性值的反序列化。这将根据属性值的实际类型执行反序列化。
我们的第一个属性是name,所以可以看到这里调用的是StringDeserializerl来获取name属性的值
当获取到属性值后回到deserializeAndSet
方法调用属性的setter方法来对实例的属性值进行设置。
后面就还是进行dowhile设置 后面price和description的值。
但是hacker是一个包含类名的数组
可以看到这里的类型不为空,也就是_valueTypeDeserializer
的值不为空
然后这里调用了_valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
跟进去这个处理函数
一直跟进到调用_deserialize方法
1 tring typeId = _locateTypeId(p, ctxt);
在这里可以看到调用_locateTypeId()
函数获取到了类型为:blog.hacker
然后这里根据取到的类型_findDeserializer
方法来寻找对应的反序列化器
我们跟进去获取到了反序列化器并返回
这里其实就是获取到了反序列化器
我们这里跟进去看一下
我们这次处理的序列化对象Object是有类型信息的,所以这里会调用_valueDeserializer.deserializeWithType
进行处理
跟进到deserializeWithType
方法
这里就是jackson用于处理多态对象的地方,typeDeserializer
会检查JSON数据中的类型信息,然后选择正确的Java类进行反序列化。这允许在JSON中表示多态数据结构,以便将其准确地还原为Java对象。
我们继续跟进
然后继续调用 _deserialize进行反序列化处理
具体的反序列化还是调用这个deserialize方法,然后将反序列化的值返回
继续跟进到这个deserialize方法
可以看到又重新跳回了deserialize调用了vanillaDeserialize
方法来解析数组内的内容
其中调用createUsingDefault()函数的时候会调用到hakcer类的无参构造函数来新建hacker类对象
然后再次调用deserializeAndSet()函数获取该属性值并设置到该实例中:
然后通过反射获取到hacker类的属性名,然后调用set方法将值设置给hacker对象
因为我在product类里面也写了一个object的set方法
所以会在来一次调用这个set方法
那么其实上面对于object类型的反序列化首先通过deserializeWithType
方法去获取到合适的反序列化器,然后再次调用deserialize方法去进行json对象的反序列化,调用无参构造函数,然然后就是通过反射机制调用该属性的setter方法进行设置。
那么根据上面的分析,我们大概可以总结一下jackson的反序列化过程:先通过无参构造类函数生成目标类的实例,然后根据属性值是否是数组,也就是是否带有类名,如果目标是一个不带有类名的就直接调用deserializeAndSet方法调用set方法将属性值设置,如果是带有类名的,也就是类似于我们上面的数组类型,会先调用deserializeWithType去寻找合适的发序列化器,进行反序列化,然后再构造对象,调用deserializeAndSet方法。
七、jackson反序列化漏洞 那么根据我们上面的分析,在jackson的反序列化中,若是调用了enableDefaultTyping()
函数或使用@JsonTypeInfo
注解指定反序列化得到的类的属性为JsonTypeInfo.Id.CLASS
或JsonTypeInfo.Id.MINIMAL_CLASS
就会调用该属性类的构造函数和set方法。那么现在出现了一个问题,如果我们在构造函数或者set方法中写入一些危险的操作的方法或者函数,那么当反序列化调用构造函数和set方法时就会造成危险利用。这就是我们这次要学习的漏洞jackson反序列化漏洞。
根据上面的分析我们可以得到jackson反序列化的利用前提条件:(满足其一就可)
}
public interface productjie { public void setprice(int price) throws IOException; public int getprice(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 运行测试: ![image-20231029173820717](https://jublog.oss-cn-beijing.aliyuncs.com/image/image-20231029173820717.png) 可以看到成功弹出了计算器。 当然我们上面打的是一个非object类的,当我们的目标属性是object类型的,那么我们的攻击面就被扩大了,因为object类型是任意类型的父类,那只需要寻找出在目标服务端环境中存在的且构造函数或setter方法存在漏洞代码的类即可进行攻击利用。 那我们直接这里修改: hacker类:
package blog1;
public class hacker { public String cmd;
public hacker(){
} public hacker(String cmd){ this.cmd=cmd;
}
public void setCmd(String cmd){
this.cmd=cmd;
System.out.println("122222");
try {
Runtime.getRuntime().exec(this.cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
}
product类:
import com.fasterxml.jackson.annotation.JsonTypeInfo;
public class Product { public int price; public String name; @JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS) public Object object; public Product() { System.out.println(“Product构造函数”); }
@Override
public String toString() {
return String.format("Prouduct.age=%d, Product.name=%s, %s", price, name, object == null ? "null" : object);
}
}
测试类: public class test { public static void main(String[] args) throws IOException { Product product = new Product(); product.price=10; product.name=”苹果”; product.object=new hacker(“calc”); ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(product);
System.out.println(json);
Product p2 = mapper.readValue(json, Product.class);
System.out.println(p2);
}
}
运行测试:
![image-20231029183331719](https://jublog.oss-cn-beijing.aliyuncs.com/image/image-20231029183331719.png)
也弹出来计算器。
# 八、总结
上面我们分析和调试了jackson反序列化漏洞产生的原因,也就是因为jackson解决多态问题反序列化的JacksonPolymorphicDeserialization机制存在问题,当调用`enableDefaultTyping()`函数或使用`@JsonTypeInfo`注解指定反序列化得到的类的属性为`JsonTypeInfo.Id.CLASS`或`JsonTypeInfo.Id.MINIMAL_CLASS`就会调用该属性类的构造函数和set方法,导致我们可以反序列化利用。
但是我们上面的测试都是自己写了一个利用类,在真实环境中,是不可能留有一个可以执行命令的后门的,所以我们这里只是简单了一下jackson反序列化的原理和流程。后门我们将分析与其相关的几个cve和利用链。