之前有段时间在搞jackson反序列化漏洞的检测,网上看到各种各样的文章,很多抄来抄去的,真是辣鸡,虽然我不懂,但也知道他们有些人在胡扯。本系列文章系统地介绍java里的json反序列化漏洞成因、防御方式、检测方式、利用方式。因为本人接触这些不久,如有错误,还请大佬留言指正。
本文是第一篇,讲讲jackson的基础知识。
一、Jackson的基本用法 直接贴代码吧,很简单的int和String操作,序列化反序列化,非常舒服。
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 public class Hello { public static void main (String args[]) throws IOException { Person p = new Person(); p.age = 10 ; p.name = "Alice" ; ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(p); System.out.println(json); Person p2 = mapper.readValue(json, Person.class); System.out.println(p2); } } class Person { public int age; public String name; @Override public String toString () { return String.format("Person.age=%d, Person.name=%s" , age, name); } }
恩,看起来没有任何问题,这怎么会有漏洞呢?没错,这么写没有漏洞。。。
如果是我这样的玩家,使用基础的用法,不用骚操作,写上面的代码,是无法被攻击的。 但json规范里有很多额外自定义的东西,这些会引入额外的解析规则。
二、DefaultTyping的配置 jackson提供一个设置,叫enableDefaultTyping ,有4个值。
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 { JAVA_LANG_OBJECT, OBJECT_AND_NON_CONCRETE, NON_CONCRETE_AND_ARRAYS, NON_FINAL }
下文println的输出内容已经作为注释被写到了代码里。
1、JAVA_LANG_OBJECT JAVA_LANG_OBJECT 当类里的属性声明为一个Object时,会对该属性进行序列化和反序列化,并且明确规定类名。(当然,这个Object本身也得是一个可被序列化/反序列化的类)
例如下面的代码,我们给Person里添加一个Object object。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class Hello { public static void main (String args[]) throws IOException { Person p = new Person(); p.age = 10 ; p.name = "Alice" ; p.object = new Dna(); ObjectMapper mapper = new ObjectMapper(); 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); } } 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); } } class Dna { public int length = 100 ; }
神奇的是,输出的String和我们平时看到的json长得不一样了,额外多出来com.leadroyal.example.Dna
的信息。 这就是 EnableDefaultTyping ,在序列化的String里,包含类原本的一些信息,将来反的时候会进行还原。
2、OBJECT_AND_NON_CONCRETE OBJECT_AND_NON_CONCRETE 。除了上文 提到的特征,当类里有Interface 、AbstractClass 时,对其进行序列化和反序列化。(当然,这些类本身需要是合法的、可以被序列化/反序列化的对象)。
例如下面的代码,这次我们添加名为 Sex 的interface,发现它被正确序列化、反序列化了,就是这个选项控制的。
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 Hello { public static void main (String args[]) throws IOException { Person p = new Person(); p.age = 10 ; p.name = "Alice" ; p.object = new Dna(); p.sex = new MySex(); ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE); String json = mapper.writeValueAsString(p); System.out.println(json); Person p2 = mapper.readValue(json, Person.class); System.out.println(p2); } } 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); } } class Dna { public int length = 100 ; } class MySex implements Sex { int sex; @Override public int getSex () { return sex; } @Override public void setSex (int sex) { this .sex = sex; } } interface Sex { public void setSex (int sex) ; public int getSex () ; }
3、NON_CONCRETE_AND_ARRAYS NON_CONCRETE_AND_ARRAYS ,除了上文提到的特征,还支持上文全部类型的Array类型。
例如下面的代码,我们的Object里存放Dna的数组。
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 public class Hello { public static void main (String args[]) throws IOException { Person p = new Person(); p.age = 10 ; p.name = "Alice" ; Dna[] dnas = new Dna[2 ]; dnas[0 ] = new Dna(); dnas[1 ] = new Dna(); p.object = dnas; p.sex = new MySex(); ObjectMapper mapper = new ObjectMapper(); 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); } } 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); } } class Dna { public int length = 100 ; } class MySex implements Sex { int sex; @Override public int getSex () { return sex; } @Override public void setSex (int sex) { this .sex = sex; } } interface Sex { public void setSex (int sex) ; public int getSex () ; }
4、NON_FINAL NON_FINAL ,包括上文提到的所有特征,而且包含即将被序列化的类里的全部、非final的属性,也就是相当于整个类、除final外的的属性信息都需要被序列化和反序列化。
例如下面的代码,添加了类型为Dna的变量,非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 53 54 55 56 57 58 59 60 61 public class Hello { public static void main (String args[]) throws IOException { Person p = new Person(); p.age = 10 ; p.name = "Alice" ; p.object = new Dna(); p.sex = new MySex(); p.dna = new Dna(); ObjectMapper mapper = new ObjectMapper(); 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); } } class Person { public int age; public String name; public Object object; public Sex sex; public Dna dna; @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, dna == null ? "null" : dna); } } class Dna { public int length = 100 ; } class MySex implements Sex { int sex; @Override public int getSex () { return sex; } @Override public void setSex (int sex) { this .sex = sex; } } interface Sex { public void setSex (int sex) ; public int getSex () ; }
默认的、无参的enableDefaultTyping是第二个等级,OBJECT_AND_NON_CONCRETE。
总结一下,如果开启了这个功能,就可以指定被反序列化的那个类!
三、序列化时的逻辑 这里是简要的逻辑,并没有太认真地去阅读代码,各位读者将就着看,不一定对。 在序列化时候,要求field可以访问到,先走getter,如果没有的话就去找field,如果还没有,就跳过这个field。 在没有开启defaultTyping的情况下,所有的field都要是基本数据类型、数组、集合类型和由常见类型组成的类等常见的数据,一旦属性里包含了不常见的东西,就直接抛异常了。 例如这个是合法的,可以被序列化和反序列化的。
1 2 3 4 5 6 7 8 9 class Person { public int age; public String name; public Dna dna; } class Dna { public int length = 100 ; }
例如这个是非法的,因为包含了一个不常见的类型。
1 2 3 4 5 6 7 8 9 10 class Person { public int age; public String name; public Dna dna; public FileInputStream stream; } class Dna { public int length = 100 ; }
在开启了 dafaultTyping的情况下,同样会对所有field都做一遍序列化,但区别在于,对不常见的类型反序列化时,需要主动指明类名,所以结构会不一样 。也就是说,开启和关闭 defaultTyping 二者是相互无法解开对方的字符串 的,因为协议不一样。 就拿上面这个例子,开启前后序列化出来的内容如下:
1 2 3 4 5 6 7 { "age": 10, "name": "Alice", "dna": { "length": 100 } }
1 2 3 4 5 6 7 8 9 10 11 12 13 [ "com.leadroyal.example.Person", { "age": 10, "name": "Alice", "dna": [ "com.leadroyal.example.Dna", { "length": 100 } ] } ]
四、反序列化时的逻辑 这个代码我是读了很多次的,也调试过,可以稍微看下,但也有不准确的地方。
首先对于不开启defaultTyping的,根据json里的key-value中的key,去找对应变量的setter,找不到的话就找对应变量,还找不到的话,如果没有设置 ignore unknown,就抛异常了。因为控制的都是基本类型,所以攻击者无法发动攻击,是一种保守、安全的配置。
如果开启了defaultTyping,就不一样了,这里我们先讲两种格式,第一种格式是上面的,类名后面加一堆key-value,去反序列化指定的类;第二种是类名后面跟一个参数,但必须是基本类型,官方解释是方便使用,一般推荐String,这样就可以从String反序列化这个类了。
好,下面进入debug时间,首先样例代码就是上面那段,我们断点下在readValue上,去反序列化自动生成的那段。然后在构造方法、setter上面同样下断点,这样就可以看到栈回溯了。
第一次,断在了Person的构造函数上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <init>:31, Person (com.leadroyal.example) newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect) newInstance:62, NativeConstructorAccessorImpl (sun.reflect) newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect) newInstance:423, Constructor (java.lang.reflect) call:119, AnnotatedConstructor (com.fasterxml.jackson.databind.introspect) createUsingDefault:243, StdValueInstantiator (com.fasterxml.jackson.databind.deser.std) vanillaDeserialize:249, BeanDeserializer (com.fasterxml.jackson.databind.deser) deserialize:125, BeanDeserializer (com.fasterxml.jackson.databind.deser) _deserialize:110, AsArrayTypeDeserializer (com.fasterxml.jackson.databind.jsontype.impl) deserializeTypedFromObject:58, AsArrayTypeDeserializer (com.fasterxml.jackson.databind.jsontype.impl) deserializeWithType:1021, BeanDeserializerBase (com.fasterxml.jackson.databind.deser) deserialize:63, TypeWrappedDeserializer (com.fasterxml.jackson.databind.deser.impl) _readMapAndClose:3807, ObjectMapper (com.fasterxml.jackson.databind) readValue:2797, ObjectMapper (com.fasterxml.jackson.databind) main:20, Hello (com.leadroyal.example)
来三张图,大概就是通过反射,调用了构造函数,这我这个testcase下,只会调用默认的构造函数。但看代码里,似乎有多参数的构造和一个参数的构造也是可以。
点下继续,下一次断在了setAge上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 setAge:35, Person (com.leadroyal.example) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) deserializeAndSet:97, MethodProperty (com.fasterxml.jackson.databind.deser.impl) vanillaDeserialize:260, BeanDeserializer (com.fasterxml.jackson.databind.deser) deserialize:125, BeanDeserializer (com.fasterxml.jackson.databind.deser) _deserialize:110, AsArrayTypeDeserializer (com.fasterxml.jackson.databind.jsontype.impl) deserializeTypedFromObject:58, AsArrayTypeDeserializer (com.fasterxml.jackson.databind.jsontype.impl) deserializeWithType:1021, BeanDeserializerBase (com.fasterxml.jackson.databind.deser) deserialize:63, TypeWrappedDeserializer (com.fasterxml.jackson.databind.deser.impl) _readMapAndClose:3807, ObjectMapper (com.fasterxml.jackson.databind) readValue:2797, ObjectMapper (com.fasterxml.jackson.databind) main:20, Hello (com.leadroyal.example)
大概就是通过反射,找到了setter方法,然后去调用赋值。
之后setName和setDna这个一模一样,后者是复杂对象,也是经过了同样的、反序列化逻辑,是嵌套的。
在没有setter方法时,应该会尝试去主动赋值,例如我们把age的setter删掉,会走下图中的代码,使用反射去赋值。
由于篇幅有限,大概就是这个样子,还是自己调试时候感受比较深刻。
这时候注意一下,前面提到,jackson代码里是可以调用到仅带有一个参数的构造方法的,当初设计这个功能可能是为了使用带String的构造方法,从而生成该对象的,也可以用其他的、仅有一个参数但是是基本类型或者数组。
再这里对各个creator做了初始化,共有9个允许的构造方法。
在寻找完毕后,可以看到我们有3个允许的构造方法。
其中,无参的构造方法是这里初始化的,之后放入第一个格子里。
有参的在这里被初始化,根据参数类型去放到相应的位置。
之前提到参数个数为1,也是这里判定的,ctor就是那个creator,将来放到9个格子里中的某一个。
五、总结 没什么好总结的吧,基本操作,多构建一些poc测测就知道了。
请期待第二篇文章~~~~~~~