Shiro反序列化漏洞笔记三(解疑篇)

0x1 前言

在前面两篇笔记中都提到过Shiro反序列化漏洞的坑:在Shiro反序列化漏洞检测中,如果直接使用CommonCollections3、5、6、7链会导致反序列化漏洞无法利用成功,这篇笔记就来记录一下失败原因的分析过程。

0x2 分析过程

这里使用的还是之前shiro的samples环境,加入commons-collections-3.2.1依赖。首先使用ysoserial的CommonsCollections6生成EXP,从Catalina日志可以看到反序列化失败,提示找不到[Lorg.apache.commons.collections.Transformer这个类。

在报错代码附近,即反序列化代码处下断点。

进一步跟进可以发现ClassResolvingObjectInputStream继承了ObjectInputStream,并且重写了resolveClass方法。

原本ObjectInputStream类中resolve方法如下。对比可以发现Shiro使用了重新定义的ClassUtils.forName方法代替了原本ObjectInputStream类中的Class.forName。

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

跟进ClassUtils类的forName方法,发现最终使用loadClass来加载类,默认使用THREAD_CL_ACCESSOR.loadClass来进行加载,如果不成功会依次再使用CLASS_CL_ACCESSOR.loadClass和SYSTEM_CL_ACCESSOR.loadClass来进行加载。

三个ClassLoaderAccessor定义如下,THREAD_CL_ACCESSOR返回当前线程上下文的ClassLoader,在Tomcat中间件中,返回的当前线程上下文的ClassLoader为ParallelWebappClassLoader。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//THREAD_CL_ACCESSOR
private static final ClassLoaderAccessor THREAD_CL_ACCESSOR = new ExceptionIgnoringAccessor() {
@Override
protected ClassLoader doGetClassLoader() throws Throwable {
return Thread.currentThread().getContextClassLoader();
}
};

//CLASS_CL_ACCESSOR
private static final ClassLoaderAccessor CLASS_CL_ACCESSOR = new ExceptionIgnoringAccessor() {
@Override
protected ClassLoader doGetClassLoader() throws Throwable {
return ClassUtils.class.getClassLoader();
}
};

//SYSTEM_CL_ACCESSOR
private static final ClassLoaderAccessor SYSTEM_CL_ACCESSOR = new ExceptionIgnoringAccessor() {
@Override
protected ClassLoader doGetClassLoader() throws Throwable {
return ClassLoader.getSystemClassLoader();
}
};

跟到clazz = cl.loadClass(fqcn);再下一步进入到Tomcat的WebappClassLoaderBase类了。

如果需要进一步跟进Tomcat的运行情况,需要先加入Tomcat的依赖。这里是mvn项目,在pom.xml添加:

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.19</version>
</dependency>

进入到WebappClassLoaderBase类,WebappClassLoaderBase继承自URLClassLoader。loadClass就是Tomcat的类加载器的核心实现代码了。

loadClass方法具体代码如下:

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
143
144
145
146
147
148
149
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

synchronized (getClassLoadingLock(name)) {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class<?> clazz = null;

// Log access to stopped class loader
checkStateForClassLoading(name);

// (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;
}

// (0.2) Try loading the class with the system class loader, to prevent
// the webapp from overriding Java SE classes. This implements
// SRV.10.7.2
String resourceName = binaryNameToPath(name, false);

ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
// Use getResource as it won't trigger an expensive
// ClassNotFoundException if the resource is not available from
// the Java SE class loader. However (see
// https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for
// details) when running under a security manager in rare cases
// this call may trigger a ClassCircularityError.
// See https://bz.apache.org/bugzilla/show_bug.cgi?id=61424 for
// details of how this may trigger a StackOverflowError
// Given these reported errors, catch Throwable to ensure any
// other edge cases are also caught
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
// Swallow all exceptions apart from those that must be re-thrown
ExceptionUtils.handleThrowable(t);
// The getResource() trick won't work for this class. We have to
// try loading it directly and accept that we might get a
// ClassNotFoundException.
tryLoadingFromJavaseLoader = true;
}

if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}

// (0.5) Permission to access this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = sm.getString("webappClassLoader.restrictedPackage", name);
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}

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

throw new ClassNotFoundException(name);
}

大体的流程如下:
1.findLoadedClass0:检查当前要加载的类是否已经被WebappClassLoader加载过。
2.findLoadedClass:从java.lang.ClassLoader类加载缓存检查当前类是否已经被加载过。
3.javaseLoader.loadClass:尝试使用ExtClassLoader类加载器加载类。
4.如果前三步没找到,通过filter()检查类是否在定义的名单范围内,如果在的话则遵循双亲委派机制,使用Class.forName()加载类。由于delegate默认为false,并且符合filter()检查的类比较少,所以可以认为Tomcat在实现大多数类的加载的时候并不遵循双亲委派机制,也就是一般会跳过这一步。

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

5.在本地仓库中寻找该类,调用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(),如下:

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

上面代码中关键代码主要是:

1
2
3
resource = resources.getClassLoaderResource(path);
...
clazz = defineClass(name, binaryContent, 0, binaryContent.length, new CodeSource(codeBase, certificates));

其中,getClassLoaderResource主要在WEB-INF/classes和WEB-INF/lib/中搜索根据寻找类名,如果寻找到则将class文件字节码内容转成字节流,然后调用ClassLoader的defineClass将字节流转成class对象,从而完成类的加载。

6.最终,如果以上步骤都无法找到该类,尝试通过Class.forName()进行加载,如果找不到则抛出ClassNotFound异常。

至于为什么[Lorg.apache.commons.collections.Transformer这个数组类会加载失败,主要调试一下上面的5,6步。

数组类的全限定类名在第5步转成路径时会因为加入了[,所以是搜索不到的。

而来到第6步,使用Class.forName()进行加载,加载器是URLClassLoader,对应的path中不包含WEB-INF/lib/,因此也无法加载到[Lorg.apache.commons.collections.Transformer这个类,最终抛出异常。