shiro简介
简单概括一下shiro是啥(
Apache Shiro 是Java 的一个安全框架。Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与Web 集成、缓存等。
shiro 反序列化
Apache Shiro 是一个广泛使用的 Java 安全框架,它提供了认证、授权、加密和会话管理等功能。Shiro 反序列化漏洞,特别是 Shiro-550(CVE-2016-4437)和 Shiro-721(CVE-2019-12422),是由于 Shiro 在处理记住我(RememberMe)功能时,使用了可预测的加密密钥进行序列化和反序列化操作,导致攻击者能够执行任意代码。
漏洞原理
Shiro-550 漏洞(CVE-2016-4437)的产生是因为 Shiro 在序列化用户身份时使用了硬编码的 AES 密钥。攻击者可以通过构造恶意的序列化对象,然后将其加密和编码后作为 RememberMe cookie 发送给服务器,服务器在反序列化时执行恶意代码
cookie 加密解密认证过程

漏洞特征
返回包中带有 rememberMe=deleteMe 字段
- 登录失败时:如果用户登录失败,Shiro会返回一个
rememberMe=deleteMe的Cookie值,表示清除之前的RememberMe状态。 - 登录成功但未勾选RememberMe时:即使用户登录成功,但如果没有勾选RememberMe选项,Shiro也会返回
rememberMe=deleteMe,表示不启用RememberMe功能。
在Shiro的RememberMe功能中,Cookie值会经过解密和反序列化处理。如果攻击者能够构造恶意的Cookie值并触发反序列化漏洞,就可能导致任意代码执行。因此,当返回包中出现rememberMe=deleteMe字段时,表明系统使用了Shiro的RememberMe功能,可能存在反序列化漏洞的风险。
审计流程
正向分析
正向就是上图的上方这条路线
首先关注 rememberIdentify 方法,这个方法就是实现的 “记住我” 功能
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}
其中的convertPrincipalsToBytes()实现了对用户信息的序列化+AES,跟进过去(其实就写在下面)
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}
那么先去看一下序列化这一块
序列化
方法的第二行调用了 serialize 方法,跟进,看看怎么实现的
protected byte[] serialize(PrincipalCollection principals) {
return getSerializer().serialize(principals);
}
是对 getSerializer().serialize(principals); 的调用,直接再跟进:
public interface Serializer<T> {
byte[] serialize(T o) throws SerializationException;
T deserialize(byte[] serialized) throws SerializationException;
}
是个接口,去看一下接口的实现
有两个,一个是DefaultSerializer,一个是 XmlSerializer ,肯定不是xml的,那就去看看默认
package org.apache.shiro.io;
import java.io.*;
public class DefaultSerializer<T> implements Serializer<T> {
public byte[] serialize(T o) throws SerializationException {
if (o == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(baos);
try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(o);
oos.close();
return baos.toByteArray();
} catch (IOException e) {
String msg = "Unable to serialize object [" + o + "]. " +
"In order for the DefaultSerializer to serialize this object, the [" + o.getClass().getName() + "] " +
"class must implement java.io.Serializable.";
throw new SerializationException(msg, e);
}
}
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
}
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
ois.close();
return deserialized;
} catch (Exception e) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, e);
}
}
}
使用的输出流是 ByteArrayOutputStream 不同于上一篇笔记示例用的文档输出流
try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(o);
oos.close();
return baos.toByteArray();
}
oos还是没区别的,一个 writeObject 完成序列化了
AES加密
回到刚刚的 convertPrincipalsToBytes 方法
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}
看完了序列化去看看 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;
}
getEncryptionCipherKey() 获取到加密的密钥,然后还有刚刚序列化得到的东西,然后一起传参给加密方法 cipherService.encrypt
跟进 cipherService.encrypt
ByteSource encrypt(byte[] raw, byte[] encryptionKey) throws CryptoException;
这里是实现了一个接口,去看一下
public ByteSource encrypt(byte[] plaintext, byte[] key) {
byte[] ivBytes = null;
boolean generate = isGenerateInitializationVectors(false);
if (generate) {
ivBytes = generateInitializationVector(false);
if (ivBytes == null || ivBytes.length == 0) {
throw new IllegalStateException("Initialization vector generation is enabled - generated vector" +
"cannot be null or empty.");
}
}
return encrypt(plaintext, key, ivBytes, generate);
}
经典的AES特征,偏移量和密钥
然后之前说过是因为硬编码的密钥,导致了漏洞的产生,那key在哪呢(
接着往上一步步跟进也找不到,直接搜索 DEFAULT_CIPHER_KEY_BYTES 就能看到(IDEA直接双击shift)
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
序列化和加密都完成了,该 base64 了
base64
回到最开始的地方
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals); //分析终了
rememberSerializedIdentity(subject, bytes);
}
去跟进 rememberSerializedIdentity 方法
protected abstract void rememberSerializedIdentity(Subject subject, byte[] serialized);
这也是个 interface ,继续(两个,选择跟进 CookieRememberMeManager 那个)
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
if (!WebUtils.isHttp(subject)) {
if (log.isDebugEnabled()) {
String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " +
"request and response in order to set the rememberMe cookie. Returning immediately and " +
"ignoring rememberMe operation.";
log.debug(msg);
}
return;
}
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);
//base 64 encode it and store as a cookie:
String base64 = Base64.encodeToString(serialized);
Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
}
看后半部分,把序列化并且AES之后的一个结果(byte[])传参进来然后直接 base64
然后其他的代码的功能就是生成好请求包,然后把我们 base64 之后的值 save 进 http 的请求包里面
反向流程的分析下集再说(逃
