• Apereo CAS反序列化漏洞中数据加解密研究


    0x01、简介

    Apereo CAS,全称为 Central Authentication Service,是一种开源的单点登录(SSO)解决方案。它提供了一个可扩展的、可定制的平台,用于统一身份验证和访问控制,支持多种认证协议和技术。Apereo CAS 可以轻松地集成到现有的应用程序和服务中,为用户提供单点登录和数据交互能力。该系统具有高度的安全性和可靠性,同时也支持多种操作系统和编程语言。Apereo CAS 是一个成熟的、高度稳定的 SSO 解决方案,已经被广泛地应用于大型机构、企业和政府机构中。

    4.1.7版本之前存在AES默认密钥的问题,利用这个默认密钥我们可以构造恶意信息触发目标反序列化漏洞,进而执行任意命令。

    最近在写攻防中用到的工具(红队版:一键开天门;蓝队版:一键守天门)

    其中蓝队版的一个功能:一键解密流量工具,为实现一键智能化

    (毕竟蓝队猴子怎么会看得明白什么是shiro、什么是cas、什么是哥斯拉\蚁剑\冰蝎等等呢(对不起,不是所有人但是真的有))

    因此需要研究一下CAS漏洞利用中execution值的加解密算法!结果网上没有一个深入研究构造结构体的,大多都是误导文章,而且都是直接套用cas原jar包

    结论:UUID + _ + 头部长度标识(7byte) + iv长度标识(1byte) + iv值(16byte) + keyName(10byte) + AES密文
    在这里插入图片描述

    0x02、网上获取资料

    针对于网上搜集到的资料进行总结:

    1. execution值处理流程过程为: 剔除头部UUID字段+base64解密+AES解密+Gzip解密
    2. AES存在默认密钥
    3. AES加密模式为 AES/CBC/PKCS7
    4. 【很多文章里都没有提到】 AES加解密相关的配置会 先去配置文件中获取 ,没有配置密钥信息的会使用 jar包默认的密钥信息
    5. 密钥库 的密码为 changeit
    6. 默认 keystore文件 位于 spring-webflow-client-repo-1.0.0.jar!/etc/keystore.jceks
    7. 网上有很多exp工具,直接使用cas工程的相关jar包、java文件,来调用加密函数,不需要自己写
    8. cas工程使用的是 Cryptacular 第三方库进行的AES加解密。

    真的如网上所说的这些信息就够了么?开始调试

    0x03、初步运行失败

    1、分析:

    1. 剔除头部uuid;
    2. AES\CBC\PKCS7Padding解密,key为changeit;
    3. iv向量为多少?网上没提到,猜测随机生成;
    import com.hotboy.utils.aesUtils;
    import com.hotboy.utils.strUtils;
    
    import java.nio.charset.StandardCharsets;
    import java.util.Arrays;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    public class casDecryptTest {
    
        public static byte[] decrypt(String input,aesUtils aes){
    		String input = "7b951c2a-e78f-4286-95fe-970782352a84_AAAAIgAAABDE2HZ3uiB2bzFHXNO5uObYAAAABmFlczEyOCPJEIAG4U8FA%2b%2bSoqcRzovVlpSWfd/raZVfVf3gXUc8f9Xz%2beN25UhRBwwmMRAv%2byjSVLKbWeRzPkGeVvof/44rS4PcFfF9pzwo%2bEbqJz6ZBCo5%2blAczVCAp8UBjRy7R/jkb/YSZj6YBHDMJ0ejjqFly779A9b3opzyMwCIJod0yvs0qtYNd0qXhd8yY/XpatlelngVKxqLDp0lrwXmP7W3YblsIV/r3bJv2mHk1qAgVL54yTX4en4i17z37qKv6CBkRxZN5ORAERYUm4E%2bsmckjnQEKcjb2bqMWSi7WKxc57DPIBwnjmUJ8plB8aoNsjKxuOYri%2bsBpkZDcFucuyiTOwPOlGm4CaUYHxpoCiaLSJb%2bsj/0Iml107F8L%2bH/pzwtH1BHQef1eVtcml0AKGpVf0YzR9UUua8PysUxbqQpFa36nC3RgIoz97v9Hi6oCkBg9WlarS0QVE7lUSG6SquiT/hzPz9TvP%2b3Yw48BrU04JltH76rboR07zDvgMy3sBk32hKb8w2qbBk5Vo3xVRqwS8Z6fUm1Zl9BnRXr1/kIFP58dLbkwraq%2bD4/%2bbtAMb4F7LB8c%2b1jihZU3vHI7gvpQTtrLr4z%2bqtm8C8NN4rkcbT77QLfKM%2bMCqRhSFzoGyy9Qs%2bhO3Xi34tHUh6oANU3FPP/Cd%2b3/B6w%2bw9W13ecESXG8H6w374I64UiWRQRSnWqciO%2b4BVhkdRtfOF2d7UCw9zL/vqwrTcMgUtbCPtbD1Wj/ucxvut5oeeBPlfPicGz0Ohr9FI0C9j1myRP6DZ6Uv2SlomPnNhrY/z7C6SbjkSF9CLvOhN92XA9OklEkm1HOIm7uFhQ5JL1Fv60muZW/ifqjUrR2kkhUv/LA4nixpcjUpOYRdbT2O7/wIXs7jhInwlFVACx3TL4KrRO6Zb79uGJJdMlHdWREfPr81dxJ9G8u%2bYNYz2djvrbTV%2b/BgZxMRmVugXLH7%2bQRAOhozyvDm5XMnGusjga/NLBFQO4A341puj2QzTdG2R%2bzUexFLeXc2TZ5XgrKqVvjIKeNYPECSKDiZZc%2bj1ivpQt4bUNS2Nx7Hup2%2bUhiwxA8pVtxiVY0YU3QWvphUXUSdu5nFp7qOGz0Yy9m5wOU4kIKyIlJnEeaKVMTETd7TtzhQ5sYEATKpzGUrezaaHei0%2bbkRjGAgK7q8/Wkb/tueJZ2af3IeHOeyulA/%2bHRpvDDMzS0AiGvhvLMVu7PNUKpIKrGuPgIPuXTy6N2WoYQiewnAKekaS03DukE2g%2bTTMFVJ2OXUBpF5MHxAs68NoJCw0s/UgdzKaYWUHH6dS9AjMiYx0eSS7RtUf6bbgMZkLQzL%2bW4CV8gRvacJ7Jn0bsg3zZBGt/8CaVfhNpXU4g3MqAYz9w0iinx0mUPgP7%2bYwf2D2Qg/KkGeY8Qg3sJH4T7oEjs4PjeqbYpPGxjQZd7Uhlv%2b8TkorNmfGXBrl6M9Deow3lWGX/zl1u9uH%2bbNTiSRJAb1dGqqiVtGE3j1Ld/RRgxCI9/TB93dBdovtdKVmhZyjfsIWQS5ypneaYgFbv/l0WBG8GvwGs3QBbYDQbNC8lF5OQ7OS0xCYWBecHqvOlEjU68Yb8crUe3f0q9YXkDum%2b3OlDwmg/SQylqrmO9taYI%2brU8JByMu/ZnTjiWPMOyUx9Codsj5ml6PxMK3OcZHBj7G9BLJJz4XHPisOZxt0LUjKMeH/0itQmxeEnPn%2blvcOWp%2bYNsF6UjmYdnKkSEd9s61jN9lPwCB3m10w%2bRWEIPbbgm6Gn/Yelf1dd4T%2b4e70tJeTWI%2bImR%2be97HxOLEyw3D7aCSzS0LSzTvtGsdk7XrvmiYTzK3aR/i/SQypZCNgpi4vncxLAQ8iJ9xT541Z0gqhWYfJB7XS087RhmnhGfwISZ5rkjYBZPr12Ho5M4xaRKfpu2qWgbnvuBBqMNJWu5JOPMbgVMm3C3DzcRnTs9oVnI81go4j1yJ0tOAguj8iviikAk1/2s3EIgFGnA6gxSZV6XlDwhqpBFzMwQNne/O0bvezmLwQ/Rx%2bjbNSo8ZhmdQ%3d%3d";
    		byte[] res = null;
            String code = "";
            input = strUtils.urlDecode(input);
    
            Pattern pattern = Pattern.compile("([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})_(.*)");
            Matcher matcher = pattern.matcher(input);
            if (matcher.find()) {
                code = matcher.group(2);
            }
            if( code == null || code == "") {
                return res;
            }
    		try {
    
                byte[] encryptData = strUtils.base64Decode(code.getBytes(StandardCharsets.UTF_8));
    
                byte[] key = "changeit".getBytes(StandardCharsets.UTF_8);
                byte[] iv = aes.generateRandomBytes(16);
                
                String mode = "CBC";
                String padding = "PKCS7Padding";
    
                byte[] result = aes.decrypt(
                        encryptData,
                        key,
                        iv,
                        mode,
                        padding
                );
                
                aes.mode_AES.set(mode);
                aes.padding_AES.set(padding);
                aes.key_AES.set(Arrays.toString(key));
                aes.iv_AES.set(Arrays.toString(iv));
                res = result;
                
            } catch (Exception e) {
                e.printStackTrace();
            }
            
            return res;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    2、Tips:

    你们直接运行不了,很多都是单独封装的模块,可以根据文中意思自己大概写一下。Sorry~ 娜扎

    0x04、分析原因

    • 1、自己写解密算法 / 直接使用cas工程的相关jar包、java文件,调用解密函数

    1. 工程内,自己已经使用了基于java标准API的crypto封装了更完善的AES加解密方法,并且支持自动检测结果是否存在gzip\class\反序列化特征并再处理,不同于Cryptacular,支持低级别的数据加密。
    2. 作为高级软件开发工程师,怎么能容忍自己的代码变成屎山呢?
    • 2、为什么会解密失败?

    1. 突然想起来,自己傻Der了~,Aes的Key怎么可能是changeit,Key应该是16位
    2. 全网都在提 “changeit” 硬编码,以下代码,被误导了:
    public EncryptedTranscoder() throws IOException {
        final BufferedBlockCipherBean bufferedBlockCipherBean = new BufferedBlockCipherBean();
        bufferedBlockCipherBean.setBlockCipherSpec(new BufferedBlockCipherSpec("AES", "CBC", "PKCS7"));
        bufferedBlockCipherBean.setKeyStore(createAndPrepareKeyStore());
        bufferedBlockCipherBean.setKeyAlias("aes128");
        bufferedBlockCipherBean.setKeyPassword("changeit");
        bufferedBlockCipherBean.setNonce(new RBGNonce());
    
        setCipherBean(bufferedBlockCipherBean);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 因为没有用过Cryptacular库,学习一波才明白,使用"changeit"这个pass初始化密钥库,然后根据Alias(“aes128”)这个keyName获取Aes真正的key。
    方法作用
    setKeyStore初始化密钥库
    setKeyAlias设置获取密钥类型
    setKeyPassword设置密钥库密码(changeit)
    1. 那必然是需要部署相关jar包、java文件调试了(Tips:直接使用网上exp工具项目,他们已经抽取完毕了)

    0x05、断点调试

    1. java中进行AES解密,基本上所有第三方包,都会进入AES解密时的java标准api
    方法作用
    cipher.init()初始化密码(Cipher)对象,需要传入加密的 mode / key / iv
    cipher.doFinal()执行加密或解密操作
    1. 想要获取key及iv向量,关键的就在cipher.init(),找到位置打断点获取key/iv的值,但是无法直接全局搜索cipher.init(因为该方法在jar包中,编辑器不支持搜索jar中class的内容)
    • 手动点开jar,根据可能的文件名点开查看寻找;(不推荐)
    • 调用外层java中的解密方法,手动根据方法调用找方法(如果开发者再次封装的不繁杂的话可以)
    • 调用外层java中的解密方法,打断点先大跳(步过)粗略走流程,然后根据可能位置逐步步入判断
    1. 采用了第二种方案,结果发现封装的裂开,很容易走错。采用第三种方案:

    最终在找到在BufferedBlockCipherBean.class中,调用到了cipher.ini()

    Tips:在步入得到过程中,有的方法[步入]功能不进去,需要使用[强制步入]功能

    在这里插入图片描述

    0x06、代码分析

    1、key/iv初始化函数确定

    key初始化方式的确如之前分析的根据keyName在密钥库中寻找lookupKey

    key初始化位置在:params = new ParametersWithIv((cipherParameters)params, header.getNonce())

    通过打断点获取到了key真正的值(byte[]):[78, -47, -80, -25, 76, 55, -57, -111, -81, -3, -54, 62, 118, 15, 113, 0]
    在这里插入图片描述

    iv初始化位置在:params = new ParametersWithIv((cipherParameters)params, header.getNonce())

    这一步会获取IV向量值存放在params中,通过多次断点调试,发现针对于相同密文,他的IV是不变的,不同密文,他的IV值是会变的。因此得到一个结论:

    key是固定的,iv是根据密文计算得来的

    2、key初始化函数分析

    • keys固定的,就不带着函数挨个进了,流程如下:

    读取配置文件中的配置;默认无,则使用"changeit"这个固定pass初始化解密读取默认密钥库(spring-webflow-client-repo-1.0.0.jar!/etc/keystore.jceks),然后根据Alias(“aes128”)这个keyName遍历获取获取Aes真正的key

    3、iv初始化函数分析

    在这里插入图片描述
    代码解读:

    • 1、将原始密文读取如bb变量; // 如 [0,0,0,34,0,0,0,16,…]
    • 2、大端序重新排列; // 不变
    • 3、从bb根据第二部的大端序读取带4个字节兵解释为带符号的整数值,作为header头部长度; // 34
    • 4、创建byte[] nonce,大小为iv长度标识; // byte[]{0,0,0,0,0,0…} 大小为16
    • 5、从bb的iv长度标识后中读取对应长度放入nonce;
    • 6、header头部处理完剩余的部分放入keyName字段; // aes128

    0x07、总结

    以此可以推断出,密文的实际构造情况为

    UUID + _ + 密文

    UUID + _ + header头部(34byte) + AES密文

    UUID + _ + 头部长度标识(7byte) + iv长度标识(1byte) + iv值(16byte) + keyName(10byte) + AES密文

    运行通过

    package com.hotboy.content.blueTeam;
    
    import com.hotboy.utils.aesUtils;
    import com.hotboy.utils.strUtils;
    
    import java.nio.charset.StandardCharsets;
    import java.util.Arrays;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    /**
     * @author Potato
     */
    public class casDecrypt {
    
        public static byte[] decrypt(String input,aesUtils aes){
            byte[] res = null;
            String code = "";
            input = strUtils.urlDecode(input);
    
            Pattern pattern = Pattern.compile("([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})_(.*)");
            Matcher matcher = pattern.matcher(input);
            if (matcher.find()) {
                code = matcher.group(2);
            }
            if( code == null || code == "") {
                return res;
            }
    
            try {
    
                byte[] encryptData = strUtils.base64Decode(code.getBytes(StandardCharsets.UTF_8));
    
                byte[] key = {78, -47, -80, -25, 76, 55, -57, -111, -81, -3, -54, 62, 118, 15, 113, 0};
                byte[] iv = new byte[16];
                System.arraycopy(encryptData, 8, iv, 0, 16);
                String mode = "CBC";
                String padding = "PKCS7Padding";
    
                // 剔除header头部34个标志性字节
                byte[] tmpEncryptData = new byte[encryptData.length - 34];
                System.arraycopy(encryptData, 34, tmpEncryptData, 0, encryptData.length - 34);
                encryptData = tmpEncryptData;
    
                byte[] result = aes.decrypt(
                        encryptData,
                        key,
                        iv,
                        mode,
                        padding
                );
    
                aes.mode_AES.set(mode);
                aes.padding_AES.set(padding);
                aes.key_AES.set(Arrays.toString(key));
                aes.iv_AES.set(Arrays.toString(iv));
    
                res = result;
    
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            return res;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    运行成功,成功获取源代码:

    在这里插入图片描述


    感谢Allan、Songqb、小严同学,虽然没给我解决任何问题:)

  • 相关阅读:
    驾驶数字未来:汽车业界数字孪生技术的崭新前景
    【Python脚本进阶】2.4、conficker蠕虫(上):Metasploit攻击Windows SMB服务
    Vue框架--Vue条件渲染
    【LeetCode刷题日志】189.轮转数组
    过滤器,计算属性,属性侦听器
    RNN循环神经网络(过程解析)
    Python基础教程:列表推导式详解
    坤坤的加法
    DPDK18.08上对VIRTIO IN ORDER 功能所做的优化
    杰理之在所有模式下打开喊话增加 mic 自动 mute【篇】
  • 原文地址:https://blog.csdn.net/weixin_43526443/article/details/132919381