引言
承接上一篇《Web应用安全————账号冻结与 Session 实时失效》关于 session 的学习,本篇博客聚焦如何通过 shiro 解决会话固定导致的漏洞问题。
首先,没怎么接触过应用安全方面的小伙伴可能会发起疑问 - 什么是会话固定?
简单来说,系统在登录前和登录后使用同一个 session id,就是会话固定,默认的session 管理机制都是会话固定的。会话固定可能会造成“会话固定攻击”(Session Fixation Attack)。这种攻击形式简单来说,有几种:
1、黑客先访问一个目标网站,在未登录的情况下,获得了一个 session id,然后将这个 session id 以钓鱼邮件的形式发给某个该网站的用户,用户缺乏信息安全意识,点击邮件中带有 session id 的连接并登录成功后,根据“会话固定”的登录前和登录后 session id 保持不变的原理,黑客可以在一定时间内使用 这个 session id 绕过登录验证,操作该用户的个人账户。
2、黑客通过某种形式的网络抓包,在某个用户登录时,劫持到了 session id,待该用户登录成功后,根据“会话固定”的原理,黑客可以在一定时间内通过这个 session id 随意操作该用户的个人账户。
一、会话固定攻击演示
固定会话攻击实际上就是黑客通过某种方式获得了正常用户登录时的 session id,然后利用用户的登录操作,使 session id 生效,并绕过登录验证。由于在登录前,session id 会通过 cookie 保存到浏览器中,因此很容易被黑客窃取。
1、首先,普通用户打开登录页面,会获得一个由服务器响应携带而来的 session id cookie:
在浏览器的控制台,我们可以轻而易举的得到这个 session id,此时,由于还未登录,session id 是无法正常使用的,黑客通过某种方式,比如脚本,或者桌面监控等,获取到了这个 session id ,放入自己准备登录的工具中。这里我使用post man 来演示黑客端的登录操作。
2、黑客输入目标网站的首页地址,这时,应用会拦截请求并弹出登录页面,此时黑客同样会收到服务器返回的 session id,不过他不会使用这个 session id,而是将窃取来的 session id 拷贝到 cookie 中,等待用户的登录操作。
3、紧接着,用户不知道自己的 session id 已经被窃取,依然执行了登录操作,并成功登录:
4、此时,黑客使用这个已经完成登录操作的 session id 再次执行首页的访问:
可以看到上面的 GIF,黑客仅仅是使用了一个 session id 就完成了对用户首页的访问,轻而易举地绕过了登录验证。如果这个用户是超级管理员,那么就可能对系统造成不小的伤害。
因此,为了解决这个会话固定的问题,必须在登录前和登录后刷新 session id ,并使登录前生成 的session 失效,这样,才不会让黑客利用会话固定绕过登录验证。
二、Shiro 防止会话固定
如果说上一篇文章可以是框架无关的操作,那么解决会话固定,就完全依赖于安全框架自身的一些封装方式。
在Shiro 中, session 的生成和保存是这样的:
1、shiro 检测到用户的请求,并生成 session,保存到缓存中 ,并将 session id 绑定在 response 的 cookie 中,响应回浏览器 。
2、用户携带 session id,完成登录操作后,服务器端就会更新 session 的状态,并将其 绑定到 Subject 中。
在登录前的 session 生成时,session 还不知道它要绑定到哪个用户上,仅仅是生成了一个标记了主机地址的SimpleSession 对象。当用户调用 shiro 的登录验证方法 login()的时候,完成正常的验证操作,Shiro 就会将这个 session 绑定到当前用户 Subject 中。
public void login(AuthenticationToken token) throws AuthenticationException {Subject subject = securityManager.login(this, token);......Session session = subject.getSession(false);if (session != null) {this.session = decorate(session);} else {this.session = null;}
}
因此,我的解决思路就是,在完成 login 方法后,注销掉当前的 session ,并重新获得一个 新的session ,绑定到 Subject 中。
/**
* 点击登录执行的动作
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String loginVali() {// ...... 从request 中获取 username 和 passwordSubject currentUser = ShiroKit.getSubject();UsernamePasswordToken token = new UsernamePasswordToken(username, password.toCharArray());......currentUser.login(token);// 创建新的session 会话,并管理 shiro session idsessionProcessor.shiroSessionIdIntoPool();// Session 重置后,重新调用login()方法,其内部自动捆绑新的 session 到当前SubjectcurrentUser.login(token);return REDIRECT + "/";
}
这里我单独提取了一个方法,来完成这一操作:
注意,getSession() 方法需要我们传入一个参数,默认是 true,意思是,如果这个用户还没有session ,就为其创建一个新的 session,如果是false,那么就不为其创建。上图的红框内,我们首先获得了当前登录用户的 session 对象,并调用了 stop 方法,将其注销,再为用户生成 一个 新的 session 对象。其他的部分,主要是为了维护一个 session id 池,将用户名与 session id的对应关系保存在 一个 map 中,方便管理员用户找到其他用户的 session id,并执行相关操作。
注意,在生成新的 session 后,必须重复调用一下 login 方法,将其再次绑定到当前用户中!
三、测试结果演示
用户打开登录界面,依然会获得一个服务端返回的 session id :
黑客窃取到这个 session id ,并拷贝到自己的登陆工具中(演示使用的是Post Man):
用户输入用户名和密码,登录到首页后,可以看到,虽然地址栏中使用了旧的 session id 完成登录操作,但是cookie 中的已经变成了新的 session id ,并为接下来的请求提供了新的 session :
此时,如果黑客使用旧的 session id 再次执行登录,就无法成功(请对比修改前的效果):
由此可见,我们在代码中刷新了 session 的操作已经成功。
总结
解决会话固定实际上就是在登录后,通过代码实现 session 的刷新(重新生成),保证登录前后使用不同的 session id 即可。虽然新的 session id 依然有可能被黑客窃取,但相对于固定的 session id ,也算在一定程度上提升了系统的安全性。
在解决会话固定的时候,我考虑了很多问题。
最初并不了解会话固定需要解决的最关键问题是什么,误以为只需要将用户过去的 session 注销即可,实则不然。导致这个想法的原因是由于我发现,当用户没有安全退出,而是直接关闭浏览器后,服务器端的 session 无法主动注销,需要等待到超时时间后,再执行注销操作。因此在 session 的处理方法中,我在用户 登录之后,先根据用户名 找到他上次没有注销,且未超时的 session ,并将其注销。但这还不是“会话固定”。
后来我了解到了会话固定实际上是登录前后拥有相同的 session id ,才意识到,注销以前使用过的 session 并没有真正解决了 session 会话固定的问题。
于是我开始思考在 Shiro 中如何创建新的 session,我本以为需要继承 DefaultSessionManager 或其子类并重写doCreateSession(SessionContext context) 方法,然后代替DefaultWebSessionManager 装载到 SecurityManager 中,但我发现 SessionContext 好像很难从请求中 手动构造出来(需要重写好几层继承关系的逻辑链),把整个 session 的创建过程和保存过程梳理了好几遍,都没找到很好的切入点,最后,还是通过 Subject 的 getSession 方法 发现了可执行的方案。
注销掉原来的 session 并通过 getSession(true) 方法成功刷新了 session 之后,我又发现,在重定向的时候,会导致系统找不到用户请求中的 session id,也就是说,我新生成的 session ,用户并没有成功接收到它的 session id。于是我又开始思考如何将 session id 放入 cookie 中返回给浏览器。但实际上,并不是如此,在生成session 的时候,SessionManager就已经将 session id 放入到了 response 的 cookie 中。
其实是用户和新生成的 session 并没有形成绑定,于是我又开始研究 Shiro 的 login()方法究竟做了哪些工作,这才发现,login 实际上在验证完成用户之后,会把已经生成的 session 绑定 到当前 Subject 上,于是,这才有了两次调用 login() 的操作。
currentUser.login(token);// 创建新的session 会话,并管理 shiro session idsessionProcessor.shiroSessionIdIntoPool();// Session 重置后,重新调用login()方法,其内部自动捆绑新的 session 到当前SubjectcurrentUser.login(token);
虽然实际修改的代码就只有两三行,但不得不说,会话固定的修改过程真是充满了挑战,前后一刻不停整整花了2大天时间。不过,通过自己的思考和努力解决一个问题真的非常有成就感。
综上,就是通过 Shiro 解决会话固定问题的全过程,欢迎大家文末留言。