Shiro-550 rememberMe 反序列化漏洞分析

本篇为 Shiro550 (CVE-2016-4437) 的漏洞复现、分析和学习

漏洞详情

https://issues.apache.org/jira/browse/SHIRO-550

Affects Version/s:1.2.4

漏洞复现

Check 采用空对象

image-20201213164250014

Attack:

ysoserial POC

java -jar ysoserial.jar CommonsCollections2 "open -a calculator"|base64 |sed ':label;N;s/\n//;b label'

Exploit

image-20201213164942281

漏洞分析

Shiro 1.2.4及以下版本下默认 cookie 中 rememberMe 字段的生成过程

  1. 序列化恶意对象(payload)
  2. 对序列化的数据进行AES加密
  3. 将加密后的数据进行base64编码
  4. 发送 rememberMe cookie

因此对应服务端的反序列化逻辑推测应该是: 接受 rememberMe cookie -> base64 解码 -> AES 解密 -> 触发反序列化

所以分析的重点就在 rememberMe 的生成和 服务端反序列化这里

加密流程

在一个正常的登录过程中,开启 Remember Me 时。若成功登录会触发 AbstractRememberMeManager#onSuccessfulLogin

image-20201205182602444

分析入口点 AbstractRememberMeManager#onSuccessfulLogin

public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
this.forgetIdentity(subject);
if (this.isRememberMe(token)) {
this.rememberIdentity(subject, token, info);
} else if (log.isDebugEnabled()) {
log.debug("AuthenticationToken did not indicate RememberMe is requested. RememberMe functionality will not be executed for corresponding account.");
}

}

关键点1: org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLoginorg.apache.shiro.mgt.AbstractRememberMeManager#isRememberMe 判断是否启用了 Remember Me 功能

image-20201205203026308

principals 对象在 org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity 创建

image-20201205204001354

principals 的值

image-20201205203805460

关键点2:序列化、加密对象

image-20201205204617882

依次看一下序列化这一步的步骤

// org.apache.shiro.mgt.AbstractRememberMeManager.convertPrincipalsToBytes
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = this.serialize(principals);

// org.apache.shiro.mgt.AbstractRememberMeManager.serialize
protected byte[] serialize(PrincipalCollection principals) {
return this.getSerializer().serialize(principals);
}

// org.apache.shiro.io.DefaultSerializer#serialize
public byte[] serialize(T o) throws SerializationException {
if (o == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
} else {
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 var6) {
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, var6);
}
}
}

// org.apache.shiro.subject.SimplePrincipalCollection#writeObject
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
boolean principalsExist = !CollectionUtils.isEmpty(this.realmPrincipals);
out.writeBoolean(principalsExist);
if (principalsExist) {
out.writeObject(this.realmPrincipals);
}

}

最后是在 org.apache.shiro.io.DefaultSerializer#serializetoByteArray 返回的字节码

image-20201205211522210

再依次看一下加密这一步的步骤

在看这个之前需要先看当前类的构造方法

image-20201205222540543

设置 AES 的各个信息 org.apache.shiro.crypto.DefaultBlockCipherService#DefaultBlockCipherService

image-20201205225320439

进入加密步骤

第一部分

// org.apache.shiro.mgt.AbstractRememberMeManager.convertPrincipalsToBytes
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
if (this.getCipherService() != null) {
bytes = this.encrypt(bytes);
}
}

/*
* org.apache.shiro.mgt.AbstractRememberMeManager#encrypt
* 后面先进入 this.getCipherService(); 的分析
*/
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = this.getCipherService();
return value;
}

// org.apache.shiro.mgt.AbstractRememberMeManager#getCipherService
public CipherService getCipherService() {
return this.cipherService;
}

/*
* org.apache.shiro.mgt.AbstractRememberMeManager
* 这个是在该类调用构造方法初始化的时候就附值的
*/
public abstract class AbstractRememberMeManager implements RememberMeManager {
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
private CipherService cipherService = new AesCipherService();

// org.apache.shiro.crypto.AesCipherService
public class AesCipherService extends DefaultBlockCipherService {
private static final String ALGORITHM_NAME = "AES";

public AesCipherService() {
super("AES");
}
}

// org.apache.shiro.crypto.DefaultBlockCipherService#DefaultBlockCipherService
public DefaultBlockCipherService(String algorithmName) {
super(algorithmName);
this.modeName = OperationMode.CBC.name();
this.paddingSchemeName = PaddingScheme.PKCS5.getTransformationName();
this.blockSize = 0;
this.streamingModeName = OperationMode.CBC.name();
this.streamingPaddingSchemeName = PaddingScheme.PKCS5.getTransformationName();
this.streamingBlockSize = 8;
}

/*
* org.apache.shiro.mgt.AbstractRememberMeManager#encrypt
* 运行刚刚这个类的后半部分
* 这里 cipherService 确定了加密类型等
*/
protected byte[] encrypt(byte[] serialized) {
if (cipherService != null) {
ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey());
value = byteSource.getBytes();
}

return value;
}

这里先看 getEncryptionCipherKey 方法的调用

/*
* org.apache.shiro.mgt.AbstractRememberMeManager#getEncryptionCipherKey
* 这里的 this.encryptionCipherKey 是之前类初始化的时候就定义的
*/
public byte[] getEncryptionCipherKey() {
return this.encryptionCipherKey;
}

/*
* 回顾类的初始化
* org.apache.shiro.mgt.AbstractRememberMeManager#AbstractRememberMeManager
* 注意 DEFAULT_CIPHER_KEY_BYTES,其实是之前 private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="); 已经硬编码好的。 这是漏洞的根源
*/
public AbstractRememberMeManager() {
this.setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

// org.apache.shiro.mgt.AbstractRememberMeManager#setCipherKey
public void setCipherKey(byte[] cipherKey) {
this.setEncryptionCipherKey(cipherKey);
this.setDecryptionCipherKey(cipherKey);
}

// org.apache.shiro.mgt.AbstractRememberMeManager#setEncryptionCipherKey
public void setEncryptionCipherKey(byte[] encryptionCipherKey) {
this.encryptionCipherKey = encryptionCipherKey;
}

第二部分 然后是 encrypt 方法 (核心 AES 加密)

传入的两个参数一个是刚刚在 org.apache.shiro.io.DefaultSerializer#serializetoByteArray 返回的字节码。另一个就是 AES KEY

image-20201205232236114

后面的就是 AES 加密的过程, 最后会返回一个 bytes

第三部分

/*
* 返回 org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity
* bytes 返回两个参数经过 encrypt 加密的值
*/
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = this.convertPrincipalsToBytes(accountPrincipals);
this.rememberSerializedIdentity(subject, bytes);
}

/*
* org.apache.shiro.web.mgt.CookieRememberMeManager#rememberSerializedIdentity
* base64 加密
* 设置 cookie
*/
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
if (!WebUtils.isHttp(subject)) {
······
} else {
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);
String base64 = Base64.encodeToString(serialized);
Cookie template = this.getCookie();
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
}
}

image-20201205234932955

完成了 序列化对象 -> AES 加密 -> Base64 加密 -> 设置 cookie 值 的过程

解密流程

POC 生成:https://github.com/P2hm1n/vulnExploit/blob/main/shiro_rememberMe_generate.py

从 POC 的触发来看解密流程

// org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity
protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) {
RememberMeManager rmm = this.getRememberMeManager();
if (rmm != null) {
try {
return rmm.getRememberedPrincipals(subjectContext);
···

/*
* 上面 return 调用 org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals
*/
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;

try {
byte[] bytes = this.getRememberedSerializedIdentity(subjectContext);

/*
* 上面调用 org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity
*/
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
···
String base64 = this.getCookie().readValue(request, response);

/*
* 上面调用 org.apache.shiro.web.servlet.SimpleCookie#readValue
* 关键函数 readValue 返回 rememberMe 的 POC 值
*/
public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
// 获取到了传入的 cookie 的名字 name: "rememberMe"
String name = this.getName();
String value = null;
// 这里获取到 rememberMe 对应的值
javax.servlet.http.Cookie cookie = getCookie(request, name);
if (cookie != null) {
value = cookie.getValue();
log.debug("Found '{}' cookie value [{}]", name, value);
} else {
log.trace("No '{}' cookie value", name);
}
// 返回 rememberMe 的值
return value;
}

/*
* 跳回 org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity
* 关键点:会进行base64解码
*/
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
···
if ("deleteMe".equals(base64)) {
return null;
} else if (base64 != null) {
// 确实是否是base64
base64 = this.ensurePadding(base64);
···
byte[] decoded = Base64.decode(base64);
···
return decoded;
} else {
return null;
}
}
}
}

/*
* 跳回 org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals
* 此时已经将 base64 进行了解码
*/
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
···
principals = this.convertBytesToPrincipals(bytes, subjectContext);


/*
* 调用 org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals
*/
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (this.getCipherService() != null) {
bytes = this.decrypt(bytes);
}

/*
* 调用 org.apache.shiro.mgt.AbstractRememberMeManager#decrypt
* 关键点: 进行 AES 解密, 并返回解密值
*/
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = this.getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, this.getDecryptionCipherKey());
serialized = byteSource.getBytes();
}

return serialized;
}

/*
* 返回 org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals
* 此处调用 deserialize
*/
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
···
return this.deserialize(bytes);
}

/*
* 调用 org.apache.shiro.mgt.AbstractRememberMeManager#deserialize
*/
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
return (PrincipalCollection)this.getSerializer().deserialize(serializedIdentity);
}

/*
* 调用 org.apache.shiro.io.DefaultSerializer#deserialize
* 最终关键点:进行反序列化
*/
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);

try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
T deserialized = ois.readObject();
ois.close();
return deserialized;
} catch (Exception var6) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, var6);
}
}
}
}

以下是几个关键点截图:

image-20201216012419243

image-20201216012229514

image-20201216011528037

image-20201216011918348

漏洞修复

https://github.com/apache/shiro/commit/4d5bb000a7f3c02d8960b32e694a565c95976848

硬编码 -> 随机值

image-20201205223500139

利用限制、坑点及思考

为什么 100key 能用

各种 shiro exploit 似乎都在集成一个爆破 key 的功能。但是根据漏洞可以看出 1.2.4 的 key 是硬编码的,然后 1.2.5 之后变成了随机值。key 值爆破似乎跟两个版本没有什么关系。

对 shiro 的了解和研究并不深刻,这个问题的答案来源于:关于Shiro反序列化漏洞的延伸—升级shiro也能被shell

可能性1:有其他开源框架整合了shiro,并且有这样一段配置文件,大家就都直接用了。

可能性2:因为这种代码都是互相抄来抄去的,在博客里,教程里,github里有这个代码,开发直接拿过来用。

Forexampe: ShiroConfig

/**
* cookie管理对象
* @return
*/
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie加密的密钥
cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}

为什么有的链打不了

这个在身边的 @p1g3 和 @l3yx 的博客中都阐述的很详细了。

简而言之就是 org.apache.shiro.io.DefaultSerializer#deserialize 中调用 的 ClassResolvingObjectInputStream 重写了 resolveClass

public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);

try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
T deserialized = ois.readObject();
ois.close();
return deserialized;
} catch (Exception var6) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, var6);
}
}
}
}

看一下 org.apache.shiro.io.ClassResolvingObjectInputStream 是怎么写的

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.apache.shiro.io;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import org.apache.shiro.util.ClassUtils;
import org.apache.shiro.util.UnknownClassException;

public class ClassResolvingObjectInputStream extends ObjectInputStream {
public ClassResolvingObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}

protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
try {
return ClassUtils.forName(osc.getName());
} catch (UnknownClassException var3) {
throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", var3);
}
}
}

Compare to java.io.ObjectInputStream#resolveClass

public Class<?> resolveClass(ObjectStreamClass objectStreamClass) throws IOException, ClassNotFoundException {
String name = objectStreamClass.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException e) {
ClassNotFoundException classNotFoundException = e;
Class<?> cls = primClasses.get(name);
if (cls != null) {
return cls;
}
throw classNotFoundException;
}
}

Shiro 中使用了 ClassUtils.forName 而原生的使用的 Class.forName

具体区别跟进 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);
} else {
return clazz;
}
}

直观可以看到的是里面大多采用了 loadClass 来加载。至于使用其加载类有什么弊端可以看 @p1g3 这段话

ClassLoader.loadClass的方式并不支持加载数组类,这也是为什么cc没法用的原因,当然这部分我并没有深入分析,因为其涉及到了Java中一种叫”双亲委派”的类加载思路 & 突破”双亲委派”的思路,这部分和漏洞无关

此时我们则无法使用任何带数组对象的gadget,而cc3.2.1中的所有链(在官方仓库内的)都需要用到数组对象transformer,所以需要重新构造链,用其他链来打。

大概小结:

Shiro resovleClass使用的是ClassLoader.loadClass()而非Class.forName(),而ClassLoader.loadClass不支持装载数组类型的class。

至于 ClassLoader.loadClass 是不是所有 class 都无法加载?

参考下一小标题

延伸 - resovleClass 的数组类加载

在 P牛圈子中有个师傅文章 看到了这一点,对于上一个小标题中总结的 ClassLoader.loadClass不支持装载数组类型的class 有了全新的看法。

[Ljava.lang.StackTraceElement 数组类加载

先留个坑,CC调完回来写

JRMP 攻击

分析思考

匆匆分析完了 Shiro550。除了加解密流程跟 POC 的编写外似乎并没有分析太多东西。由于对 AES 的理解不足直接在加解密过程中直接忽略了 AES 的具体步骤。感觉这样浅尝辄止的分析不是太好。因此在下一次分析的时候要具体细化一下。

前文中提到了 resovleClass 对 Shiro 利用链的限制,那么什么链可以使用,该如何去构造新的链,是我们值得深思的问题。先留个坑,准备去理一下 CommonsCollections gadget chain

Shiro-721 PaddingOracle CBC Attack

Shiro-721 PaddingOracle CBC Attack

个人总结漏洞原理以下几点:

  1. rememberMe 加解密原理
  2. Shiro 1.4.1及其之前版本的Cookie中的rememberMe字段是使用AES-128-CBC模式来加密生成的

限制:

  1. Apache Shiro <= 1.4.1
  2. 需要有正常用户登录的 Cookie rememberMe

涉及到密码算法,且实际利用有限。不在本文分析范围

Reference

https://l3yx.github.io/

https://payloads.info/2020/06/23/Java%E5%AE%89%E5%85%A8-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%AF%87-Shiro%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/#%E5%8A%A0%E5%AF%86%E8%BF%87%E7%A8%8B

http://www.lmxspace.com/2020/08/24/%E4%B8%80%E7%A7%8D%E5%8F%A6%E7%B1%BB%E7%9A%84shiro%E6%A3%80%E6%B5%8B%E6%96%B9%E5%BC%8F/

ysoserial URLDNS 调试分析 网鼎杯线下半决赛 faka 题目复盘
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×