SnakeYaml反序列化

SnakeYaml反序列化

Yaml规范

yaml对象

对象键值对使用冒号结构表示 key: value,冒号后面要加一个空格。

也可以使用 key:{key1: value1, key2: value2, ...}

还可以使用缩进表示层级关系:

key: child-key: valuechild-key2: value2

yaml数组

以 - 开头的行表示构成一个数组:

- A
- B
- C

一个相对复杂的例子:

companies:-id: 1name: company1price: 200W-id: 2name: company2price: 500W

上面的例子中, companies是一个数组, 它有两个元素, 每个元素又有三个属性

数组也可以使用流式(flow)的方式表示:

companies: [{id: 1,name: company1,price: 200W},{id: 2,name: company2,price: 500W}]

这样看起来更直观

复合结构

数组和对象可以构成复合结构,例:

languages:- Ruby- Perl- Python 
websites:YAML: yaml.org Ruby: ruby-lang.org Python: python.org Perl: use.perl.org

纯量

纯量是最基本的,不可再分的值,包括:

  • 字符串
  • 布尔值
  • 整数
  • 浮点数
  • Null
  • 时间
  • 日期
    例子:
boolean: - TRUE  #true,True都可以- FALSE  #false,False都可以
float:- 3.14- 6.8523015e+5  #可以使用科学计数法
int:- 123- 0b1010_0111_0100_1010_1110    #二进制表示
null:nodeName: 'node'parent: ~  #使用~表示null
string:- 哈哈- 'Hello world'  #可以使用双引号或者单引号包裹特殊字符- newlinenewline2    #字符串可以拆成多行,每一行会被转化成一个空格
date:- 2018-02-17    #日期必须使用ISO 8601格式,即yyyy-MM-dd
datetime: -  2018-02-17T15:02:31+08:00    #时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区

引用

& 锚点和 * 别名,可以用来引用:

defaults: &defaultsadapter:  postgreshost:     localhostdevelopment:database: myapp_development<<: *defaultstest:database: myapp_test<<: *defaults

& 用来建立锚点(defaults),<< 表示合并到当前数据,* 用来引用锚点。

上面的例子相当于

defaults:adapter:  postgreshost:     localhostdevelopment:database: myapp_developmentadapter:  postgreshost:     localhosttest:database: myapp_testadapter:  postgreshost:     localhost

参考链接:

https://www.runoob.com/w3cnote/yaml-intro.html

SnakeYaml库使用

  • Yaml.load():将yaml数据反序列化成一个Java对象
  • Yaml.dump():将Java对象序列化成yaml

导入依赖

<dependency><groupId>org.yaml</groupId><artifactId>snakeyaml</artifactId><version>1.27</version>
</dependency>

被序列化的类
Person.java

package org.n4c1.unserial;  public class Person {  private String username;  private int age;  public Person() {}  public Person(String username, int age) {  this.username = username;  this.age = age;  }  public int getAge() {  System.out.println("getAge方法调用");  return age;  }  public String getUsername() {  System.out.println("getUsername方法调用");  return username;  }  public void setAge(int age) {  System.out.println("setAge方法调用");  this.age = age;  }  public void setUsername(String username) {  System.out.println("setUsername方法调用");  this.username = username;  }  
}

demo 测试序列化

package org.n4c1.unserial;  import org.yaml.snakeyaml.Yaml;  public class testYaml {  public static void main(String[] args) {  Yaml yaml = new Yaml();  Person person = new Person("n4c1", 20);  String str = yaml.dump(person);  System.out.println(str);  }  
}

输出结果

getAge方法调用
getUsername方法调用
!!org.n4c1.unserial.Person {age: 20, username: n4c1}

demo 测试反序列化

package org.n4c1.unserial;  import org.yaml.snakeyaml.Yaml;  public class testYaml {  public static void main(String[] args) {  Yaml yaml = new Yaml();  Person person = new Person("n4c1", 20);  String str = yaml.dump(person);  System.out.println(str);  String yamlStr = "!!org.n4c1.unserial.Person {age: 20, username: n4c1}";  Person person2 = (Person) yaml.load(yamlStr);  System.out.println(person2.getUsername());  }  
}

输出

getAge方法调用
getUsername方法调用
!!org.n4c1.unserial.Person {age: 20, username: n4c1}setAge方法调用
setUsername方法调用
getUsername方法调用
n4c1

调试分析

snakeYaml的解析可以说是以节点为基础
假如有以下yaml文档

person:name: "张三"age: 30hobbies:- 读书- 健身- 编程

词法分析器会逐个token解析出各个节点, 拆分出来各个节点对应:

  • 整个文档的根节点

    • 节点类型: 映射 (Mapping)

    • 代表: 整个文档的顶层结构。

    • 说明: 这个节点包含一个键值对,键是 person,值是它下面的整个结构。

  • name 节点

    • 节点类型: 标量 (Scalar)

    • 代表: 一个单一的值。

    • 说明: 这是一个键值对中的 ,其值为 "张三"。它是一个字符串。

  • age 节点

    • 节点类型: 标量 (Scalar)

    • 代表: 一个单一的值。

    • 说明: 它的值为 30,一个整数。

  • hobbies 节点

    • 节点类型: 序列 (Sequence)

    • 代表: 一个有序的列表。

    • 说明: 它的值是下面的三个项目,它包含了三个子节点。

  • hobbies 下的子节点

    • 节点类型: 标量 (Scalar)

    • 代表: 序列中的每一个项。

    • 说明: 这三个节点分别是 "读书""健身""编程",它们都是独立的字符串标量。

当反序列化我们的字符串时

String yamlStr = "!!org.n4c1.unserial.Person {age: 20, username: n4c1}";

根节点就是org.n4c1.unserial.Person它的值就是花括号中代表的内容, 其中又有age节点和username节点

再load方法打上断点分析

image.png
跟进到loadFromReader方法

它会实例化了一个composer, 并放在constructor中, 通过constructor来构建对象

Composer 是 SnakeYAML 的核心组件之一,它的职责是将 YAML 事件流(由 Parser 生成)合成为一个单一的、完整的 YAML 节点图(node graph)。

Construct会根据这些节点的类型和内容,把它们组装成一个你想要的 Java 对象。比如,它会把一个 映射节点 变成一个 Java Map 对象,或者一个自定义的类对象

image.png

在getSingleData方法中可以看见首先由composer获取了一个MappingNode, 描述的是该yaml文档的整体结构(node graph)
image.png

constructDocument根据文档结构, 还原对象
image.png

这里constructedObjects应该是类似缓存, 先从这里获取, 如果没有则创建
image.png
跟进constructObjectNoCheck
image.png

获取构造器
image.png

这里MappingNode的tag其实是tag:yaml.org,2002:org.n4c1.unserial.Person, 默认yaml是没有这个构造器的, 直接返回了一个 {Constructor$ConstructYamlObject@1306} 构造器, 之后用这个构造器取还原类

这里会进入ConstructYamlObject这个构造器的consructor方法
image.png
跟进
image.png

getClassForNode方法会反射获取我们节点的class
image.png

调用的是这个方法, 传入类名, 返回类的class, 然后把class存在node的type属性
image.png

之后回去看construct方法
image.png
有一步Constructor.this.newInstance(mnode), 这个newInstance方法是这个snakeYaml类自己实现的, 里面具体逻辑主要就是用刚刚反射得到的class,反射获取构造器, 实例出对象

之后这里的constructJavaBean2ndStep方法就是给对象的属性进行反序列化然后赋值,

image.png

首先flattenMapping对对象进行展平, 主要包括了处理重复的键处理合并键(<<
image.png

先看processDuplicateKeys方法
这个实际上是实例出键, 利用tuple去重, 可以看见调用了constructObject来构建对象
image.png
之后调用了对象的hashCode方法,

这里就出现了一个攻击面, hashCode方法可以触发到cc链等一些链子

后面的逻辑大概还是和去重有关, 不用管
image.png
flattenMapping结束后回到constructJavaBean2ndStep方法

protected Object constructJavaBean2ndStep(MappingNode node, Object object) {  flattenMapping(node);  Class<? extends Object> beanType = node.getType();  List<NodeTuple> nodeValue = node.getValue();  for (NodeTuple tuple : nodeValue) {  ScalarNode keyNode;  if (tuple.getKeyNode() instanceof ScalarNode) {  // key must be scalar  keyNode = (ScalarNode) tuple.getKeyNode();  } else {  throw new YAMLException(  "Keys must be scalars but found: " + tuple.getKeyNode());  }  Node valueNode = tuple.getValueNode();  // keys can only be Strings  keyNode.setType(String.class);  String key = (String) constructObject(keyNode);  try {  TypeDescription memberDescription = typeDefinitions.get(beanType);  Property property = memberDescription == null ? getProperty(beanType, key)  : memberDescription.getProperty(key);  if (!property.isWritable()) {  throw new YAMLException("No writable property '" + key + "' on class: "  + beanType.getName());  }  valueNode.setType(property.getType());  final boolean typeDetected = (memberDescription != null)  ? memberDescription.setupPropertyType(key, valueNode)  : false;  if (!typeDetected && valueNode.getNodeId() != NodeId.scalar) {  // only if there is no explicit TypeDescription  Class<?>[] arguments = property.getActualTypeArguments();  if (arguments != null && arguments.length > 0) {  // type safe (generic) collection may contain the  // proper class                    if (valueNode.getNodeId() == NodeId.sequence) {  Class<?> t = arguments[0];  SequenceNode snode = (SequenceNode) valueNode;  snode.setListType(t);  } else if (Set.class.isAssignableFrom(valueNode.getType())) {  Class<?> t = arguments[0];  MappingNode mnode = (MappingNode) valueNode;  mnode.setOnlyKeyType(t);  mnode.setUseClassConstructor(true);  } else if (Map.class.isAssignableFrom(valueNode.getType())) {  Class<?> keyType = arguments[0];  Class<?> valueType = arguments[1];  MappingNode mnode = (MappingNode) valueNode;  mnode.setTypes(keyType, valueType);  mnode.setUseClassConstructor(true);  }  }  }  Object value = (memberDescription != null)  ? newInstance(memberDescription, key, valueNode)  : constructObject(valueNode);  // Correct when the property expects float but double was  // constructed            if (property.getType() == Float.TYPE || property.getType() == Float.class) {  if (value instanceof Double) {  value = ((Double) value).floatValue();  }  }  // Correct when the property a String but the value is binary  if (property.getType() == String.class && Tag.BINARY.equals(valueNode.getTag())  && value instanceof byte[]) {  value = new String((byte[]) value);  }  if (memberDescription == null  || !memberDescription.setProperty(object, key, value)) {  property.set(object, value);  }  } catch (DuplicateKeyException e) {  throw e;  } catch (Exception e) {  throw new ConstructorException(  "Cannot create property=" + key + " for JavaBean=" + object,  node.getStartMark(), e.getMessage(), valueNode.getStartMark(), e);  }  }  return object;  
}

image.png

可以看见又使用constructObject对键进行了构造, 之后获取JavaBean的property, 把value构造出来放进去, 依然是使用constructObject方法
最后将value set进去
image.png

两个问题

  1. property如何被获取的
  2. property.set内部如何给属性赋值的

这个property是org.yaml.snakeyaml.introspector.MethodProperty类型

set方法
image.png
发现内部包装的是一个PropertyDescriptor

跟进getWriteMethod方法,

image.png

this.writeMethodRef已经存在这个set方法

搜索this.writeMethodRef.set, 发现有setWriteMethod, 打上断点
image.png

重新运行, 强制运行到此处, 可以看到调用栈是这样的

setWriteMethod:321, PropertyDescriptor (java.beans)
<init>:161, PropertyDescriptor (java.beans)
getTargetPropertyInfo:522, Introspector (java.beans)
getBeanInfo:428, Introspector (java.beans)
getBeanInfo:173, Introspector (java.beans)
getPropertiesMap:83, PropertyUtils (org.yaml.snakeyaml.introspector)
getProperty:152, PropertyUtils (org.yaml.snakeyaml.introspector)
getProperty:148, PropertyUtils (org.yaml.snakeyaml.introspector)
getProperty:309, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
constructJavaBean2ndStep:230, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
construct:171, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
construct:331, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:229, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:219, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:173, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:157, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:490, Yaml (org.yaml.snakeyaml)
load:416, Yaml (org.yaml.snakeyaml)
main:13, testYaml (org.n4c1.unserial)

可以看见是getProperty的时候就set了
image.png